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..cb72edf --- /dev/null +++ b/src/dir.c @@ -0,0 +1,92 @@ +/* + * 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); + 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); +} + +/* + * 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); + 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); +} + +/* + * 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; + 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); +} + +/* closedir(3) reports its failure directly and invalidates dirp on success. */ +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); +} + +/* + * 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); + 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..cd3e18a --- /dev/null +++ b/tests/test_dir.c @@ -0,0 +1,154 @@ +#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; + + /* 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); + 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); +}