1 Commits
26 ... 10

Author SHA1 Message Date
af5a4b861a Add directory stream wrappers
All checks were successful
libakstdlib CI Build / cmake_build (push) Successful in 2m56s
libakstdlib CI Build / sanitizers (push) Successful in 2m53s
libakstdlib CI Build / coverage (push) Successful in 2m44s
libakstdlib CI Build / mutation_test (push) Successful in 13m17s
2026-08-03 13:04:33 -04:00
5 changed files with 285 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

69
src/dir.c Normal file
View File

@@ -0,0 +1,69 @@
/* POSIX directory-stream wrappers. */
#include <akstdlib.h>
#include <errno.h>
#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);
}

147
tests/test_dir.c Normal file
View File

@@ -0,0 +1,147 @@
#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;
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);
}