[libc-commits] [libc] [libc] Support dynamically-grown lines in FlatFileDatabase (PR #223811)
Jeff Bailey via libc-commits
libc-commits at lists.llvm.org
Wed Sep 16 07:49:49 PDT 2026
https://github.com/kaladron updated https://github.com/llvm/llvm-project/pull/223811
>From 09173b049210d972f44b5dab22c9bed09aae270c Mon Sep 17 00:00:00 2001
From: Jeff Bailey <jbailey at raspberryginger.com>
Date: Mon, 14 Sep 2026 07:59:29 +0100
Subject: [PATCH 1/2] [libc] Support dynamically-grown lines in
FlatFileDatabase
Add pwd::DynamicBuffer and overloads of FlatFileDatabase::getnext and
lookup that read into a growable buffer rather than a fixed span.
parse_line now returns ErrorOr<void> and accepts the buffer and line
length so parsers can report ERANGE or EINVAL directly.
Fixed-buffer getnext keeps its signature and never allocates.
Fixed-buffer lookup skips unrelated oversized preceding records using a
temporary ScopedDynamicBuffer and reports ERANGE only when the matched
record does not fit in the caller's buffer.
DynamicBuffer is trivially destructible by design so that it can be a
constant-initialised process global. Storage is released explicitly by
whoever owns it.
* Add pwd::DynamicBuffer in src/__support/pwd/dynamic_buffer.h
* Add growable overloads to FlatFileDatabase
* Update parse_line<struct passwd> signature
* Add unit tests for dynamically-grown lines in FlatFileDatabase
Assisted-by: Automated tooling, human reviewed.
---
libc/src/__support/pwd/CMakeLists.txt | 19 +-
libc/src/__support/pwd/dynamic_buffer.h | 104 +++++++++
libc/src/__support/pwd/flat_file_db.h | 206 ++++++++++++++++--
libc/src/pwd/pwd_utils.cpp | 6 +-
libc/src/pwd/pwd_utils.h | 41 ++--
libc/test/src/__support/pwd/CMakeLists.txt | 4 +
.../src/__support/pwd/flat_file_db_test.cpp | 150 ++++++++++---
7 files changed, 463 insertions(+), 67 deletions(-)
create mode 100644 libc/src/__support/pwd/dynamic_buffer.h
diff --git a/libc/src/__support/pwd/CMakeLists.txt b/libc/src/__support/pwd/CMakeLists.txt
index 5ea67f910784e..a7c17936afb4b 100644
--- a/libc/src/__support/pwd/CMakeLists.txt
+++ b/libc/src/__support/pwd/CMakeLists.txt
@@ -10,16 +10,33 @@ add_header_library(
libc.src.__support.macros.config
)
+add_header_library(
+ dynamic_buffer
+ HDRS
+ dynamic_buffer.h
+ DEPENDS
+ libc.hdr.func.free
+ libc.hdr.func.realloc
+ libc.hdr.types.size_t
+ libc.src.__support.CPP.limits
+ libc.src.__support.CPP.span
+ libc.src.__support.CPP.type_traits
+ libc.src.__support.macros.attributes
+ libc.src.__support.macros.config
+)
+
add_header_library(
flat_file_db
HDRS
flat_file_db.h
DEPENDS
- .field_tokenizer
+ .dynamic_buffer
libc.hdr.errno_macros
libc.hdr.stdio_macros
+ libc.hdr.types.off_t
libc.hdr.types.size_t
libc.src.__support.CPP.functional
+ libc.src.__support.CPP.limits
libc.src.__support.CPP.span
libc.src.__support.File.file
libc.src.__support.error_or
diff --git a/libc/src/__support/pwd/dynamic_buffer.h b/libc/src/__support/pwd/dynamic_buffer.h
new file mode 100644
index 0000000000000..59f205d4161e4
--- /dev/null
+++ b/libc/src/__support/pwd/dynamic_buffer.h
@@ -0,0 +1,104 @@
+//===----------------------------------------------------------------------===//
+//
+// 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
+/// Growable byte buffer with explicit lifetime management.
+///
+//===----------------------------------------------------------------------===//
+
+#ifndef LLVM_LIBC_SRC___SUPPORT_PWD_DYNAMIC_BUFFER_H
+#define LLVM_LIBC_SRC___SUPPORT_PWD_DYNAMIC_BUFFER_H
+
+#include "hdr/func/free.h"
+#include "hdr/func/realloc.h"
+#include "hdr/types/size_t.h"
+#include "src/__support/CPP/limits.h"
+#include "src/__support/CPP/span.h"
+#include "src/__support/CPP/type_traits/is_trivially_destructible.h"
+#include "src/__support/macros/attributes.h"
+#include "src/__support/macros/config.h"
+
+namespace LIBC_NAMESPACE_DECL {
+namespace pwd {
+
+// A heap-backed, growable byte buffer for the flat-file database engine.
+//
+// This type is trivially destructible by design, so an instance can be a
+// constant-initialised process global. The owner releases the storage
+// explicitly when done.
+class DynamicBuffer {
+ static constexpr size_t INITIAL_CAPACITY = 256;
+
+ char *ptr = nullptr;
+ size_t cap = 0;
+
+public:
+ LIBC_INLINE constexpr DynamicBuffer() = default;
+
+ DynamicBuffer(const DynamicBuffer &) = delete;
+ DynamicBuffer &operator=(const DynamicBuffer &) = delete;
+
+ // Grows the buffer to hold at least new_capacity bytes, preserving the
+ // existing contents. Capacity doubles so that repeated growth stays linear
+ // in the number of bytes read. Returns false if allocation failed, in which
+ // case the buffer is left untouched.
+ [[nodiscard]] LIBC_INLINE bool reserve(size_t new_capacity) {
+ if (new_capacity <= cap)
+ return true;
+
+ size_t next = cap == 0 ? INITIAL_CAPACITY : cap;
+ while (next < new_capacity) {
+ if (next > cpp::numeric_limits<size_t>::max() / 2)
+ return false;
+ next *= 2;
+ }
+
+ void *new_ptr = ::realloc(ptr, next);
+ if (new_ptr == nullptr)
+ return false;
+
+ ptr = static_cast<char *>(new_ptr);
+ cap = next;
+ return true;
+ }
+
+ // Doubles the current capacity, or allocates the initial capacity when the
+ // buffer is empty. Returns false if allocation failed.
+ [[nodiscard]] LIBC_INLINE bool grow() {
+ if (cap == cpp::numeric_limits<size_t>::max())
+ return false;
+ return reserve(cap == 0 ? INITIAL_CAPACITY : cap + 1);
+ }
+
+ // Frees the storage and returns the buffer to its empty state. Safe to call
+ // more than once.
+ LIBC_INLINE void release() {
+ ::free(ptr);
+ ptr = nullptr;
+ cap = 0;
+ }
+
+ [[nodiscard]] LIBC_INLINE cpp::span<char> span() { return {ptr, cap}; }
+ [[nodiscard]] LIBC_INLINE size_t capacity() const { return cap; }
+};
+
+static_assert(cpp::is_trivially_destructible<DynamicBuffer>::value,
+ "DynamicBuffer must be trivially destructible");
+
+// RAII wrapper around DynamicBuffer for stack-local buffers.
+class ScopedDynamicBuffer : public DynamicBuffer {
+public:
+ using DynamicBuffer::DynamicBuffer;
+
+ LIBC_INLINE ~ScopedDynamicBuffer() { this->release(); }
+};
+
+} // namespace pwd
+} // namespace LIBC_NAMESPACE_DECL
+
+#endif // LLVM_LIBC_SRC___SUPPORT_PWD_DYNAMIC_BUFFER_H
diff --git a/libc/src/__support/pwd/flat_file_db.h b/libc/src/__support/pwd/flat_file_db.h
index df0b168d17556..318faf0cf5fac 100644
--- a/libc/src/__support/pwd/flat_file_db.h
+++ b/libc/src/__support/pwd/flat_file_db.h
@@ -16,27 +16,37 @@
#include "hdr/errno_macros.h"
#include "hdr/stdio_macros.h"
+#include "hdr/types/off_t.h"
#include "hdr/types/size_t.h"
#include "src/__support/CPP/functional.h"
+#include "src/__support/CPP/limits.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"
+#include "src/__support/pwd/dynamic_buffer.h"
namespace LIBC_NAMESPACE_DECL {
namespace pwd {
+// Struct to hold the result of a line read operation.
struct ReadLineResult {
size_t bytes_read;
+ size_t raw_bytes_consumed;
bool truncated;
- // True only when no data was read because the file stream reached EOF.
+ // True only when zero bytes were read because the stream was already at EOF.
+ // A final line without a trailing newline returns bytes_read > 0 and
+ // eof == false; the following call returns bytes_read == 0 and eof == true.
bool eof;
};
-// Forward declaration of record parser for flat database files.
+// Parses a record in place and fills entry.
+// If the buffer is too small for auxiliary structures (such as pointer arrays),
+// specializations must return Error(ERANGE) prior to modifying the buffer.
template <typename EntryType>
-bool parse_line(cpp::span<char> line, EntryType *entry);
+ErrorOr<void> parse_line(cpp::span<char> buffer, size_t line_len,
+ EntryType *entry);
// Generic flat colon-delimited database engine.
template <typename EntryType> class FlatFileDatabase {
@@ -46,12 +56,18 @@ template <typename EntryType> class FlatFileDatabase {
private:
const char *file_path;
File *file = nullptr;
+ off_t current_offset = 0;
+ off_t last_line_start = 0;
// Reads a single line from the given file into the provided buffer, stripping
- // any trailing '\n' and ensuring the result is null-terminated.
+ // any trailing '\n' and ensuring the result is null-terminated. A line too
+ // long for the buffer is reported as truncated.
+ //
// 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.
+ // iteration and lookups must operate in-place within a fixed, bounded buffer
+ // without dynamic heap allocations during getnext. See read_line_growing for
+ // the variant used by the non-reentrant interfaces, which own their buffer
+ // and may grow it.
LIBC_INLINE static ErrorOr<ReadLineResult> read_line(File *f,
cpp::span<char> buf) {
if (!f)
@@ -61,6 +77,7 @@ template <typename EntryType> class FlatFileDatabase {
File::FileLock lock(f);
size_t bytes_read = 0;
+ size_t raw_bytes_consumed = 0;
FileIOResult result(0);
bool truncated = false;
@@ -71,6 +88,7 @@ template <typename EntryType> class FlatFileDatabase {
if (result.value != 1)
break;
++bytes_read;
+ ++raw_bytes_consumed;
if (ch == '\n')
break;
}
@@ -79,14 +97,17 @@ template <typename EntryType> class FlatFileDatabase {
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())
return Error(result.error);
- if (result.value != 1 || c == '\n')
+ if (result.value != 1)
break;
+ ++raw_bytes_consumed;
+ if (c == '\n')
+ break;
+ truncated = true;
}
}
@@ -98,13 +119,67 @@ template <typename EntryType> class FlatFileDatabase {
--bytes_read;
buf[bytes_read] = '\0';
- return ReadLineResult{bytes_read, truncated, eof};
+ return ReadLineResult{bytes_read, raw_bytes_consumed, truncated, eof};
+ }
+
+ // Reads a single line into a caller-owned buffer, growing it as needed so
+ // that arbitrarily long records can be read. Otherwise behaves as read_line;
+ // the result is never truncated.
+ LIBC_INLINE static ErrorOr<ReadLineResult>
+ read_line_growing(File *f, DynamicBuffer &buf) {
+ if (!f)
+ return Error(EINVAL);
+
+ File::FileLock lock(f);
+ size_t bytes_read = 0;
+
+ while (true) {
+ // One byte for the character about to be read, one for the terminator.
+ if (bytes_read > cpp::numeric_limits<size_t>::max() - 2 ||
+ (bytes_read + 2 > buf.capacity() && !buf.reserve(bytes_read + 2))) {
+ char c = '\0';
+ while (true) {
+ FileIOResult drain = f->read_unlocked(&c, 1);
+ if (drain.has_error() || drain.value != 1 || c == '\n')
+ break;
+ }
+ return Error(ENOMEM);
+ }
+
+ char ch = '\0';
+ FileIOResult result = f->read_unlocked(&ch, 1);
+ if (result.has_error())
+ return Error(result.error);
+ if (result.value != 1)
+ break;
+
+ buf.span()[bytes_read++] = ch;
+ if (ch == '\n')
+ break;
+ }
+
+ if (f->error_unlocked())
+ return Error(EIO);
+
+ bool eof = (bytes_read == 0);
+ size_t raw_bytes_consumed = bytes_read;
+
+ // If the line ended with a newline, strip it.
+ if (bytes_read > 0 && buf.span()[bytes_read - 1] == '\n')
+ --bytes_read;
+
+ buf.span()[bytes_read] = '\0';
+ return ReadLineResult{bytes_read, raw_bytes_consumed, /*truncated=*/false,
+ eof};
}
public:
LIBC_INLINE constexpr explicit FlatFileDatabase(const char *path)
: file_path(path) {}
+ FlatFileDatabase(const FlatFileDatabase &) = delete;
+ FlatFileDatabase &operator=(const FlatFileDatabase &) = delete;
+
// Sets or overrides the file path for database operations.
LIBC_INLINE void set_path(const char *path) {
if (!path)
@@ -114,10 +189,14 @@ template <typename EntryType> class FlatFileDatabase {
file = nullptr;
}
file_path = path;
+ current_offset = 0;
+ last_line_start = 0;
}
// Opens or rewinds the database file stream.
LIBC_INLINE ErrorOr<void> setdb() {
+ current_offset = 0;
+ last_line_start = 0;
if (!file) {
auto result = openfile(file_path, "r");
if (!result.has_value())
@@ -128,11 +207,14 @@ template <typename EntryType> class FlatFileDatabase {
auto result = file->seek(0, SEEK_SET);
if (!result.has_value())
return Error(result.error());
+ file->clearerr();
return {};
}
// Closes the database file stream.
LIBC_INLINE ErrorOr<void> enddb() {
+ current_offset = 0;
+ last_line_start = 0;
if (file) {
int result = file->close();
file = nullptr;
@@ -142,9 +224,10 @@ template <typename EntryType> class FlatFileDatabase {
return {};
}
- // Reads and parses the next record from the database. Returns true if an
- // entry was read, false if EOF was reached, or an Error on failure. Blank
- // lines are skipped.
+ // Reads and parses the next record from the database into a fixed buffer.
+ // Returns true if an entry was read, false if EOF was reached, or an Error on
+ // failure. Blank lines are skipped. A record that does not fit in the buffer
+ // is reported as ERANGE.
LIBC_INLINE ErrorOr<bool> getnext(EntryType *entry, cpp::span<char> buffer) {
if (!entry)
return Error(EINVAL);
@@ -156,11 +239,13 @@ template <typename EntryType> class FlatFileDatabase {
}
while (true) {
+ last_line_start = current_offset;
auto result = read_line(file, buffer);
if (!result.has_value())
return Error(result.error());
ReadLineResult res = result.value();
+ current_offset += static_cast<off_t>(res.raw_bytes_consumed);
if (res.eof)
return false; // EOF
@@ -171,16 +256,61 @@ template <typename EntryType> class FlatFileDatabase {
if (res.truncated)
return Error(ERANGE);
- if (parse_line(buffer.first(res.bytes_read + 1), entry))
- return true;
+ auto parse_res = parse_line<EntryType>(buffer, res.bytes_read, entry);
+ if (!parse_res.has_value())
+ return Error(parse_res.error());
+ return true;
+ }
+ }
+ // Reads and parses the next record from the database into a caller-owned
+ // buffer, growing it as needed. Behaves as the fixed-buffer overload except
+ // that a long record grows the buffer rather than producing ERANGE.
+ LIBC_INLINE ErrorOr<bool> getnext(EntryType *entry, DynamicBuffer &buffer) {
+ if (!entry)
return Error(EINVAL);
+
+ if (!file) {
+ auto res = setdb();
+ if (!res.has_value())
+ return Error(res.error());
+ }
+
+ while (true) {
+ last_line_start = current_offset;
+ auto result = read_line_growing(file, buffer);
+ if (!result.has_value())
+ return Error(result.error());
+
+ ReadLineResult res = result.value();
+ current_offset += static_cast<off_t>(res.raw_bytes_consumed);
+ if (res.eof)
+ return false; // EOF
+
+ // Skip blank lines.
+ if (res.bytes_read == 0)
+ continue;
+
+ while (true) {
+ auto parse_res =
+ parse_line<EntryType>(buffer.span(), res.bytes_read, entry);
+ if (parse_res.has_value())
+ return true;
+ if (parse_res.error() != ERANGE)
+ return Error(parse_res.error());
+ if (!buffer.grow())
+ return Error(ENOMEM);
+ }
}
}
// Searches for a record matching a given predicate. Returns true if the
// entry was found, false if it's missing, or an Error if lookup failed.
- LIBC_INLINE ErrorOr<bool> lookup(Matcher matcher, EntryType *entry,
+ //
+ // Per POSIX, ERANGE is reported only if the matched entry does not fit in the
+ // caller's buffer; unrelated preceding records larger than buffer are
+ // skipped.
+ LIBC_INLINE ErrorOr<bool> lookup(const Matcher &matcher, EntryType *entry,
cpp::span<char> buffer) {
if (!entry)
return Error(EINVAL);
@@ -189,6 +319,52 @@ template <typename EntryType> class FlatFileDatabase {
if (!res.has_value())
return Error(res.error());
+ ScopedDynamicBuffer scratch_buf;
+ while (true) {
+ auto next_res = getnext(entry, buffer);
+ if (next_res.has_value()) {
+ if (!next_res.value())
+ return false; // EOF without match
+ if (matcher(*entry))
+ return true;
+ continue;
+ }
+
+ if (next_res.error() != ERANGE)
+ return Error(next_res.error());
+
+ // The record at last_line_start exceeded buffer. Check whether it is
+ // actually the target entry before reporting ERANGE. Use a stack-local
+ // scratch_entry so we do not leave dangling pointers in the caller's
+ // *entry when scratch_buf goes out of scope.
+ auto seek_res = file->seek(last_line_start, SEEK_SET);
+ if (!seek_res.has_value())
+ return Error(seek_res.error());
+ file->clearerr();
+ current_offset = last_line_start;
+
+ EntryType scratch_entry{};
+ auto dyn_res = getnext(&scratch_entry, scratch_buf);
+ if (!dyn_res.has_value())
+ return Error(dyn_res.error());
+ if (!dyn_res.value())
+ return false;
+ if (matcher(scratch_entry))
+ return Error(ERANGE);
+ }
+ }
+
+ // As above, but reads into a caller-owned buffer that grows to fit long
+ // records instead of reporting ERANGE.
+ LIBC_INLINE ErrorOr<bool> lookup(const Matcher &matcher, EntryType *entry,
+ DynamicBuffer &buffer) {
+ if (!entry)
+ return Error(EINVAL);
+
+ auto res = setdb();
+ if (!res.has_value())
+ return Error(res.error());
+
while (true) {
auto next_res = getnext(entry, buffer);
if (!next_res.has_value())
diff --git a/libc/src/pwd/pwd_utils.cpp b/libc/src/pwd/pwd_utils.cpp
index fb4e75f9544c9..55052c8c39232 100644
--- a/libc/src/pwd/pwd_utils.cpp
+++ b/libc/src/pwd/pwd_utils.cpp
@@ -13,6 +13,7 @@
#include "src/pwd/pwd_utils.h"
#include "hdr/errno_macros.h"
+#include "hdr/types/size_t.h"
#include "hdr/types/struct_passwd.h"
#include "src/__support/CPP/span.h"
#include "src/__support/CPP/string_view.h"
@@ -33,8 +34,9 @@ ErrorOr<struct passwd> parse_passwd_line(char *line) {
struct passwd pwd;
size_t len = internal::string_length(line);
- if (!parse_line(cpp::span<char>(line, len + 1), &pwd))
- return Error(EINVAL);
+ auto res = parse_line(cpp::span<char>(line, len + 1), len, &pwd);
+ if (!res.has_value())
+ return Error(res.error());
return pwd;
}
diff --git a/libc/src/pwd/pwd_utils.h b/libc/src/pwd/pwd_utils.h
index 1af49d2019cf7..36a98c7bc41d7 100644
--- a/libc/src/pwd/pwd_utils.h
+++ b/libc/src/pwd/pwd_utils.h
@@ -16,6 +16,7 @@
#include "hdr/errno_macros.h"
#include "hdr/types/gid_t.h"
+#include "hdr/types/size_t.h"
#include "hdr/types/struct_passwd.h"
#include "hdr/types/uid_t.h"
#include "src/__support/CPP/span.h"
@@ -34,59 +35,63 @@ namespace pwd {
// 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;
+LIBC_INLINE ErrorOr<void> parse_line<struct passwd>(cpp::span<char> buffer,
+ size_t line_len,
+ struct passwd *pwd) {
+ if (!pwd || line_len == 0 || line_len >= buffer.size())
+ return Error(EINVAL);
- FieldTokenizer tokenizer(line);
+ FieldTokenizer tokenizer(buffer.first(line_len + 1));
auto name = tokenizer.next_field();
- if (!name)
- return false;
+ if (!name || name->empty() || name->front() == '\0')
+ return Error(EINVAL);
pwd->pw_name = name->data();
auto passwd = tokenizer.next_field();
if (!passwd)
- return false;
+ return Error(EINVAL);
pwd->pw_passwd = passwd->data();
auto uid_str = tokenizer.next_field();
if (!uid_str || uid_str->empty() || !internal::isdigit(uid_str->front()))
- return false;
+ return Error(EINVAL);
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() ||
+ static_cast<size_t>(uid_res.parsed_len) + 1 != uid_str->size() ||
(*uid_str)[uid_res.parsed_len] != '\0')
- return false;
+ return Error(EINVAL);
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;
+ return Error(EINVAL);
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() ||
+ static_cast<size_t>(gid_res.parsed_len) + 1 != gid_str->size() ||
(*gid_str)[gid_res.parsed_len] != '\0')
- return false;
+ return Error(EINVAL);
pwd->pw_gid = gid_res.value;
auto gecos = tokenizer.next_field();
if (!gecos)
- return false;
+ return Error(EINVAL);
pwd->pw_gecos = gecos->data();
auto dir = tokenizer.next_field();
if (!dir)
- return false;
+ return Error(EINVAL);
pwd->pw_dir = dir->data();
auto shell = tokenizer.next_field();
if (!shell)
- return false;
+ return Error(EINVAL);
pwd->pw_shell = shell->data();
- return true;
+ if (tokenizer.next_field())
+ return Error(EINVAL);
+
+ return {};
}
// Parses a colon-separated password database line into a struct passwd.
diff --git a/libc/test/src/__support/pwd/CMakeLists.txt b/libc/test/src/__support/pwd/CMakeLists.txt
index c6ec4ae517d83..a63fe2ecb010d 100644
--- a/libc/test/src/__support/pwd/CMakeLists.txt
+++ b/libc/test/src/__support/pwd/CMakeLists.txt
@@ -27,7 +27,11 @@ add_libc_test(
libc.src.__support.CPP.string_view
libc.src.__support.File.file
libc.src.__support.File.platform_file
+ libc.src.__support.error_or
+ libc.src.__support.pwd.dynamic_buffer
+ libc.src.__support.pwd.field_tokenizer
libc.src.__support.pwd.flat_file_db
libc.src.stdio.remove
libc.src.string.string_utils
+ libc.test.UnitTest.ErrnoCheckingTest
)
diff --git a/libc/test/src/__support/pwd/flat_file_db_test.cpp b/libc/test/src/__support/pwd/flat_file_db_test.cpp
index 00c709b484767..2bcb141fff8ff 100644
--- a/libc/test/src/__support/pwd/flat_file_db_test.cpp
+++ b/libc/test/src/__support/pwd/flat_file_db_test.cpp
@@ -12,14 +12,17 @@
//===----------------------------------------------------------------------===//
#include "hdr/errno_macros.h"
+#include "hdr/types/size_t.h"
#include "src/__support/CPP/span.h"
+#include "src/__support/CPP/string_view.h"
#include "src/__support/File/file.h"
+#include "src/__support/error_or.h"
+#include "src/__support/pwd/dynamic_buffer.h"
#include "src/__support/pwd/field_tokenizer.h"
#include "src/__support/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 {
@@ -59,22 +62,24 @@ namespace LIBC_NAMESPACE_DECL {
namespace pwd {
template <>
-inline bool parse_line<SimpleTestEntry>(cpp::span<char> line,
- SimpleTestEntry *entry) {
- if (line.empty() || !entry)
- return false;
- FieldTokenizer tokenizer(line);
+inline ErrorOr<void> parse_line<SimpleTestEntry>(cpp::span<char> buffer,
+ size_t line_len,
+ SimpleTestEntry *entry) {
+ if (!entry || line_len == 0 || line_len >= buffer.size())
+ return Error(EINVAL);
+
+ FieldTokenizer tokenizer(buffer.first(line_len + 1));
auto k = tokenizer.next_field();
if (!k)
- return false;
+ return Error(EINVAL);
entry->key = k->data();
auto v = tokenizer.next_field();
if (!v)
- return false;
+ return Error(EINVAL);
entry->val = v->data();
- return true;
+ return {};
}
} // namespace pwd
@@ -85,7 +90,7 @@ TEST_F(LlvmLibcFlatFileDbTest, GetNextAndLookup) {
HermeticFile test_file(libc_make_test_file_path("flat_db_test.test"),
content);
- LIBC_NAMESPACE::pwd::FlatFileDatabase<SimpleTestEntry> db(
+ LIBC_NAMESPACE::pwd::ScopedFlatFileDatabase<SimpleTestEntry> db(
test_file.get_path());
char buffer[128];
SimpleTestEntry entry;
@@ -94,20 +99,20 @@ TEST_F(LlvmLibcFlatFileDbTest, GetNextAndLookup) {
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");
+ EXPECT_STREQ(entry.key, "user1");
+ EXPECT_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");
+ EXPECT_STREQ(entry.key, "user2");
+ EXPECT_STREQ(entry.val, "secret2");
// EOF
auto r3 = db.getnext(&entry, buffer);
ASSERT_TRUE(r3.has_value());
- ASSERT_FALSE(r3.value());
+ EXPECT_FALSE(r3.value());
// Rewind and lookup
db.setdb();
@@ -117,10 +122,8 @@ TEST_F(LlvmLibcFlatFileDbTest, GetNextAndLookup) {
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();
+ EXPECT_STREQ(entry.key, "user2");
+ EXPECT_STREQ(entry.val, "secret2");
}
TEST_F(LlvmLibcFlatFileDbTest, LookupNotFound) {
@@ -128,7 +131,7 @@ TEST_F(LlvmLibcFlatFileDbTest, LookupNotFound) {
HermeticFile test_file(libc_make_test_file_path("flat_db_not_found.test"),
content);
- LIBC_NAMESPACE::pwd::FlatFileDatabase<SimpleTestEntry> db(
+ LIBC_NAMESPACE::pwd::ScopedFlatFileDatabase<SimpleTestEntry> db(
test_file.get_path());
char buffer[128];
SimpleTestEntry entry;
@@ -138,9 +141,7 @@ TEST_F(LlvmLibcFlatFileDbTest, LookupNotFound) {
};
auto lookup_res = db.lookup(matcher, &entry, buffer);
ASSERT_TRUE(lookup_res.has_value());
- ASSERT_FALSE(lookup_res.value());
-
- db.enddb();
+ EXPECT_FALSE(lookup_res.value());
}
TEST_F(LlvmLibcFlatFileDbTest, TruncatedLineReturnsErange) {
@@ -148,16 +149,14 @@ TEST_F(LlvmLibcFlatFileDbTest, TruncatedLineReturnsErange) {
HermeticFile test_file(libc_make_test_file_path("flat_db_trunc.test"),
content);
- LIBC_NAMESPACE::pwd::FlatFileDatabase<SimpleTestEntry> db(
+ LIBC_NAMESPACE::pwd::ScopedFlatFileDatabase<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();
+ EXPECT_EQ(res.error(), ERANGE);
}
TEST_F(LlvmLibcFlatFileDbTest, MalformedLineReturnsEinval) {
@@ -165,16 +164,14 @@ TEST_F(LlvmLibcFlatFileDbTest, MalformedLineReturnsEinval) {
HermeticFile test_file(libc_make_test_file_path("flat_db_malformed.test"),
content);
- LIBC_NAMESPACE::pwd::FlatFileDatabase<SimpleTestEntry> db(
+ LIBC_NAMESPACE::pwd::ScopedFlatFileDatabase<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();
+ EXPECT_EQ(res.error(), EINVAL);
}
TEST_F(LlvmLibcFlatFileDbTest, BlankLinesSkipped) {
@@ -217,3 +214,94 @@ TEST_F(LlvmLibcFlatFileDbTest, BlankLinesSkipped) {
ASSERT_STREQ(entry.key, "user2");
ASSERT_STREQ(entry.val, "secret2");
}
+
+TEST_F(LlvmLibcFlatFileDbTest, DynamicBufferReadsArbitrarilyLongLines) {
+ // Two records, each far beyond the buffer's initial capacity, so that
+ // iteration exercises repeated growth.
+ constexpr size_t RECORD_COUNT = 2;
+ constexpr size_t VALUE_LENGTH = 4000;
+ constexpr size_t RECORD_OVERHEAD = 32;
+ char content[RECORD_COUNT * (VALUE_LENGTH + RECORD_OVERHEAD)];
+
+ size_t pos = 0;
+ for (size_t record = 0; record < RECORD_COUNT; ++record) {
+ const char *key = record == 0 ? "key0:" : "key1:";
+ for (const char *p = key; *p != '\0'; ++p)
+ content[pos++] = *p;
+ for (size_t i = 0; i < VALUE_LENGTH; ++i)
+ content[pos++] = 'v';
+ content[pos++] = '\n';
+ }
+ content[pos] = '\0';
+
+ HermeticFile test_file(libc_make_test_file_path("flat_db_longline.test"),
+ content);
+
+ LIBC_NAMESPACE::pwd::ScopedFlatFileDatabase<SimpleTestEntry> db(
+ test_file.get_path());
+ LIBC_NAMESPACE::pwd::ScopedDynamicBuffer buffer;
+ SimpleTestEntry entry;
+
+ auto r1 = db.getnext(&entry, buffer);
+ ASSERT_TRUE(r1.has_value());
+ ASSERT_TRUE(r1.value());
+ ASSERT_STREQ(entry.key, "key0");
+ ASSERT_EQ(LIBC_NAMESPACE::internal::string_length(entry.val), VALUE_LENGTH);
+
+ auto r2 = db.getnext(&entry, buffer);
+ ASSERT_TRUE(r2.has_value());
+ ASSERT_TRUE(r2.value());
+ ASSERT_STREQ(entry.key, "key1");
+
+ auto r3 = db.getnext(&entry, buffer);
+ ASSERT_TRUE(r3.has_value());
+ ASSERT_FALSE(r3.value());
+}
+
+TEST_F(LlvmLibcFlatFileDbTest, PrecedingLongRecordsSkippedDuringLookup) {
+ const char *content =
+ "huge_unrelated_key:012345678901234567890123456789012345\n"
+ "target:short\n";
+ HermeticFile test_file(libc_make_test_file_path("flat_db_longskip.test"),
+ content);
+
+ LIBC_NAMESPACE::pwd::ScopedFlatFileDatabase<SimpleTestEntry> db(
+ test_file.get_path());
+ // 24 bytes is large enough for "target:short" (12 chars + '\0') but smaller
+ // than "huge_unrelated_key:...". Fixed-buffer lookup must proceed past
+ // unrelated long records without falsely returning ERANGE.
+ constexpr size_t SMALL_BUFFER_SIZE = 24;
+ char buffer[SMALL_BUFFER_SIZE];
+ SimpleTestEntry entry;
+
+ auto res = db.lookup(
+ [](const SimpleTestEntry &e) {
+ return LIBC_NAMESPACE::cpp::string_view(e.key) == "target";
+ },
+ &entry, buffer);
+ ASSERT_TRUE(res.has_value());
+ ASSERT_TRUE(res.value());
+ ASSERT_STREQ(entry.key, "target");
+ ASSERT_STREQ(entry.val, "short");
+}
+
+TEST_F(LlvmLibcFlatFileDbTest, DynamicBufferReserveGrowRelease) {
+ LIBC_NAMESPACE::pwd::ScopedDynamicBuffer buffer;
+ ASSERT_EQ(buffer.capacity(), static_cast<size_t>(0));
+
+ ASSERT_TRUE(buffer.grow());
+ size_t initial = buffer.capacity();
+ ASSERT_GT(initial, static_cast<size_t>(0));
+
+ ASSERT_TRUE(buffer.grow());
+ ASSERT_EQ(buffer.capacity(), initial * 2);
+
+ buffer.release();
+ ASSERT_EQ(buffer.capacity(), static_cast<size_t>(0));
+ buffer.release();
+ ASSERT_EQ(buffer.capacity(), static_cast<size_t>(0));
+
+ constexpr size_t TARGET_RESERVE_CAPACITY = 1024;
+ ASSERT_TRUE(buffer.reserve(TARGET_RESERVE_CAPACITY));
+ ASSERT_GE(buffer.capacity(), TARGET_RESERVE_CAPACITY);
+}
>From 8603f1a49a77dca7cc25170bd380d5c8a73069e5 Mon Sep 17 00:00:00 2001
From: Jeff Bailey <jbailey at raspberryginger.com>
Date: Wed, 16 Sep 2026 15:44:36 +0100
Subject: [PATCH 2/2] [libc] Address PR 223811 review feedback in
FlatFileDatabase
* Set DynamicBuffer::INITIAL_CAPACITY to 1024 to match
_SC_GETGR_R_SIZE_MAX
* Mark DynamicBuffer::span() const
* Clarify ReadLineResult::eof comment for blank and newline-only lines
* Drop redundant buf.capacity() check before buf.reserve() in
read_line_growing
* Propagate drain.error from the line-drain loop in read_line_growing
* Extract clear_file_stream() helper shared by set_path() and enddb()
Assisted-by: Automated tooling, human reviewed.
---
libc/src/__support/pwd/dynamic_buffer.h | 4 +-
libc/src/__support/pwd/flat_file_db.h | 50 +++++++++++++------------
2 files changed, 28 insertions(+), 26 deletions(-)
diff --git a/libc/src/__support/pwd/dynamic_buffer.h b/libc/src/__support/pwd/dynamic_buffer.h
index 59f205d4161e4..44d269071c173 100644
--- a/libc/src/__support/pwd/dynamic_buffer.h
+++ b/libc/src/__support/pwd/dynamic_buffer.h
@@ -32,7 +32,7 @@ namespace pwd {
// constant-initialised process global. The owner releases the storage
// explicitly when done.
class DynamicBuffer {
- static constexpr size_t INITIAL_CAPACITY = 256;
+ static constexpr size_t INITIAL_CAPACITY = 1024;
char *ptr = nullptr;
size_t cap = 0;
@@ -83,7 +83,7 @@ class DynamicBuffer {
cap = 0;
}
- [[nodiscard]] LIBC_INLINE cpp::span<char> span() { return {ptr, cap}; }
+ [[nodiscard]] LIBC_INLINE cpp::span<char> span() const { return {ptr, cap}; }
[[nodiscard]] LIBC_INLINE size_t capacity() const { return cap; }
};
diff --git a/libc/src/__support/pwd/flat_file_db.h b/libc/src/__support/pwd/flat_file_db.h
index 318faf0cf5fac..49524e40a8948 100644
--- a/libc/src/__support/pwd/flat_file_db.h
+++ b/libc/src/__support/pwd/flat_file_db.h
@@ -35,9 +35,11 @@ struct ReadLineResult {
size_t bytes_read;
size_t raw_bytes_consumed;
bool truncated;
- // True only when zero bytes were read because the stream was already at EOF.
- // A final line without a trailing newline returns bytes_read > 0 and
- // eof == false; the following call returns bytes_read == 0 and eof == true.
+ // True only when no raw bytes were read (raw_bytes_consumed == 0) because the
+ // stream was already at EOF. A blank line ("\n") or a final line without a
+ // trailing newline consumes at least one raw byte and returns eof == false
+ // (with bytes_read == 0 for "\n"); the following call returns
+ // raw_bytes_consumed == 0 and eof == true.
bool eof;
};
@@ -59,6 +61,19 @@ template <typename EntryType> class FlatFileDatabase {
off_t current_offset = 0;
off_t last_line_start = 0;
+ // Closes the file stream if open and resets stream position tracking.
+ LIBC_INLINE ErrorOr<void> clear_file_stream() {
+ current_offset = 0;
+ last_line_start = 0;
+ if (file) {
+ int result = file->close();
+ file = nullptr;
+ if (result != 0)
+ return Error(result);
+ }
+ return {};
+ }
+
// Reads a single line from the given file into the provided buffer, stripping
// any trailing '\n' and ensuring the result is null-terminated. A line too
// long for the buffer is reported as truncated.
@@ -93,7 +108,7 @@ template <typename EntryType> class FlatFileDatabase {
break;
}
- bool eof = (bytes_read == 0);
+ bool eof = (raw_bytes_consumed == 0);
auto read_span = buf.first(bytes_read);
if (result.value == 1 && !read_span.empty() && read_span.back() != '\n') {
@@ -136,11 +151,13 @@ template <typename EntryType> class FlatFileDatabase {
while (true) {
// One byte for the character about to be read, one for the terminator.
if (bytes_read > cpp::numeric_limits<size_t>::max() - 2 ||
- (bytes_read + 2 > buf.capacity() && !buf.reserve(bytes_read + 2))) {
+ !buf.reserve(bytes_read + 2)) {
char c = '\0';
while (true) {
FileIOResult drain = f->read_unlocked(&c, 1);
- if (drain.has_error() || drain.value != 1 || c == '\n')
+ if (drain.has_error())
+ return Error(drain.error);
+ if (drain.value != 1 || c == '\n')
break;
}
return Error(ENOMEM);
@@ -161,8 +178,8 @@ template <typename EntryType> class FlatFileDatabase {
if (f->error_unlocked())
return Error(EIO);
- bool eof = (bytes_read == 0);
size_t raw_bytes_consumed = bytes_read;
+ bool eof = (raw_bytes_consumed == 0);
// If the line ended with a newline, strip it.
if (bytes_read > 0 && buf.span()[bytes_read - 1] == '\n')
@@ -184,13 +201,8 @@ template <typename EntryType> class FlatFileDatabase {
LIBC_INLINE void set_path(const char *path) {
if (!path)
return;
- if (file) {
- file->close();
- file = nullptr;
- }
+ clear_file_stream();
file_path = path;
- current_offset = 0;
- last_line_start = 0;
}
// Opens or rewinds the database file stream.
@@ -212,17 +224,7 @@ template <typename EntryType> class FlatFileDatabase {
}
// Closes the database file stream.
- LIBC_INLINE ErrorOr<void> enddb() {
- current_offset = 0;
- last_line_start = 0;
- if (file) {
- int result = file->close();
- file = nullptr;
- if (result != 0)
- return Error(result);
- }
- return {};
- }
+ LIBC_INLINE ErrorOr<void> enddb() { return clear_file_stream(); }
// Reads and parses the next record from the database into a fixed buffer.
// Returns true if an entry was read, false if EOF was reached, or an Error on
More information about the libc-commits
mailing list