Add directory stream wrappers #40
@@ -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/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/string.c` | the `string.h` surface |
|
||||||
| `src/stream.c` | `stdio.h` beyond open/read/write/close |
|
| `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/collections.c` | list and tree operations, hash map, string buffer, FNV-1a |
|
||||||
| `src/aksl_internal.h` | shared internals; not installed, not public |
|
| `src/aksl_internal.h` | shared internals; not installed, not public |
|
||||||
|
|
||||||
|
|||||||
@@ -208,6 +208,7 @@ add_library(akstdlib SHARED
|
|||||||
src/string.c
|
src/string.c
|
||||||
src/stream.c
|
src/stream.c
|
||||||
src/stat.c
|
src/stat.c
|
||||||
|
src/dir.c
|
||||||
src/collections.c
|
src/collections.c
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -308,6 +309,7 @@ install(FILES
|
|||||||
set(AKSL_TESTS
|
set(AKSL_TESTS
|
||||||
collections
|
collections
|
||||||
convert
|
convert
|
||||||
|
dir
|
||||||
format
|
format
|
||||||
hashmap
|
hashmap
|
||||||
linkedlist
|
linkedlist
|
||||||
|
|||||||
@@ -63,6 +63,7 @@
|
|||||||
|
|
||||||
/*
|
/*
|
||||||
* What this header needs in its own declarations, and no more:
|
* What this header needs in its own declarations, and no more:
|
||||||
|
* dirent.h DIR, struct dirent
|
||||||
* stdio.h FILE
|
* stdio.h FILE
|
||||||
* stddef.h size_t
|
* stddef.h size_t
|
||||||
* stdint.h uint32_t
|
* stdint.h uint32_t
|
||||||
@@ -72,11 +73,12 @@
|
|||||||
* which every consumer then got whether it wanted them or not. stddef.h in place
|
* 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.
|
* of stdlib.h is the same size_t at a fraction of the namespace.
|
||||||
*/
|
*/
|
||||||
|
#include <dirent.h>
|
||||||
|
#include <fcntl.h>
|
||||||
#include <stdarg.h>
|
#include <stdarg.h>
|
||||||
#include <stddef.h>
|
#include <stddef.h>
|
||||||
#include <stdint.h>
|
#include <stdint.h>
|
||||||
#include <stdio.h>
|
#include <stdio.h>
|
||||||
#include <fcntl.h>
|
|
||||||
#include <sys/stat.h>
|
#include <sys/stat.h>
|
||||||
#include <sys/statvfs.h>
|
#include <sys/statvfs.h>
|
||||||
/* off_t, for the aksl_fseeko/aksl_ftello pair. POSIX, like aksl_realpath. */
|
/* 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);
|
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
|
/** @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)
|
||||||
|
andrew marked this conversation as resolved
|
|||||||
|
{
|
||||||
|
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);
|
||||||
|
andrew marked this conversation as resolved
Outdated
andrew
commented
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(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
None of these functions have
manstyle comment blocks for context on their operations, the same way other sources in this library do.