Add directory stream wrappers #40

Merged
andrew merged 2 commits from 10 into main 2026-08-03 15:26:00 -04:00
5 changed files with 315 additions and 1 deletions

View File

@@ -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 |

View File

@@ -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

View File

@@ -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 <dirent.h>
#include <fcntl.h>
#include <stdarg.h>
#include <stddef.h>
#include <stdint.h>
#include <stdio.h>
#include <fcntl.h>
#include <sys/stat.h>
#include <sys/statvfs.h>
/* 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

92
src/dir.c Normal file
View File

@@ -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 <akstdlib.h>
andrew marked this conversation as resolved
Review

None of these functions have man style comment blocks for context on their operations, the same way other sources in this library do.

None of these functions have `man` style comment blocks for context on their operations, the same way other sources in this library do.
#include <errno.h>
#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);
}

154
tests/test_dir.c Normal file
View File

@@ -0,0 +1,154 @@
#include "aksl_capture.h"
#include <dirent.h>
#include <errno.h>
#include <fcntl.h>
#include <sys/stat.h>
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. */
andrew marked this conversation as resolved Outdated

None of these checks have any comments explaining what the actual error cases are that they're testing

None of these checks have any comments explaining what the actual error cases are that they're testing
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);
}