From d5e5e95c61b797c3b1e7bd54eb345b0465bcb974 Mon Sep 17 00:00:00 2001 From: "Logikoma (Codex GPT-5)" Date: Mon, 3 Aug 2026 13:04:33 -0400 Subject: [PATCH 1/2] Add directory stream wrappers --- AGENTS.md | 1 + CMakeLists.txt | 2 + include/akstdlib.h | 67 ++++++++++++++++++++- src/dir.c | 69 +++++++++++++++++++++ tests/test_dir.c | 147 +++++++++++++++++++++++++++++++++++++++++++++ 5 files changed, 285 insertions(+), 1 deletion(-) create mode 100644 src/dir.c create mode 100644 tests/test_dir.c diff --git a/AGENTS.md b/AGENTS.md index b4b356f..1a7eaa8 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -14,6 +14,7 @@ reasoning. The implementation is split by domain: | `src/stdlib.c` | memory, formatted output, string-to-number, realpath, djb2, and the list/tree traversal entry points | | `src/string.c` | the `string.h` surface | | `src/stream.c` | `stdio.h` beyond open/read/write/close | +| `src/dir.c` | directory stream open/read/rewind/close | | `src/collections.c` | list and tree operations, hash map, string buffer, FNV-1a | | `src/aksl_internal.h` | shared internals; not installed, not public | diff --git a/CMakeLists.txt b/CMakeLists.txt index 3ea02a4..45fbc29 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -208,6 +208,7 @@ add_library(akstdlib SHARED src/string.c src/stream.c src/stat.c + src/dir.c src/collections.c ) @@ -308,6 +309,7 @@ install(FILES set(AKSL_TESTS collections convert + dir format hashmap linkedlist diff --git a/include/akstdlib.h b/include/akstdlib.h index aef0773..6dab689 100644 --- a/include/akstdlib.h +++ b/include/akstdlib.h @@ -63,6 +63,7 @@ /* * What this header needs in its own declarations, and no more: + * dirent.h DIR, struct dirent * stdio.h FILE * stddef.h size_t * stdint.h uint32_t @@ -72,11 +73,12 @@ * which every consumer then got whether it wanted them or not. stddef.h in place * of stdlib.h is the same size_t at a fraction of the namespace. */ +#include +#include #include #include #include #include -#include #include #include /* off_t, for the aksl_fseeko/aksl_ftello pair. POSIX, like aksl_realpath. */ @@ -892,6 +894,69 @@ akerr_ErrorContext AKERR_NOIGNORE *aksl_statvfs(const char *path, struct statvfs akerr_ErrorContext AKERR_NOIGNORE *aksl_fstatvfs(int fd, struct statvfs *dest); +/** @} */ +/* ====================================================================== */ +/** @name Directories + * + * Directory entries are copied into caller-owned storage. `d_type` may be + * `DT_UNKNOWN`; callers that require a type must fall back to aksl_stat or + * aksl_fstatat. + * @{ + */ +/* ====================================================================== */ + +/** + * @brief Open a directory stream by path. + * @param[in] pathname Directory path. Required. + * @param[out] dest Open directory stream, or NULL on failure. Required. + * @throws AKERR_NULLPOINTER If pathname or dest is NULL. + * @throws AKERR_IO If opendir(3) fails without setting errno. + * @throws (errno) The errno opendir(3) set, reported directly as the status. + * @return NULL on success, an error context otherwise. + */ +akerr_ErrorContext AKERR_NOIGNORE *aksl_opendir(const char *pathname, DIR **dest); + +/** + * @brief Open a directory stream from a file descriptor. + * @param[in] fd Open directory descriptor. Ownership transfers on success. + * @param[out] dest Open directory stream, or NULL on failure. Required. + * @throws AKERR_NULLPOINTER If dest is NULL. + * @throws AKERR_IO If fdopendir(3) fails without setting errno. + * @throws (errno) The errno fdopendir(3) set, reported directly as the status. + * @return NULL on success, an error context otherwise. + */ +akerr_ErrorContext AKERR_NOIGNORE *aksl_fdopendir(int fd, DIR **dest); + +/** + * @brief Read and copy the next directory entry. + * @param[in] dirp Open directory stream. Required. + * @param[out] dest Caller-owned storage for the copied entry. Required. + * @throws AKERR_NULLPOINTER If dirp or dest is NULL. + * @throws AKERR_EOF At the end of the directory stream. + * @throws AKERR_IO If readdir(3) fails without setting errno. + * @throws (errno) The errno readdir(3) set, reported directly as the status. + * @return NULL on success, an error context otherwise. + */ +akerr_ErrorContext AKERR_NOIGNORE *aksl_readdir(DIR *dirp, struct dirent *dest); + +/** + * @brief Close a directory stream. + * @param[in] dirp Open directory stream. Required. + * @throws AKERR_NULLPOINTER If dirp is NULL. + * @throws AKERR_IO If closedir(3) fails without setting errno. + * @throws (errno) The errno closedir(3) set, reported directly as the status. + * @return NULL on success, an error context otherwise. + */ +akerr_ErrorContext AKERR_NOIGNORE *aksl_closedir(DIR *dirp); + +/** + * @brief Reset a directory stream to its beginning. + * @param[in] dirp Open directory stream. Required. + * @throws AKERR_NULLPOINTER If dirp is NULL. + * @return NULL on success, an error context otherwise. + */ +akerr_ErrorContext AKERR_NOIGNORE *aksl_rewinddir(DIR *dirp); + /** @} */ /* ====================================================================== */ /** @name Streams: open, read, write, close diff --git a/src/dir.c b/src/dir.c new file mode 100644 index 0000000..963d94d --- /dev/null +++ b/src/dir.c @@ -0,0 +1,69 @@ +/* POSIX directory-stream wrappers. */ +#include + +#include + +#include "aksl_internal.h" + +akerr_ErrorContext AKERR_NOIGNORE *aksl_opendir(const char *pathname, DIR **dest) +{ + PREPARE_ERROR(e); + FAIL_ZERO_RETURN(e, dest, AKERR_NULLPOINTER, "pathname=%p, dest=%p", + (void *)pathname, (void *)dest); + *dest = NULL; + FAIL_ZERO_RETURN(e, pathname, AKERR_NULLPOINTER, "pathname=%p, dest=%p", + (void *)pathname, (void *)dest); + errno = 0; + *dest = opendir(pathname); + FAIL_ZERO_RETURN(e, *dest, AKSL_ERRNO_OR(AKERR_IO), "pathname=%s", pathname); + SUCCEED_RETURN(e); +} + +akerr_ErrorContext AKERR_NOIGNORE *aksl_fdopendir(int fd, DIR **dest) +{ + PREPARE_ERROR(e); + FAIL_ZERO_RETURN(e, dest, AKERR_NULLPOINTER, "fd=%d, dest=%p", fd, (void *)dest); + *dest = NULL; + errno = 0; + *dest = fdopendir(fd); + FAIL_ZERO_RETURN(e, *dest, AKSL_ERRNO_OR(AKERR_IO), "fd=%d", fd); + SUCCEED_RETURN(e); +} + +akerr_ErrorContext AKERR_NOIGNORE *aksl_readdir(DIR *dirp, struct dirent *dest) +{ + struct dirent *entry = NULL; + PREPARE_ERROR(e); + FAIL_ZERO_RETURN(e, dirp, AKERR_NULLPOINTER, "dirp=%p, dest=%p", + (void *)dirp, (void *)dest); + FAIL_ZERO_RETURN(e, dest, AKERR_NULLPOINTER, "dirp=%p, dest=%p", + (void *)dirp, (void *)dest); + + /* readdir uses errno to distinguish failure from end-of-directory. */ + errno = 0; + entry = readdir(dirp); + if ( entry == NULL ) { + FAIL_NONZERO_RETURN(e, errno, AKSL_ERRNO_OR(AKERR_IO), "readdir failed"); + FAIL_RETURN(e, AKERR_EOF, "end of directory"); + } + *dest = *entry; + SUCCEED_RETURN(e); +} + +akerr_ErrorContext AKERR_NOIGNORE *aksl_closedir(DIR *dirp) +{ + PREPARE_ERROR(e); + FAIL_ZERO_RETURN(e, dirp, AKERR_NULLPOINTER, "dirp=%p", (void *)dirp); + errno = 0; + FAIL_NONZERO_RETURN(e, closedir(dirp), AKSL_ERRNO_OR(AKERR_IO), + "closedir failed"); + SUCCEED_RETURN(e); +} + +akerr_ErrorContext AKERR_NOIGNORE *aksl_rewinddir(DIR *dirp) +{ + PREPARE_ERROR(e); + FAIL_ZERO_RETURN(e, dirp, AKERR_NULLPOINTER, "dirp=%p", (void *)dirp); + rewinddir(dirp); + SUCCEED_RETURN(e); +} diff --git a/tests/test_dir.c b/tests/test_dir.c new file mode 100644 index 0000000..b212512 --- /dev/null +++ b/tests/test_dir.c @@ -0,0 +1,147 @@ +#include "aksl_capture.h" + +#include +#include +#include +#include + +static int make_directory(char *path, size_t size) +{ + const char *tmp = getenv("TMPDIR"); + if ( tmp == NULL || tmp[0] == '\0' ) { + tmp = "/tmp"; + } + if ( (size_t)snprintf(path, size, "%s/aksl_dir_XXXXXX", tmp) >= size ) { + return 1; + } + return mkdtemp(path) == NULL; +} + +static int test_open_errors_and_nulls(void) +{ + char file[AKSL_TMP_MAX]; + DIR *dirp = (DIR *)1; + struct dirent entry; + + AKSL_CHECK_STATUS(aksl_opendir("/nonexistent/aksl/dir", &dirp), ENOENT); + AKSL_CHECK(dirp == NULL); + AKSL_CHECK(aksl_temp_file(file, sizeof(file)) == 0); + AKSL_CHECK_STATUS(aksl_opendir(file, &dirp), ENOTDIR); + AKSL_CHECK_STATUS(aksl_opendir(NULL, &dirp), AKERR_NULLPOINTER); + AKSL_CHECK_STATUS(aksl_opendir(".", NULL), AKERR_NULLPOINTER); + AKSL_CHECK_STATUS(aksl_fdopendir(-1, &dirp), EBADF); + AKSL_CHECK_STATUS(aksl_fdopendir(0, NULL), AKERR_NULLPOINTER); + AKSL_CHECK_STATUS(aksl_readdir(NULL, &entry), AKERR_NULLPOINTER); + AKSL_CHECK_OK(aksl_opendir(".", &dirp)); + AKSL_CHECK_STATUS(aksl_readdir(dirp, NULL), AKERR_NULLPOINTER); + AKSL_CHECK_OK(aksl_closedir(dirp)); + AKSL_CHECK_STATUS(aksl_closedir(NULL), AKERR_NULLPOINTER); + AKSL_CHECK_STATUS(aksl_rewinddir(NULL), AKERR_NULLPOINTER); + AKSL_CHECK(unlink(file) == 0); + return 0; +} + +static int test_copy_eof_rewind_and_fdopendir(void) +{ + char path[AKSL_TMP_MAX], first_path[AKSL_TMP_MAX], second_path[AKSL_TMP_MAX]; + struct dirent entry, saved; + char first_read[sizeof(entry.d_name)]; + DIR *dirp = NULL; + int fd = -1, seen_first = 0, seen_second = 0; + + AKSL_CHECK(make_directory(path, sizeof(path)) == 0); + AKSL_CHECK(snprintf(first_path, sizeof(first_path), "%s/first", path) < (int)sizeof(first_path)); + AKSL_CHECK(snprintf(second_path, sizeof(second_path), "%s/second", path) < (int)sizeof(second_path)); + fd = open(first_path, O_CREAT | O_WRONLY, 0600); + AKSL_CHECK(fd >= 0); + AKSL_CHECK(close(fd) == 0); + fd = open(second_path, O_CREAT | O_WRONLY, 0600); + AKSL_CHECK(fd >= 0); + AKSL_CHECK(close(fd) == 0); + + AKSL_CHECK_OK(aksl_opendir(path, &dirp)); + do { + akerr_ErrorContext *error = aksl_readdir(dirp, &entry); + if ( error != NULL ) { + int status = error->status; + RELEASE_ERROR(error); + AKSL_CHECK(status == AKERR_EOF); + break; + } + if ( strcmp(entry.d_name, "first") == 0 ) { saved = entry; seen_first++; } + if ( strcmp(entry.d_name, "second") == 0 ) seen_second++; + } while ( 1 ); + AKSL_CHECK(seen_first == 1 && seen_second == 1); + AKSL_CHECK(strcmp(saved.d_name, "first") == 0); + AKSL_CHECK_OK(aksl_rewinddir(dirp)); + AKSL_CHECK_OK(aksl_readdir(dirp, &entry)); + AKSL_CHECK(snprintf(first_read, sizeof(first_read), "%s", entry.d_name) < (int)sizeof(first_read)); + AKSL_CHECK_OK(aksl_readdir(dirp, &entry)); + AKSL_CHECK_OK(aksl_rewinddir(dirp)); + AKSL_CHECK_OK(aksl_readdir(dirp, &entry)); + AKSL_CHECK(strcmp(entry.d_name, first_read) == 0); + AKSL_CHECK_OK(aksl_closedir(dirp)); + + fd = open(path, O_RDONLY | O_DIRECTORY); + AKSL_CHECK(fd >= 0); + AKSL_CHECK_OK(aksl_fdopendir(fd, &dirp)); + AKSL_CHECK_OK(aksl_readdir(dirp, &entry)); + AKSL_CHECK_OK(aksl_closedir(dirp)); + AKSL_CHECK(unlink(first_path) == 0); + AKSL_CHECK(unlink(second_path) == 0); + AKSL_CHECK(rmdir(path) == 0); + return 0; +} + +static int test_empty_directory_reaches_eof_after_dot_entries(void) +{ + char path[AKSL_TMP_MAX]; + struct dirent entry; + DIR *dirp = NULL; + int count = 0, saw_dot = 0, saw_dotdot = 0; + + AKSL_CHECK(make_directory(path, sizeof(path)) == 0); + AKSL_CHECK_OK(aksl_opendir(path, &dirp)); + for ( ;; ) { + akerr_ErrorContext *error = aksl_readdir(dirp, &entry); + if ( error != NULL ) { + int status = error->status; + RELEASE_ERROR(error); + AKSL_CHECK(status == AKERR_EOF); + break; + } + count++; + if ( strcmp(entry.d_name, ".") == 0 ) saw_dot++; + if ( strcmp(entry.d_name, "..") == 0 ) saw_dotdot++; + } + AKSL_CHECK(count == 2 && saw_dot == 1 && saw_dotdot == 1); + AKSL_CHECK_OK(aksl_closedir(dirp)); + AKSL_CHECK(rmdir(path) == 0); + return 0; +} + +static int test_permission_denied(void) +{ + char path[AKSL_TMP_MAX]; + DIR *dirp = NULL; + AKSL_CHECK(make_directory(path, sizeof(path)) == 0); + AKSL_CHECK(chmod(path, 0000) == 0); + if ( geteuid() == 0 ) { + fprintf(stderr, " (skipped: running as root, chmod 000 denies nothing)\n"); + } else { + AKSL_CHECK_STATUS(aksl_opendir(path, &dirp), EACCES); + } + AKSL_CHECK(chmod(path, 0700) == 0); + AKSL_CHECK(rmdir(path) == 0); + return 0; +} + +int main(void) +{ + int failures = 0; + AKSL_RUN(failures, test_open_errors_and_nulls); + AKSL_RUN(failures, test_copy_eof_rewind_and_fdopendir); + AKSL_RUN(failures, test_empty_directory_reaches_eof_after_dot_entries); + AKSL_RUN(failures, test_permission_denied); + AKSL_REPORT(failures); +} -- 2.43.0 From 83ff77608f81a222354196dc7674a94ade0c9e3b Mon Sep 17 00:00:00 2001 From: Logikoma Date: Mon, 3 Aug 2026 15:00:56 -0400 Subject: [PATCH 2/2] Address directory wrapper review --- src/dir.c | 25 ++++++++++++++++++++++++- tests/test_dir.c | 7 +++++++ 2 files changed, 31 insertions(+), 1 deletion(-) diff --git a/src/dir.c b/src/dir.c index 963d94d..cb72edf 100644 --- a/src/dir.c +++ b/src/dir.c @@ -1,10 +1,20 @@ -/* POSIX directory-stream wrappers. */ +/* + * POSIX directory-stream wrappers. + * + * opendir(3), fdopendir(3), readdir(3), closedir(3), and rewinddir(3) expose + * three different failure conventions between them. These wrappers turn all + * three into error contexts and make end-of-directory an explicit AKERR_EOF. + */ #include #include #include "aksl_internal.h" +/* + * opendir(3) returns NULL for failure. Clear *dest first so a failed open + * cannot leave the caller holding a stale directory stream. + */ akerr_ErrorContext AKERR_NOIGNORE *aksl_opendir(const char *pathname, DIR **dest) { PREPARE_ERROR(e); @@ -19,6 +29,10 @@ akerr_ErrorContext AKERR_NOIGNORE *aksl_opendir(const char *pathname, DIR **dest SUCCEED_RETURN(e); } +/* + * fdopendir(3) takes ownership of fd only when it succeeds. On success the + * matching aksl_closedir call closes both the stream and its descriptor. + */ akerr_ErrorContext AKERR_NOIGNORE *aksl_fdopendir(int fd, DIR **dest) { PREPARE_ERROR(e); @@ -30,6 +44,10 @@ akerr_ErrorContext AKERR_NOIGNORE *aksl_fdopendir(int fd, DIR **dest) SUCCEED_RETURN(e); } +/* + * readdir(3) owns and may reuse its returned storage. Copy the entry into the + * caller's destination, and use errno to distinguish failure from exhaustion. + */ akerr_ErrorContext AKERR_NOIGNORE *aksl_readdir(DIR *dirp, struct dirent *dest) { struct dirent *entry = NULL; @@ -50,6 +68,7 @@ akerr_ErrorContext AKERR_NOIGNORE *aksl_readdir(DIR *dirp, struct dirent *dest) SUCCEED_RETURN(e); } +/* closedir(3) reports its failure directly and invalidates dirp on success. */ akerr_ErrorContext AKERR_NOIGNORE *aksl_closedir(DIR *dirp) { PREPARE_ERROR(e); @@ -60,6 +79,10 @@ akerr_ErrorContext AKERR_NOIGNORE *aksl_closedir(DIR *dirp) SUCCEED_RETURN(e); } +/* + * rewinddir(3) has no failure return. Preserve that contract after rejecting + * a NULL stream, which would otherwise be undefined behaviour. + */ akerr_ErrorContext AKERR_NOIGNORE *aksl_rewinddir(DIR *dirp) { PREPARE_ERROR(e); diff --git a/tests/test_dir.c b/tests/test_dir.c index b212512..cd3e18a 100644 --- a/tests/test_dir.c +++ b/tests/test_dir.c @@ -23,14 +23,21 @@ static int test_open_errors_and_nulls(void) DIR *dirp = (DIR *)1; struct dirent entry; + /* opendir propagates missing-path and non-directory failures. */ AKSL_CHECK_STATUS(aksl_opendir("/nonexistent/aksl/dir", &dirp), ENOENT); AKSL_CHECK(dirp == NULL); AKSL_CHECK(aksl_temp_file(file, sizeof(file)) == 0); AKSL_CHECK_STATUS(aksl_opendir(file, &dirp), ENOTDIR); + + /* Every pointer required by the wrapped operation rejects NULL. */ AKSL_CHECK_STATUS(aksl_opendir(NULL, &dirp), AKERR_NULLPOINTER); AKSL_CHECK_STATUS(aksl_opendir(".", NULL), AKERR_NULLPOINTER); + + /* fdopendir propagates an invalid descriptor and validates its out-param. */ AKSL_CHECK_STATUS(aksl_fdopendir(-1, &dirp), EBADF); AKSL_CHECK_STATUS(aksl_fdopendir(0, NULL), AKERR_NULLPOINTER); + + /* The remaining wrappers reject NULL streams and destinations. */ AKSL_CHECK_STATUS(aksl_readdir(NULL, &entry), AKERR_NULLPOINTER); AKSL_CHECK_OK(aksl_opendir(".", &dirp)); AKSL_CHECK_STATUS(aksl_readdir(dirp, NULL), AKERR_NULLPOINTER); -- 2.43.0