Add directory stream wrappers
All checks were successful
All checks were successful
This commit is contained in:
@@ -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 |
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
69
src/dir.c
Normal 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
147
tests/test_dir.c
Normal 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);
|
||||
}
|
||||
Reference in New Issue
Block a user