[libc-commits] [libc] [libc][NFC] Add FieldTokenizer and FlatFileDatabase (PR #214720)
Jeff Bailey via libc-commits
libc-commits at lists.llvm.org
Fri Aug 7 05:51:45 PDT 2026
https://github.com/kaladron created https://github.com/llvm/llvm-project/pull/214720
Refactored the pwd database backend to separate file stream mechanics from record parsing using generic FlatFileDatabase and FieldTokenizer template engines.
POSIX user and group database queries must operate without dynamic heap allocations (ruling out getline/getdelim) and service both static process state (getpwent) and user-supplied reentrant buffers (getpwnam_r).
To satisfy these requirements with zero runtime overhead:
* Implemented FieldTokenizer to provide safe, in-place span tokenisation without pointer arithmetic or index-tracking hazards.
* Implemented FlatFileDatabase<EntryType> to encapsulate file lifecycle (setdb, enddb, getnext) and predicate-based linear search (lookup) parameterized directly on record types and free parse_line functions.
* Refactored PasswdParser into free parse_line and updated process-global passwd state to use FlatFileDatabase<struct passwd>.
This establishes the common scanning and search engine that will be reused directly by reentrant user lookups (getpwnam_r, getpwuid_r) and the upcoming group database subsystem (getgrent, getgrnam, getgrgid).
Assisted-by: Automated tooling, human reviewed.
>From 9946161dd518b7e4065261fd1f5f867098435280 Mon Sep 17 00:00:00 2001
From: Jeff Bailey <jbailey at raspberryginger.com>
Date: Fri, 7 Aug 2026 10:18:44 +0100
Subject: [PATCH] [libc][NFC] Add FieldTokenizer and FlatFileDatabase
Refactored the pwd database backend to separate file stream mechanics
from record parsing using generic FlatFileDatabase and FieldTokenizer
template engines.
POSIX user and group database queries must operate without dynamic heap
allocations (ruling out getline/getdelim) and service both static process
state (getpwent) and user-supplied reentrant buffers (getpwnam_r).
To satisfy these requirements with zero runtime overhead:
* Implemented FieldTokenizer to provide safe, in-place span tokenisation
without pointer arithmetic or index-tracking hazards.
* Implemented FlatFileDatabase<EntryType> to encapsulate file lifecycle
(setdb, enddb, getnext) and predicate-based linear search (lookup)
parameterized directly on record types and free parse_line functions.
* Refactored PasswdParser into free parse_line and updated process-global
passwd state to use FlatFileDatabase<struct passwd>.
This establishes the common scanning and search engine that will be
reused directly by reentrant user lookups (getpwnam_r, getpwuid_r) and
the upcoming group database subsystem (getgrent, getgrnam, getgrgid).
Assisted-by: Automated tooling, human reviewed.
---
libc/src/pwd/CMakeLists.txt | 8 +
libc/src/pwd/field_tokenizer.h | 70 +++++++
libc/src/pwd/flat_file_db.h | 224 +++++++++++++++++++++
libc/src/pwd/pwd_utils.cpp | 171 ++--------------
libc/src/pwd/pwd_utils.h | 69 ++++++-
libc/test/src/pwd/CMakeLists.txt | 28 +++
libc/test/src/pwd/field_tokenizer_test.cpp | 150 ++++++++++++++
libc/test/src/pwd/flat_file_db_test.cpp | 178 ++++++++++++++++
libc/test/src/pwd/getpwent_test.cpp | 1 +
9 files changed, 742 insertions(+), 157 deletions(-)
create mode 100644 libc/src/pwd/field_tokenizer.h
create mode 100644 libc/src/pwd/flat_file_db.h
create mode 100644 libc/test/src/pwd/field_tokenizer_test.cpp
create mode 100644 libc/test/src/pwd/flat_file_db_test.cpp
diff --git a/libc/src/pwd/CMakeLists.txt b/libc/src/pwd/CMakeLists.txt
index 4cde157adf673..00699e2c41197 100644
--- a/libc/src/pwd/CMakeLists.txt
+++ b/libc/src/pwd/CMakeLists.txt
@@ -45,19 +45,27 @@ add_entrypoint_object(
add_object_library(
pwd_utils
HDRS
+ field_tokenizer.h
+ flat_file_db.h
pwd_utils.h
SRCS
pwd_utils.cpp
DEPENDS
libc.hdr.errno_macros
libc.hdr.stdio_macros
+ libc.hdr.types.gid_t
+ libc.hdr.types.size_t
libc.hdr.types.struct_passwd
+ libc.hdr.types.uid_t
libc.src.string.string_utils
+ libc.src.__support.CPP.optional
libc.src.__support.CPP.span
+ libc.src.__support.CPP.string_view
libc.src.__support.File.file
libc.src.__support.File.platform_file
libc.src.__support.ctype_utils
libc.src.__support.error_or
libc.src.__support.str_to_integer
+ libc.src.__support.macros.attributes
libc.src.__support.macros.config
)
diff --git a/libc/src/pwd/field_tokenizer.h b/libc/src/pwd/field_tokenizer.h
new file mode 100644
index 0000000000000..eb1a95d2121f1
--- /dev/null
+++ b/libc/src/pwd/field_tokenizer.h
@@ -0,0 +1,70 @@
+//===----------------------------------------------------------------------===//
+//
+// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
+// See https://llvm.org/LICENSE.txt for license information.
+// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
+//
+//===----------------------------------------------------------------------===//
+///
+/// \file
+/// In-place field tokenizer for colon-separated flat database files.
+///
+//===----------------------------------------------------------------------===//
+
+#ifndef LLVM_LIBC_SRC_PWD_FIELD_TOKENIZER_H
+#define LLVM_LIBC_SRC_PWD_FIELD_TOKENIZER_H
+
+#include "src/__support/CPP/optional.h"
+#include "src/__support/CPP/span.h"
+#include "src/__support/CPP/string_view.h"
+#include "src/__support/macros/attributes.h"
+#include "src/__support/macros/config.h"
+
+namespace LIBC_NAMESPACE_DECL {
+namespace internal {
+
+// In-place field tokenizer for delimited database records.
+template <char Separator> class FieldTokenizer {
+ char *data_ptr;
+ size_t data_len;
+
+public:
+ LIBC_INLINE constexpr explicit FieldTokenizer(cpp::span<char> buf)
+ : data_ptr(buf.data()), data_len(buf.size()) {}
+
+ // Extracts the next null-terminated field.
+ LIBC_INLINE cpp::optional<cpp::span<char>> next_field() {
+ if (data_len == 0)
+ return cpp::nullopt;
+
+ cpp::string_view sv(data_ptr, data_len);
+ size_t pos = sv.find_first_of(Separator);
+
+ // If a delimiter was found, replace it with a null terminator and return
+ // the field.
+ if (pos != cpp::string_view::npos) {
+ data_ptr[pos] = '\0';
+ auto field = cpp::span<char>(data_ptr, pos + 1);
+ data_ptr += pos + 1;
+ data_len -= pos + 1;
+ return field;
+ }
+
+ // If null-terminated without delimiters, return the remaining span as the
+ // final field.
+ if (cpp::span<char>(data_ptr, data_len).back() == '\0') {
+ auto field = cpp::span<char>(data_ptr, data_len);
+ data_ptr += data_len;
+ data_len = 0;
+ return field;
+ }
+
+ // Otherwise, no more fields remain.
+ return cpp::nullopt;
+ }
+};
+
+} // namespace internal
+} // namespace LIBC_NAMESPACE_DECL
+
+#endif // LLVM_LIBC_SRC_PWD_FIELD_TOKENIZER_H
diff --git a/libc/src/pwd/flat_file_db.h b/libc/src/pwd/flat_file_db.h
new file mode 100644
index 0000000000000..3bde2c6a7c659
--- /dev/null
+++ b/libc/src/pwd/flat_file_db.h
@@ -0,0 +1,224 @@
+//===----------------------------------------------------------------------===//
+//
+// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
+// See https://llvm.org/LICENSE.txt for license information.
+// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
+//
+//===----------------------------------------------------------------------===//
+///
+/// \file
+/// Generic flat-file database template engine.
+///
+//===----------------------------------------------------------------------===//
+
+#ifndef LLVM_LIBC_SRC_PWD_FLAT_FILE_DB_H
+#define LLVM_LIBC_SRC_PWD_FLAT_FILE_DB_H
+
+#include "hdr/errno_macros.h"
+#include "hdr/stdio_macros.h"
+#include "hdr/types/size_t.h"
+#include "src/__support/CPP/span.h"
+#include "src/__support/File/file.h"
+#include "src/__support/error_or.h"
+#include "src/__support/macros/attributes.h"
+#include "src/__support/macros/config.h"
+
+namespace LIBC_NAMESPACE_DECL {
+namespace internal {
+
+struct ReadLineResult {
+ size_t bytes_read;
+ bool truncated;
+};
+
+// Forward declaration of record parser for flat database files.
+template <typename EntryType>
+bool parse_line(cpp::span<char> line, EntryType *entry);
+
+// Generic flat colon-delimited database engine.
+//
+// \tparam EntryType The data structure type representing a record.
+template <typename EntryType> class FlatFileDatabase {
+private:
+ const char *file_path;
+ File *file = nullptr;
+
+ // Reads a single line from the given file into the provided buffer.
+ // Note: POSIX getline/getdelim cannot be used here because user database
+ // lookups (including reentrant _r variants) must operate in-place within a
+ // fixed, bounded buffer without dynamic heap allocations or realloc.
+ LIBC_INLINE static ErrorOr<ReadLineResult> read_line(File *f,
+ cpp::span<char> buf) {
+ if (!f || buf.size() < 2)
+ return Error(EINVAL);
+
+ f->lock();
+ size_t bytes_read = 0;
+ FileIOResult result(0);
+ bool truncated = false;
+
+ for (char &ch : buf.first(buf.size() - 1)) {
+ result = f->read_unlocked(&ch, 1);
+ if (result.has_error()) {
+ f->unlock();
+ return Error(result.error);
+ }
+ if (result.value != 1)
+ break;
+ ++bytes_read;
+ if (ch == '\n')
+ break;
+ }
+
+ auto read_span = buf.first(bytes_read);
+ if (result.value == 1 && !read_span.empty() && read_span.back() != '\n') {
+ truncated = true;
+ char c = '\0';
+ while (true) {
+ result = f->read_unlocked(&c, 1);
+ if (result.has_error()) {
+ f->unlock();
+ return Error(result.error);
+ }
+ if (result.value != 1 || c == '\n')
+ break;
+ }
+ }
+
+ bool has_error = f->error_unlocked();
+ f->unlock();
+
+ if (has_error)
+ return Error(EIO);
+
+ buf[bytes_read] = '\0';
+ return ReadLineResult{bytes_read, truncated};
+ }
+
+public:
+ LIBC_INLINE constexpr explicit FlatFileDatabase(const char *path)
+ : file_path(path) {}
+
+ // Sets or overrides the file path for database operations.
+ LIBC_INLINE void set_path(const char *path) {
+ if (!path)
+ return;
+ if (file) {
+ file->close();
+ file = nullptr;
+ }
+ file_path = path;
+ }
+
+ // Opens or rewinds the database file stream.
+ LIBC_INLINE ErrorOr<int> setdb() {
+ if (!file) {
+ auto result = openfile(file_path, "r");
+ if (!result.has_value())
+ return Error(result.error());
+ file = result.value();
+ return 0;
+ }
+ auto result = file->seek(0, SEEK_SET);
+ if (!result.has_value())
+ return Error(result.error());
+ return 0;
+ }
+
+ // Closes the database file stream.
+ LIBC_INLINE ErrorOr<int> enddb() {
+ if (file) {
+ int result = file->close();
+ file = nullptr;
+ if (result != 0)
+ return Error(result);
+ }
+ return 0;
+ }
+
+ // Reads and parses the next record from the database.
+ LIBC_INLINE ErrorOr<bool> getnext(EntryType *entry, cpp::span<char> buffer) {
+ if (!entry)
+ return Error(EINVAL);
+
+ if (!file) {
+ auto res = setdb();
+ if (!res.has_value())
+ return Error(res.error());
+ }
+
+ auto result = read_line(file, buffer);
+ if (!result.has_value())
+ return Error(result.error());
+
+ ReadLineResult res = result.value();
+ if (res.bytes_read == 0)
+ return false; // EOF
+
+ if (res.truncated)
+ return Error(ERANGE);
+
+ auto line = buffer.first(res.bytes_read);
+ if (!line.empty() && line.back() == '\n')
+ line.back() = '\0';
+
+ size_t valid_len = (!line.empty() && line.back() == '\0')
+ ? res.bytes_read
+ : (res.bytes_read + 1);
+ if (valid_len > buffer.size())
+ valid_len = buffer.size();
+
+ if (parse_line(buffer.subspan(0, valid_len), entry))
+ return true;
+
+ return Error(EINVAL);
+ }
+
+ // Iterates sequentially through records using a callback function.
+ template <typename Func>
+ LIBC_INLINE ErrorOr<int> iterate(Func func, cpp::span<char> buffer) {
+ auto res = setdb();
+ if (!res.has_value())
+ return Error(res.error());
+
+ EntryType entry;
+ while (true) {
+ auto next_res = getnext(&entry, buffer);
+ if (!next_res.has_value())
+ return Error(next_res.error());
+ if (!next_res.value())
+ break; // EOF
+ if (!func(entry))
+ break; // Stopped by callback
+ }
+ return 0;
+ }
+
+ // Searches for a record matching a given predicate.
+ template <typename Matcher>
+ LIBC_INLINE ErrorOr<bool> lookup(Matcher matcher, EntryType *entry,
+ cpp::span<char> buffer) {
+ if (!entry)
+ return Error(EINVAL);
+
+ bool found = false;
+ auto callback = [&found, matcher, entry](const EntryType &e) -> bool {
+ if (matcher(e)) {
+ *entry = e;
+ found = true;
+ return false; // Stop iteration
+ }
+ return true; // Continue searching
+ };
+
+ auto err = iterate(callback, buffer);
+ if (!err.has_value())
+ return Error(err.error());
+ return found;
+ }
+};
+
+} // namespace internal
+} // namespace LIBC_NAMESPACE_DECL
+
+#endif // LLVM_LIBC_SRC_PWD_FLAT_FILE_DB_H
diff --git a/libc/src/pwd/pwd_utils.cpp b/libc/src/pwd/pwd_utils.cpp
index aaf0faf37e4ea..0f51bdd9399a8 100644
--- a/libc/src/pwd/pwd_utils.cpp
+++ b/libc/src/pwd/pwd_utils.cpp
@@ -13,12 +13,11 @@
#include "src/pwd/pwd_utils.h"
#include "hdr/errno_macros.h"
-#include "hdr/stdio_macros.h"
+#include "hdr/types/gid_t.h"
#include "hdr/types/struct_passwd.h"
+#include "hdr/types/uid_t.h"
#include "src/__support/CPP/span.h"
-#include "src/__support/File/file.h"
-#include "src/__support/ctype_utils.h"
-#include "src/__support/str_to_integer.h"
+#include "src/pwd/flat_file_db.h"
#include "src/string/string_utils.h"
#ifndef LIBC_COPT_PWD_FILE_PATH
@@ -33,44 +32,8 @@ ErrorOr<struct passwd> parse_passwd_line(char *line) {
return Error(EINVAL);
struct passwd pwd;
- char *context = line;
-
- pwd.pw_name = string_token<false>(nullptr, ":", &context);
- if (!pwd.pw_name)
- return Error(EINVAL);
-
- pwd.pw_passwd = string_token<false>(nullptr, ":", &context);
- if (!pwd.pw_passwd)
- return Error(EINVAL);
-
- char *uid_str = string_token<false>(nullptr, ":", &context);
- if (!uid_str || !isdigit(uid_str[0]))
- return Error(EINVAL);
- auto uid_res = strtointeger<uid_t>(uid_str, 10);
- if (uid_res.has_error() || uid_res.parsed_len == 0 ||
- uid_str[uid_res.parsed_len] != '\0')
- return Error(EINVAL);
- pwd.pw_uid = uid_res.value;
-
- char *gid_str = string_token<false>(nullptr, ":", &context);
- if (!gid_str || !isdigit(gid_str[0]))
- return Error(EINVAL);
- auto gid_res = strtointeger<gid_t>(gid_str, 10);
- if (gid_res.has_error() || gid_res.parsed_len == 0 ||
- gid_str[gid_res.parsed_len] != '\0')
- return Error(EINVAL);
- pwd.pw_gid = gid_res.value;
-
- pwd.pw_gecos = string_token<false>(nullptr, ":", &context);
- if (!pwd.pw_gecos)
- return Error(EINVAL);
-
- pwd.pw_dir = string_token<false>(nullptr, ":", &context);
- if (!pwd.pw_dir)
- return Error(EINVAL);
-
- pwd.pw_shell = string_token<false>(nullptr, ":", &context);
- if (!pwd.pw_shell)
+ size_t len = internal::string_length(line);
+ if (!parse_line(cpp::span<char>(line, len + 1), &pwd))
return Error(EINVAL);
return pwd;
@@ -80,129 +43,25 @@ ErrorOr<struct passwd> parse_passwd_line(char *line) {
namespace passwd {
-static File *pwd_file = nullptr;
-static const char *pwd_file_path = LIBC_COPT_PWD_FILE_PATH;
+static internal::FlatFileDatabase<struct passwd> db(LIBC_COPT_PWD_FILE_PATH);
// Note: These static buffers are process-global and NOT protected by a mutex
// at this stage. POSIX getpwent is non-reentrant.
static char line_buffer[1024];
static struct passwd pwd_entry;
-void TESTONLY_set_passwd_path(const char *path) {
- if (!path)
- return;
- if (pwd_file) {
- pwd_file->close();
- pwd_file = nullptr;
- }
- pwd_file_path = path;
-}
-
-ErrorOr<int> open() {
- if (!pwd_file) {
- auto result = openfile(pwd_file_path, "r");
- if (!result.has_value())
- return Error(result.error());
- pwd_file = result.value();
- } else {
- auto result = pwd_file->seek(0, SEEK_SET);
- if (!result.has_value())
- return Error(result.error());
- }
- return 0;
-}
-
-ErrorOr<int> close() {
- if (pwd_file) {
- int result = pwd_file->close();
- pwd_file = nullptr;
- if (result != 0)
- return Error(result);
- }
- return 0;
-}
-
-struct ReadLineResult {
- size_t bytes_read;
- bool truncated;
-};
-
-// Reads a line from the given file into buf.
-static ErrorOr<ReadLineResult> read_line(File *f, cpp::span<char> buf) {
- if (!f || buf.empty())
- return Error(EINVAL);
-
- f->lock();
- size_t bytes_read = 0;
- FileIOResult result(0);
- bool truncated = false;
-
- for (char &ch : buf.first(buf.size() - 1)) {
- result = f->read_unlocked(&ch, 1);
- if (result.has_error()) {
- f->unlock();
- return Error(result.error);
- }
- if (result.value != 1)
- break;
- ++bytes_read;
- if (ch == '\n')
- break;
- }
-
- if (result.value == 1 && bytes_read > 0 && buf[bytes_read - 1] != '\n') {
- truncated = true;
- char c = '\0';
- while (true) {
- result = f->read_unlocked(&c, 1);
- if (result.has_error()) {
- f->unlock();
- return Error(result.error);
- }
- if (result.value != 1 || c == '\n')
- break;
- }
- }
+void TESTONLY_set_passwd_path(const char *path) { db.set_path(path); }
- bool has_error = f->error_unlocked();
- f->unlock();
+ErrorOr<int> open() { return db.setdb(); }
- if (has_error)
- return Error(EIO);
-
- buf[bytes_read] = '\0';
- return ReadLineResult{bytes_read, truncated};
-}
+ErrorOr<int> close() { return db.enddb(); }
ErrorOr<struct passwd *> read_next() {
- if (!pwd_file) {
- auto result = open();
- if (!result.has_value())
- return Error(result.error());
- }
-
- while (true) {
- auto result = read_line(pwd_file, line_buffer);
- if (!result.has_value())
- return Error(result.error());
-
- ReadLineResult res = result.value();
- if (res.bytes_read == 0)
- return nullptr;
-
- if (res.truncated)
- return Error(EINVAL);
-
- size_t len = res.bytes_read;
- if (len > 0 && line_buffer[len - 1] == '\n')
- line_buffer[len - 1] = '\0';
-
- auto passwd_or = internal::parse_passwd_line(line_buffer);
- if (!passwd_or.has_value())
- return Error(passwd_or.error());
-
- pwd_entry = passwd_or.value();
- return &pwd_entry;
- }
+ auto res = db.getnext(&pwd_entry, line_buffer);
+ if (!res.has_value())
+ return Error(res.error());
+ if (!res.value())
+ return nullptr;
+ return &pwd_entry;
}
} // namespace passwd
diff --git a/libc/src/pwd/pwd_utils.h b/libc/src/pwd/pwd_utils.h
index 7b48b2cabc8f5..6c6e9e84afe62 100644
--- a/libc/src/pwd/pwd_utils.h
+++ b/libc/src/pwd/pwd_utils.h
@@ -7,20 +7,87 @@
//===----------------------------------------------------------------------===//
///
/// \file
-/// Declarations of helper functions for pwd.
+/// Declarations of helper functions and parser for pwd.
///
//===----------------------------------------------------------------------===//
#ifndef LLVM_LIBC_SRC_PWD_PWD_UTILS_H
#define LLVM_LIBC_SRC_PWD_PWD_UTILS_H
+#include "hdr/errno_macros.h"
+#include "hdr/types/gid_t.h"
#include "hdr/types/struct_passwd.h"
+#include "hdr/types/uid_t.h"
+#include "src/__support/CPP/span.h"
+#include "src/__support/ctype_utils.h"
#include "src/__support/error_or.h"
+#include "src/__support/macros/attributes.h"
#include "src/__support/macros/config.h"
+#include "src/__support/str_to_integer.h"
+#include "src/pwd/field_tokenizer.h"
+#include "src/pwd/flat_file_db.h"
+#include "src/string/string_utils.h"
namespace LIBC_NAMESPACE_DECL {
namespace internal {
+// Parses a colon-separated line in-place into a struct passwd.
+template <>
+LIBC_INLINE bool parse_line<struct passwd>(cpp::span<char> line,
+ struct passwd *pwd) {
+ if (line.empty() || !pwd)
+ return false;
+
+ FieldTokenizer<':'> tokenizer(line);
+
+ auto name = tokenizer.next_field();
+ if (!name)
+ return false;
+ pwd->pw_name = name->data();
+
+ auto passwd = tokenizer.next_field();
+ if (!passwd)
+ return false;
+ pwd->pw_passwd = passwd->data();
+
+ auto uid_str = tokenizer.next_field();
+ if (!uid_str || uid_str->empty() || !internal::isdigit(uid_str->front()))
+ return false;
+ auto uid_res = internal::strtointeger<uid_t>(uid_str->data(), 10);
+ if (uid_res.has_error() || uid_res.parsed_len <= 0 ||
+ static_cast<size_t>(uid_res.parsed_len) >= uid_str->size() ||
+ (*uid_str)[uid_res.parsed_len] != '\0')
+ return false;
+ pwd->pw_uid = uid_res.value;
+
+ auto gid_str = tokenizer.next_field();
+ if (!gid_str || gid_str->empty() || !internal::isdigit(gid_str->front()))
+ return false;
+ auto gid_res = internal::strtointeger<gid_t>(gid_str->data(), 10);
+ if (gid_res.has_error() || gid_res.parsed_len <= 0 ||
+ static_cast<size_t>(gid_res.parsed_len) >= gid_str->size() ||
+ (*gid_str)[gid_res.parsed_len] != '\0')
+ return false;
+ pwd->pw_gid = gid_res.value;
+
+ auto gecos = tokenizer.next_field();
+ if (!gecos)
+ return false;
+ pwd->pw_gecos = gecos->data();
+
+ auto dir = tokenizer.next_field();
+ if (!dir)
+ return false;
+ pwd->pw_dir = dir->data();
+
+ auto shell = tokenizer.next_field();
+ if (!shell)
+ return false;
+ pwd->pw_shell = shell->data();
+
+ return true;
+}
+
// Parses a colon-separated password database line into a struct passwd.
ErrorOr<struct passwd> parse_passwd_line(char *line);
diff --git a/libc/test/src/pwd/CMakeLists.txt b/libc/test/src/pwd/CMakeLists.txt
index 06b7d59febe11..130811736f99e 100644
--- a/libc/test/src/pwd/CMakeLists.txt
+++ b/libc/test/src/pwd/CMakeLists.txt
@@ -4,6 +4,33 @@ if(NOT TARGET libc.src.pwd.pwd_utils)
return()
endif()
+add_libc_unittest(
+ field_tokenizer_test
+ SUITE
+ libc_pwd_unittests
+ SRCS
+ field_tokenizer_test.cpp
+ DEPENDS
+ libc.src.__support.CPP.span
+ libc.src.pwd.pwd_utils
+)
+
+add_libc_unittest(
+ flat_file_db_test
+ SUITE
+ libc_pwd_unittests
+ SRCS
+ flat_file_db_test.cpp
+ DEPENDS
+ libc.hdr.errno_macros
+ libc.src.__support.CPP.span
+ libc.src.__support.File.file
+ libc.src.__support.File.platform_file
+ libc.src.pwd.pwd_utils
+ libc.src.stdio.remove
+ libc.src.string.string_utils
+)
+
add_libc_unittest(
pwd_utils_test
SUITE
@@ -23,6 +50,7 @@ add_libc_unittest(
SRCS
getpwent_test.cpp
DEPENDS
+ libc.hdr.errno_macros
libc.hdr.types.struct_passwd
libc.src.__support.File.file
libc.src.__support.File.platform_file
diff --git a/libc/test/src/pwd/field_tokenizer_test.cpp b/libc/test/src/pwd/field_tokenizer_test.cpp
new file mode 100644
index 0000000000000..a84ec18deabc4
--- /dev/null
+++ b/libc/test/src/pwd/field_tokenizer_test.cpp
@@ -0,0 +1,150 @@
+//===----------------------------------------------------------------------===//
+//
+// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
+// See https://llvm.org/LICENSE.txt for license information.
+// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
+//
+//===----------------------------------------------------------------------===//
+///
+/// \file
+/// Unit tests for FieldTokenizer.
+///
+//===----------------------------------------------------------------------===//
+
+#include "src/__support/CPP/span.h"
+#include "src/pwd/field_tokenizer.h"
+#include "test/UnitTest/Test.h"
+
+TEST(LlvmLibcFieldTokenizerTest, StandardPasswdLine) {
+ char line[] = "root:x:0:0:root:/root:/bin/bash";
+ LIBC_NAMESPACE::internal::FieldTokenizer<':'> tokenizer(
+ LIBC_NAMESPACE::cpp::span<char>(line, sizeof(line)));
+
+ auto f1 = tokenizer.next_field();
+ ASSERT_TRUE(f1.has_value());
+ ASSERT_STREQ(f1->data(), "root");
+
+ auto f2 = tokenizer.next_field();
+ ASSERT_TRUE(f2.has_value());
+ ASSERT_STREQ(f2->data(), "x");
+
+ auto f3 = tokenizer.next_field();
+ ASSERT_TRUE(f3.has_value());
+ ASSERT_STREQ(f3->data(), "0");
+
+ auto f4 = tokenizer.next_field();
+ ASSERT_TRUE(f4.has_value());
+ ASSERT_STREQ(f4->data(), "0");
+
+ auto f5 = tokenizer.next_field();
+ ASSERT_TRUE(f5.has_value());
+ ASSERT_STREQ(f5->data(), "root");
+
+ auto f6 = tokenizer.next_field();
+ ASSERT_TRUE(f6.has_value());
+ ASSERT_STREQ(f6->data(), "/root");
+
+ auto f7 = tokenizer.next_field();
+ ASSERT_TRUE(f7.has_value());
+ ASSERT_STREQ(f7->data(), "/bin/bash");
+
+ auto f8 = tokenizer.next_field();
+ ASSERT_FALSE(f8.has_value());
+}
+
+TEST(LlvmLibcFieldTokenizerTest, EmptyFields) {
+ char line[] = "a::c:";
+ LIBC_NAMESPACE::internal::FieldTokenizer<':'> tokenizer(
+ LIBC_NAMESPACE::cpp::span<char>(line, sizeof(line)));
+
+ auto f1 = tokenizer.next_field();
+ ASSERT_TRUE(f1.has_value());
+ ASSERT_STREQ(f1->data(), "a");
+
+ auto f2 = tokenizer.next_field();
+ ASSERT_TRUE(f2.has_value());
+ ASSERT_STREQ(f2->data(), "");
+
+ auto f3 = tokenizer.next_field();
+ ASSERT_TRUE(f3.has_value());
+ ASSERT_STREQ(f3->data(), "c");
+
+ auto f4 = tokenizer.next_field();
+ ASSERT_TRUE(f4.has_value());
+ ASSERT_STREQ(f4->data(), "");
+
+ auto f5 = tokenizer.next_field();
+ ASSERT_FALSE(f5.has_value());
+}
+
+TEST(LlvmLibcFieldTokenizerTest, LeadingAndConsecutiveSeparators) {
+ char line[] = ":first::last";
+ LIBC_NAMESPACE::internal::FieldTokenizer<':'> tokenizer(
+ LIBC_NAMESPACE::cpp::span<char>(line, sizeof(line)));
+
+ auto f1 = tokenizer.next_field();
+ ASSERT_TRUE(f1.has_value());
+ ASSERT_STREQ(f1->data(), "");
+
+ auto f2 = tokenizer.next_field();
+ ASSERT_TRUE(f2.has_value());
+ ASSERT_STREQ(f2->data(), "first");
+
+ auto f3 = tokenizer.next_field();
+ ASSERT_TRUE(f3.has_value());
+ ASSERT_STREQ(f3->data(), "");
+
+ auto f4 = tokenizer.next_field();
+ ASSERT_TRUE(f4.has_value());
+ ASSERT_STREQ(f4->data(), "last");
+
+ auto f5 = tokenizer.next_field();
+ ASSERT_FALSE(f5.has_value());
+}
+
+TEST(LlvmLibcFieldTokenizerTest, SingleField) {
+ char line[] = "single";
+ LIBC_NAMESPACE::internal::FieldTokenizer<':'> tokenizer(
+ LIBC_NAMESPACE::cpp::span<char>(line, sizeof(line)));
+
+ auto f1 = tokenizer.next_field();
+ ASSERT_TRUE(f1.has_value());
+ ASSERT_STREQ(f1->data(), "single");
+
+ auto f2 = tokenizer.next_field();
+ ASSERT_FALSE(f2.has_value());
+}
+
+TEST(LlvmLibcFieldTokenizerTest, EmptyBuffer) {
+ char line[] = "";
+ LIBC_NAMESPACE::internal::FieldTokenizer<':'> tokenizer(
+ LIBC_NAMESPACE::cpp::span<char>(line, sizeof(line)));
+
+ auto f1 = tokenizer.next_field();
+ ASSERT_TRUE(f1.has_value());
+ ASSERT_STREQ(f1->data(), "");
+
+ auto f2 = tokenizer.next_field();
+ ASSERT_FALSE(f2.has_value());
+}
+
+TEST(LlvmLibcFieldTokenizerTest, CustomSeparator) {
+ char line[] = "foo,bar,baz";
+ LIBC_NAMESPACE::internal::FieldTokenizer<','> tokenizer(
+ LIBC_NAMESPACE::cpp::span<char>(line, sizeof(line)));
+
+ auto f1 = tokenizer.next_field();
+ ASSERT_TRUE(f1.has_value());
+ ASSERT_STREQ(f1->data(), "foo");
+
+ auto f2 = tokenizer.next_field();
+ ASSERT_TRUE(f2.has_value());
+ ASSERT_STREQ(f2->data(), "bar");
+
+ auto f3 = tokenizer.next_field();
+ ASSERT_TRUE(f3.has_value());
+ ASSERT_STREQ(f3->data(), "baz");
+
+ auto f4 = tokenizer.next_field();
+ ASSERT_FALSE(f4.has_value());
+}
diff --git a/libc/test/src/pwd/flat_file_db_test.cpp b/libc/test/src/pwd/flat_file_db_test.cpp
new file mode 100644
index 0000000000000..f386935235319
--- /dev/null
+++ b/libc/test/src/pwd/flat_file_db_test.cpp
@@ -0,0 +1,178 @@
+//===----------------------------------------------------------------------===//
+//
+// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
+// See https://llvm.org/LICENSE.txt for license information.
+// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
+//
+//===----------------------------------------------------------------------===//
+///
+/// \file
+/// Unit tests for FlatFileDatabase.
+///
+//===----------------------------------------------------------------------===//
+
+#include "hdr/errno_macros.h"
+#include "src/__support/CPP/span.h"
+#include "src/__support/File/file.h"
+#include "src/pwd/field_tokenizer.h"
+#include "src/pwd/flat_file_db.h"
+#include "src/stdio/remove.h"
+#include "src/string/string_utils.h"
+#include "test/UnitTest/ErrnoCheckingTest.h"
+#include "test/UnitTest/ErrnoSetterMatcher.h"
+#include "test/UnitTest/Test.h"
+
+namespace {
+
+struct SimpleTestEntry {
+ const char *key;
+ const char *val;
+};
+
+class HermeticFile {
+ char path[256];
+
+public:
+ HermeticFile(const char *file_path, const char *content) {
+ LIBC_NAMESPACE::internal::strlcpy(path, file_path, sizeof(path));
+
+ auto file_or = LIBC_NAMESPACE::openfile(path, "w");
+ if (file_or.has_value()) {
+ auto *f = file_or.value();
+ size_t len = LIBC_NAMESPACE::internal::string_length(content);
+ f->write(content, len);
+ f->close();
+ }
+ }
+
+ ~HermeticFile() { LIBC_NAMESPACE::remove(path); }
+
+ const char *get_path() const { return path; }
+};
+
+class LlvmLibcFlatFileDbTest
+ : public LIBC_NAMESPACE::testing::ErrnoCheckingTest {};
+
+} // namespace
+
+namespace LIBC_NAMESPACE_DECL {
+namespace internal {
+
+template <>
+inline bool parse_line<SimpleTestEntry>(cpp::span<char> line,
+ SimpleTestEntry *entry) {
+ if (line.empty() || !entry)
+ return false;
+ FieldTokenizer<':'> tokenizer(line);
+ auto k = tokenizer.next_field();
+ if (!k)
+ return false;
+ entry->key = k->data();
+
+ auto v = tokenizer.next_field();
+ if (!v)
+ return false;
+ entry->val = v->data();
+
+ return true;
+}
+
+} // namespace internal
+} // namespace LIBC_NAMESPACE_DECL
+
+TEST_F(LlvmLibcFlatFileDbTest, GetNextAndLookup) {
+ const char *content = "user1:secret1\nuser2:secret2\n";
+ HermeticFile test_file(libc_make_test_file_path("flat_db_test.test"),
+ content);
+
+ LIBC_NAMESPACE::internal::FlatFileDatabase<SimpleTestEntry> db(
+ test_file.get_path());
+ char buffer[128];
+ SimpleTestEntry entry;
+
+ // First record
+ auto r1 = db.getnext(&entry, buffer);
+ ASSERT_TRUE(r1.has_value());
+ ASSERT_TRUE(r1.value());
+ ASSERT_STREQ(entry.key, "user1");
+ ASSERT_STREQ(entry.val, "secret1");
+
+ // Second record
+ auto r2 = db.getnext(&entry, buffer);
+ ASSERT_TRUE(r2.has_value());
+ ASSERT_TRUE(r2.value());
+ ASSERT_STREQ(entry.key, "user2");
+ ASSERT_STREQ(entry.val, "secret2");
+
+ // EOF
+ auto r3 = db.getnext(&entry, buffer);
+ ASSERT_TRUE(r3.has_value());
+ ASSERT_FALSE(r3.value());
+
+ // Rewind and lookup
+ db.setdb();
+ auto matcher = [](const SimpleTestEntry &e) {
+ return LIBC_NAMESPACE::cpp::string_view(e.key) == "user2";
+ };
+ auto lookup_res = db.lookup(matcher, &entry, buffer);
+ ASSERT_TRUE(lookup_res.has_value());
+ ASSERT_TRUE(lookup_res.value());
+ ASSERT_STREQ(entry.key, "user2");
+ ASSERT_STREQ(entry.val, "secret2");
+
+ db.enddb();
+}
+
+TEST_F(LlvmLibcFlatFileDbTest, LookupNotFound) {
+ const char *content = "foo:bar\n";
+ HermeticFile test_file(libc_make_test_file_path("flat_db_not_found.test"),
+ content);
+
+ LIBC_NAMESPACE::internal::FlatFileDatabase<SimpleTestEntry> db(
+ test_file.get_path());
+ char buffer[128];
+ SimpleTestEntry entry;
+
+ auto matcher = [](const SimpleTestEntry &e) {
+ return LIBC_NAMESPACE::cpp::string_view(e.key) == "nonexistent";
+ };
+ auto lookup_res = db.lookup(matcher, &entry, buffer);
+ ASSERT_TRUE(lookup_res.has_value());
+ ASSERT_FALSE(lookup_res.value());
+
+ db.enddb();
+}
+
+TEST_F(LlvmLibcFlatFileDbTest, TruncatedLineReturnsErange) {
+ const char *content = "verylongkeyname:verylongvaluename\n";
+ HermeticFile test_file(libc_make_test_file_path("flat_db_trunc.test"),
+ content);
+
+ LIBC_NAMESPACE::internal::FlatFileDatabase<SimpleTestEntry> db(
+ test_file.get_path());
+ char small_buffer[8];
+ SimpleTestEntry entry;
+
+ auto res = db.getnext(&entry, small_buffer);
+ ASSERT_FALSE(res.has_value());
+ ASSERT_EQ(res.error(), ERANGE);
+
+ db.enddb();
+}
+
+TEST_F(LlvmLibcFlatFileDbTest, MalformedLineReturnsEinval) {
+ const char *content = "invalid_line_without_delimiter\n";
+ HermeticFile test_file(libc_make_test_file_path("flat_db_malformed.test"),
+ content);
+
+ LIBC_NAMESPACE::internal::FlatFileDatabase<SimpleTestEntry> db(
+ test_file.get_path());
+ char buffer[128];
+ SimpleTestEntry entry;
+
+ auto res = db.getnext(&entry, buffer);
+ ASSERT_FALSE(res.has_value());
+ ASSERT_EQ(res.error(), EINVAL);
+
+ db.enddb();
+}
diff --git a/libc/test/src/pwd/getpwent_test.cpp b/libc/test/src/pwd/getpwent_test.cpp
index 5fca34d9b3b2b..b8dbd9a4cb161 100644
--- a/libc/test/src/pwd/getpwent_test.cpp
+++ b/libc/test/src/pwd/getpwent_test.cpp
@@ -11,6 +11,7 @@
///
//===----------------------------------------------------------------------===//
+#include "hdr/errno_macros.h"
#include "hdr/types/struct_passwd.h"
#include "src/__support/File/file.h"
#include "src/__support/libc_errno.h"
More information about the libc-commits
mailing list