From 5af4197ee93f056150c643f0df6ef3dcb7e2429c Mon Sep 17 00:00:00 2001 From: huntedbytheirs Date: Sat, 5 Sep 2026 21:08:57 -0400 Subject: [PATCH] feat(dirent): directory iteration --- include/dirent.h | 179 +++++++++++++++++ src/dirent/alphasort.c | 22 ++ src/dirent/closedir.c | 25 +++ src/dirent/dirent_impl.h | 57 ++++++ src/dirent/dirfd.c | 19 ++ src/dirent/fdopendir.c | 47 +++++ src/dirent/opendir.c | 37 ++++ src/dirent/readdir.c | 78 ++++++++ src/dirent/readdir_r.c | 46 +++++ src/dirent/rewinddir.c | 29 +++ src/dirent/scandir.c | 124 ++++++++++++ src/dirent/seekdir.c | 31 +++ src/dirent/telldir.c | 22 ++ tests/test_dirent.c | 423 +++++++++++++++++++++++++++++++++++++++ 14 files changed, 1139 insertions(+) create mode 100644 include/dirent.h create mode 100644 src/dirent/alphasort.c create mode 100644 src/dirent/closedir.c create mode 100644 src/dirent/dirent_impl.h create mode 100644 src/dirent/dirfd.c create mode 100644 src/dirent/fdopendir.c create mode 100644 src/dirent/opendir.c create mode 100644 src/dirent/readdir.c create mode 100644 src/dirent/readdir_r.c create mode 100644 src/dirent/rewinddir.c create mode 100644 src/dirent/scandir.c create mode 100644 src/dirent/seekdir.c create mode 100644 src/dirent/telldir.c create mode 100644 tests/test_dirent.c diff --git a/include/dirent.h b/include/dirent.h new file mode 100644 index 0000000..41208f3 --- /dev/null +++ b/include/dirent.h @@ -0,0 +1,179 @@ +#ifndef VLIBC_DIRENT_H +#define VLIBC_DIRENT_H + +/* + * vlibc — . + * + * Directory streams: opendir/fdopendir/readdir/rewinddir/closedir, + * seekdir/telldir/dirfd (POSIX.1-2008 base) iterate a directory via the + * SYS_getdents64 kernel ABI, exposing one entry at a time. + * + * Level 1 (onlyposix): the base family above. + * Level 2 (muslmimic): scandir, alphasort (XSI) and readdir_r + * (obsolescent). versionsort is GNU and is not + * provided. + * + * struct dirent mirrors the x86_64 getdents64 record layout (verified by + * the static assertions below): d_ino/d_off are 64-bit, d_reclen is the + * kernel record length, d_type carries the DT_* file type, and d_name is a + * 256-byte NUL-terminated name buffer (NAME_MAX 255 + NUL). + * + * DIR is an opaque handle whose layout lives in the internal header + * src/dirent/dirent_impl.h. None of these declarations carries an intent + * attribute: every function performs I/O with side effects and reports + * failures through errno, so const/pure would be unsound (the same + * rationale sys/stat.h documents for its I/O family). + */ + +#include + +#include + +#include + +#ifdef __cplusplus +extern "C" { +#endif + +/* File type values for the d_type member of struct dirent (kernel UAPI). */ +#define DT_UNKNOWN 0 +#define DT_FIFO 1 +#define DT_CHR 2 +#define DT_DIR 4 +#define DT_BLK 6 +#define DT_REG 8 +#define DT_LNK 10 +#define DT_SOCK 12 +#define DT_WHT 14 + +/* + * One directory entry. Layout equals the x86_64 struct linux_dirent64 + * fields 0..18 verbatim; the name then follows at offset 19, stored in the + * conventional 256-byte buffer. + */ +struct dirent +{ + ino_t d_ino; /* 0: inode number */ + off_t d_off; /* 8: offset of the next entry (seek cookie) */ + unsigned short d_reclen; /* 16: length of the kernel record */ + unsigned char d_type; /* 18: DT_* file type */ + char d_name[256]; /* 19: NUL-terminated file name */ +}; + +/* Pin the layout to the x86_64 kernel ABI. */ +_Static_assert(sizeof(struct dirent) == 280, "struct dirent must match the x86_64 getdents64 layout"); +_Static_assert(offsetof(struct dirent, d_off) == 8, "d_off must sit at offset 8"); +_Static_assert(offsetof(struct dirent, d_type) == 18, "d_type must sit at offset 18"); +_Static_assert(offsetof(struct dirent, d_name) == 19, "d_name must sit at offset 19"); + +/* + * Opaque directory stream handle. The struct tag stays usable from the + * internal layout header, which defines struct vlibc_DIR (see + * src/dirent/dirent_impl.h). + */ +typedef struct vlibc_DIR DIR; + +/* + * Open the directory named by path for reading and return a directory + * stream positioned at its first entry, or NULL with errno set (a + * non-directory path fails with ENOTDIR). The stream owns a descriptor + * that closedir() releases. + */ +DIR * +opendir(const char *path); + +/* + * Like opendir(), but over the already-open descriptor fd, which must + * refer to a directory (validated with fstat). On failure NULL is returned + * with errno set and fd is left open and owned by the caller. + */ +DIR * +fdopendir(int fd); + +/* + * Return the next directory entry of dir, or NULL at the end of the + * directory (errno untouched) or on error (errno set). The result points + * at storage owned by dir and is valid until the next call to readdir, + * rewinddir, seekdir, or closedir on the same stream. + */ +struct dirent * +readdir(DIR *dir); + +/* + * Reset dir to the beginning of the directory: the next readdir returns + * the first entry again. Never fails. + */ +void +rewinddir(DIR *dir); + +/* + * Close dir, releasing its descriptor and storage. Return 0, or -1 with + * errno set if the underlying close fails. + */ +int +closedir(DIR *dir); + +/* + * Reposition dir so the next readdir resumes at the location loc, which + * must be a value previously returned by telldir (a getdents64 seek + * cookie). Never fails. + */ +void +seekdir(DIR *dir, long loc); + +/* + * Return the current location of dir, for a later seekdir. The location is + * the point after the entry most recently returned by readdir; it becomes + * indeterminate after rewinddir or closedir. + */ +long +telldir(DIR *dir); + +/* + * Return the descriptor underlying dir. The descriptor stays owned by the + * stream and remains valid until closedir. + */ +int +dirfd(DIR *dir); + +#if VLIBC_LEVEL_GE(2) + +/* Level 2 (muslmimic): XSI and obsolescent. */ + +/* + * Reentrant readdir: store the next entry in *buf and set *result to buf; + * at the end of the directory *result is NULL. Returns 0 at end of + * directory, an error number on failure (errno is not used for the error + * report), and leaves errno unmodified on success. Obsolescent. + */ +int +readdir_r(DIR *restrict dir, struct dirent *restrict buf, + struct dirent **restrict result); + +/* + * Read the whole directory named by path and store a malloc'd array of + * malloc'd struct dirent copies in *res (both released with free; the + * array is NULL-terminated with one extra NULL pointer). Only entries for + * which sel is NULL or returns nonzero are kept; cmp, when non-NULL, sorts + * the array (alphasort is the strcmp-on-name comparator). Returns the + * number of entries, or -1 with errno set. XSI. + */ +int +scandir(const char *path, struct dirent ***res, + int (*sel)(const struct dirent *), + int (*cmp)(const struct dirent **, const struct dirent **)); + +/* + * Lexicographic comparator over the d_name fields of two struct dirent + * pointers, for use as scandir's cmp argument. XSI. + */ +int +alphasort(const struct dirent **a, const struct dirent **b); + +#endif /* VLIBC_LEVEL_GE(2) */ + +#ifdef __cplusplus +} +#endif + +#endif /* VLIBC_DIRENT_H */ diff --git a/src/dirent/alphasort.c b/src/dirent/alphasort.c new file mode 100644 index 0000000..dce4df2 --- /dev/null +++ b/src/dirent/alphasort.c @@ -0,0 +1,22 @@ +#ifdef HAVE_CONFIG_H +#include +#endif + +#include + +#include + +#if VLIBC_LEVEL_GE(2) + +/* + * alphasort (todo 24, XSI): the scandir comparator that orders entries by + * strcmp on their names. Receives two pointers to the array's struct + * dirent pointers. + */ +int +alphasort(const struct dirent **a, const struct dirent **b) +{ + return strcmp((*a)->d_name, (*b)->d_name); +} + +#endif /* VLIBC_LEVEL_GE(2) */ diff --git a/src/dirent/closedir.c b/src/dirent/closedir.c new file mode 100644 index 0000000..6970b6a --- /dev/null +++ b/src/dirent/closedir.c @@ -0,0 +1,25 @@ +#ifdef HAVE_CONFIG_H +#include +#endif + +#include + +#include "../internal/malloc.h" +#include "../internal/syscall.h" +#include "dirent_impl.h" + +/* + * closedir (todo 24): release the descriptor and the stream storage. The + * descriptor is closed first so a close failure still reports -1, but the + * stream is freed either way (its errno is preserved across the free). + */ +int +closedir(DIR *dir) +{ + struct vlibc_DIR *d = dir; + int fd = d->fd; + int r = syscall_ret(__syscall1(SYS_close, fd)); + + __libc_free(d); + return r; +} diff --git a/src/dirent/dirent_impl.h b/src/dirent/dirent_impl.h new file mode 100644 index 0000000..10b7d2f --- /dev/null +++ b/src/dirent/dirent_impl.h @@ -0,0 +1,57 @@ +#ifndef VLIBC_DIRENT_INTERNAL_H +#define VLIBC_DIRENT_INTERNAL_H + +#include + +#include + +/* + * vlibc — internal directory-stream state (todo 24). + * + * struct vlibc_DIR is the completion of the opaque DIR handle from + * . readdir parses raw getdents64 records out of buf; the kernel + * fills buf with whole records and advances its own per-fd directory + * offset, so sequential iteration needs no lseek. seekdir/telldir hand the + * kernel's per-record d_off cookie (the offset of the NEXT entry) back and + * forth via lseek: d->de.d_off therefore always holds the resume point + * after the entry most recently returned by readdir. + */ +#define VLIBC_DIRENT_BUFSZ 2048 + +struct vlibc_DIR +{ + int fd; /* directory descriptor, owned by the stream */ + size_t buf_pos; /* next unparsed byte within buf */ + size_t buf_end; /* first unused byte of buf */ + struct dirent de; /* storage for the entry readdir returns */ + char buf[VLIBC_DIRENT_BUFSZ]; +}; + +/* + * The kernel-side record produced by SYS_getdents64, transcribed as the + * x86_64 linux_dirent64 layout: ino at 0, off at 8, reclen at 16, type at + * 18, name at 19. The 256-byte name array makes the struct exactly the + * size of a maximal record: 19 + (255-name + NUL) = 275, padded by struct + * alignment to 280, which is also the largest d_reclen the kernel emits + * (every record length is rounded up to a multiple of 8). readdir memcpy's + * a whole record into this struct before reading the fields, so no + * unaligned or aliasing access ever happens. + */ +struct vlibc_linux_dirent64 +{ + ino_t d_ino; + off_t d_off; + unsigned short d_reclen; + unsigned char d_type; + char d_name[256]; +}; + +_Static_assert(sizeof(struct vlibc_linux_dirent64) == 280, + "linux_dirent64 with a 256-byte name must be 280 bytes"); +_Static_assert(offsetof(struct vlibc_linux_dirent64, d_name) == 19, + "linux_dirent64 name must sit at offset 19"); +_Static_assert(offsetof(struct dirent, d_name) == + offsetof(struct vlibc_linux_dirent64, d_name), + "dirent and linux_dirent64 must share the name offset"); + +#endif /* VLIBC_DIRENT_INTERNAL_H */ diff --git a/src/dirent/dirfd.c b/src/dirent/dirfd.c new file mode 100644 index 0000000..89f5000 --- /dev/null +++ b/src/dirent/dirfd.c @@ -0,0 +1,19 @@ +#ifdef HAVE_CONFIG_H +#include +#endif + +#include + +#include "dirent_impl.h" + +/* + * dirfd (todo 24): return the descriptor the stream reads through. The + * descriptor stays owned by the stream; closedir closes it. + */ +int +dirfd(DIR *dir) +{ + struct vlibc_DIR *d = dir; + + return d->fd; +} diff --git a/src/dirent/fdopendir.c b/src/dirent/fdopendir.c new file mode 100644 index 0000000..7f82885 --- /dev/null +++ b/src/dirent/fdopendir.c @@ -0,0 +1,47 @@ +#ifdef HAVE_CONFIG_H +#include +#endif + +#include + +#include + +#include + +#include "../internal/malloc.h" +#include "../internal/syscall.h" +#include "dirent_impl.h" + +/* + * fdopendir (todo 24): validate the descriptor with SYS_fstat (POSIX + * requires the fd to name a directory), then allocate the stream state. + * On failure the descriptor is NOT closed — it stays owned by the caller, + * exactly as passed in. + */ +DIR * +fdopendir(int fd) +{ + struct stat st; + DIR *d; + + if (syscall_ret(__syscall2(SYS_fstat, fd, (long)&st)) < 0) + { + return NULL; + } + if (!S_ISDIR(st.st_mode)) + { + errno = ENOTDIR; + return NULL; + } + d = __libc_malloc(sizeof *d); + if (d == NULL) + { + errno = ENOMEM; + return NULL; + } + d->fd = fd; + d->buf_pos = 0; + d->buf_end = 0; + d->de.d_off = 0; + return d; +} diff --git a/src/dirent/opendir.c b/src/dirent/opendir.c new file mode 100644 index 0000000..9ac7b70 --- /dev/null +++ b/src/dirent/opendir.c @@ -0,0 +1,37 @@ +#ifdef HAVE_CONFIG_H +#include +#endif + +#include + +#include + +#include "../internal/syscall.h" +#include "dirent_impl.h" + +/* + * opendir (todo 24): open the directory via SYS_openat with O_DIRECTORY so + * the kernel rejects non-directories with ENOTDIR before any stream state + * exists, then hand the descriptor to fdopendir (which re-validates with + * fstat). On fdopendir failure the descriptor is closed again: opendir + * never leaks. + */ +DIR * +opendir(const char *path) +{ + int fd = syscall_ret( + __syscall3(SYS_openat, AT_FDCWD, (long)path, O_RDONLY | O_DIRECTORY)); + DIR *d; + + if (fd < 0) + { + return NULL; + } + d = fdopendir(fd); + if (d == NULL) + { + syscall_ret(__syscall1(SYS_close, fd)); + return NULL; + } + return d; +} diff --git a/src/dirent/readdir.c b/src/dirent/readdir.c new file mode 100644 index 0000000..d611206 --- /dev/null +++ b/src/dirent/readdir.c @@ -0,0 +1,78 @@ +#ifdef HAVE_CONFIG_H +#include +#endif + +#include + +#include + +#include + +#include "../internal/syscall.h" +#include "dirent_impl.h" + +/* + * readdir (todo 24): expose the entries the kernel wrote into the stream + * buffer by SYS_getdents64, one per call. When the buffer is exhausted a + * fresh getdents64 continues at the kernel's per-fd directory offset (no + * lseek needed for sequential iteration). A zero-length result is end of + * directory: NULL is returned with errno untouched. Each record is copied + * into the stream's own struct dirent, so the returned pointer stays valid + * until the next call on the same stream. + */ +struct dirent * +readdir(DIR *dir) +{ + struct vlibc_DIR *d = dir; + struct vlibc_linux_dirent64 k; + unsigned short reclen; + size_t namelen; + + for (;;) + { + if (d->buf_pos >= d->buf_end) + { + long r = + __syscall3(SYS_getdents64, d->fd, (long)d->buf, + (long)sizeof(d->buf)); + + if (r <= 0) + { + syscall_ret(r); + return NULL; + } + d->buf_pos = 0; + d->buf_end = (size_t)r; + } + + if (d->buf_end - d->buf_pos < offsetof(struct vlibc_linux_dirent64, d_name)) + { + return NULL; + } + memcpy(&reclen, + d->buf + d->buf_pos + offsetof(struct vlibc_linux_dirent64, d_reclen), + sizeof reclen); + if (reclen < offsetof(struct vlibc_linux_dirent64, d_name) + 2 || + reclen > sizeof k) + { + /* The kernel always emits whole well-formed records; a broken + * length would otherwise walk off the buffer. Stop the scan. */ + return NULL; + } + memcpy(&k, d->buf + d->buf_pos, reclen); + d->buf_pos += reclen; + + d->de.d_ino = k.d_ino; + d->de.d_off = k.d_off; + d->de.d_reclen = reclen; + d->de.d_type = k.d_type; + namelen = (size_t)reclen - offsetof(struct vlibc_linux_dirent64, d_name); + if (namelen >= sizeof(d->de.d_name)) + { + namelen = sizeof(d->de.d_name) - 1; + } + memcpy(d->de.d_name, k.d_name, namelen); + d->de.d_name[namelen] = '\0'; + return &d->de; + } +} diff --git a/src/dirent/readdir_r.c b/src/dirent/readdir_r.c new file mode 100644 index 0000000..8dc0e5c --- /dev/null +++ b/src/dirent/readdir_r.c @@ -0,0 +1,46 @@ +#ifdef HAVE_CONFIG_H +#include +#endif + +#include + +#include + +#include + +#if VLIBC_LEVEL_GE(2) + +/* + * readdir_r (todo 24, obsolescent): reentrant readdir. The result is + * copied into the caller's buf and *result points at it; at end of + * directory *result is NULL. Errors are reported as the return value + * (never through errno), and errno is preserved across successful calls. + * readdir_r is distinguished from end-of-directory by checking whether + * readdir moved errno. + */ +int +readdir_r(DIR *restrict dir, struct dirent *restrict buf, + struct dirent **restrict result) +{ + struct dirent *de; + int saved = errno; + + errno = 0; + de = readdir(dir); + if (de != NULL) + { + memcpy(buf, de, sizeof *buf); + *result = buf; + errno = saved; + return 0; + } + if (errno != 0) + { + return errno; + } + errno = saved; + *result = NULL; + return 0; +} + +#endif /* VLIBC_LEVEL_GE(2) */ diff --git a/src/dirent/rewinddir.c b/src/dirent/rewinddir.c new file mode 100644 index 0000000..2e7587f --- /dev/null +++ b/src/dirent/rewinddir.c @@ -0,0 +1,29 @@ +#ifdef HAVE_CONFIG_H +#include +#endif + +#include + +#include + +#include "../internal/syscall.h" +#include "dirent_impl.h" + +/* + * rewinddir (todo 24): return the stream to the start of the directory. + * SYS_lseek back to offset 0 resets the kernel's per-fd directory + * position; the cached records (if any) are dropped and the stored seek + * cookie is reset, so the next readdir refills from the first entry. + * rewinddir has no error return; a failed lseek leaves the stream + * positioned wherever the kernel is. + */ +void +rewinddir(DIR *dir) +{ + struct vlibc_DIR *d = dir; + + syscall_ret(__syscall3(SYS_lseek, d->fd, 0, SEEK_SET)); + d->buf_pos = 0; + d->buf_end = 0; + d->de.d_off = 0; +} diff --git a/src/dirent/scandir.c b/src/dirent/scandir.c new file mode 100644 index 0000000..0cd318d --- /dev/null +++ b/src/dirent/scandir.c @@ -0,0 +1,124 @@ +#ifdef HAVE_CONFIG_H +#include +#endif + +#include + +#include + +#include + +#include + +#include + +#include "../internal/malloc.h" + +#if VLIBC_LEVEL_GE(2) + +/* + * scandir (todo 24, XSI): walk the whole directory and hand back a sorted, + * NULL-terminated array of heap copies. Entries are copied into + * minimal-size blocks (header fields plus the name), all released with the + * public free(); the array itself is a malloc'd growable buffer resized + * with realloc. "." and ".." are reported like any other entry. + */ + +/* Copy one entry into minimal heap storage sized for its name. */ +static struct dirent * +dirent_dup(const struct dirent *de) +{ + size_t nlen = strlen(de->d_name) + 1; + struct dirent *nd = + __libc_malloc(offsetof(struct dirent, d_name) + nlen); + + if (nd == NULL) + { + return NULL; + } + memcpy(nd, de, offsetof(struct dirent, d_name)); + memcpy(nd->d_name, de->d_name, nlen); + return nd; +} + +int +scandir(const char *path, struct dirent ***res, + int (*sel)(const struct dirent *), + int (*cmp)(const struct dirent **, const struct dirent **)) +{ + DIR *d; + struct dirent *de; + struct dirent **names = NULL; + size_t cnt = 0; + size_t cap = 0; + + d = opendir(path); + if (d == NULL) + { + return -1; + } + while ((de = readdir(d)) != NULL) + { + if (sel != NULL && !sel(de)) + { + continue; + } + if (cnt == cap) + { + size_t ncap = cap == 0 ? 8 : cap * 2; + + if (ncap <= cap || ncap > (size_t)-1 / sizeof *names) + { + errno = ENOMEM; + goto fail; + } + names = realloc(names, ncap * sizeof *names); + if (names == NULL) + { + goto fail; + } + cap = ncap; + } + names[cnt] = dirent_dup(de); + if (names[cnt] == NULL) + { + errno = ENOMEM; + goto fail; + } + cnt++; + } + closedir(d); + + if (cmp != NULL) + { + qsort(names, cnt, sizeof *names, + (int (*)(const void *, const void *))cmp); + } + names = realloc(names, (cnt + 1) * sizeof *names); + if (names == NULL) + { + goto fail_nofree; + } + names[cnt] = NULL; + *res = names; + return (int)cnt; + +fail: + while (cnt > 0) + { + __libc_free(names[--cnt]); + } + __libc_free(names); + closedir(d); + return -1; + +fail_nofree: + while (cnt > 0) + { + __libc_free(names[--cnt]); + } + __libc_free(names); + return -1; +} + +#endif /* VLIBC_LEVEL_GE(2) */ diff --git a/src/dirent/seekdir.c b/src/dirent/seekdir.c new file mode 100644 index 0000000..3603ca6 --- /dev/null +++ b/src/dirent/seekdir.c @@ -0,0 +1,31 @@ +#ifdef HAVE_CONFIG_H +#include +#endif + +#include + +#include + +#include "../internal/syscall.h" +#include "dirent_impl.h" + +/* + * seekdir (todo 24): reposition the stream to the directory location loc + * (a d_off cookie previously returned by telldir). SYS_lseek to that + * cookie makes the next SYS_getdents64 resume at the entry the cookie + * denotes; any records cached ahead of the seek point are dropped. The + * kernel accepts exactly the cookies getdents64 hands out, which is all + * telldir ever returns. seekdir has no error return; on a failed lseek + * the buffered records are kept so reading continues where it left off. + */ +void +seekdir(DIR *dir, long loc) +{ + struct vlibc_DIR *d = dir; + + if (syscall_ret(__syscall3(SYS_lseek, d->fd, (long)loc, SEEK_SET)) >= 0) + { + d->buf_pos = 0; + d->buf_end = 0; + } +} diff --git a/src/dirent/telldir.c b/src/dirent/telldir.c new file mode 100644 index 0000000..a9e20c0 --- /dev/null +++ b/src/dirent/telldir.c @@ -0,0 +1,22 @@ +#ifdef HAVE_CONFIG_H +#include +#endif + +#include + +#include "dirent_impl.h" + +/* + * telldir (todo 24): return the stream's current directory location. + * readdir stores the kernel's d_off cookie of every entry it hands out — + * the offset at which iteration resumes AFTER that entry — so the cookie + * of the most recently returned entry is exactly the location a matching + * seekdir must restore. A fresh or rewound stream reports 0, the start. + */ +long +telldir(DIR *dir) +{ + struct vlibc_DIR *d = dir; + + return (long)d->de.d_off; +} diff --git a/tests/test_dirent.c b/tests/test_dirent.c new file mode 100644 index 0000000..cffcb15 --- /dev/null +++ b/tests/test_dirent.c @@ -0,0 +1,423 @@ +/* + * vlibc — dirent test (todo 24). + * + * Exercises the dirent.h surface end to end against a scratch directory + * created with raw syscalls: + * + * 1. opendir on the temp dir lists exactly . .. alpha beta gamma; every + * entry carries a nonzero d_ino; . reports d_type DT_DIR and alpha + * DT_REG. + * 2. readdir past the end returns NULL; a repeated readdir stays NULL. + * 3. rewinddir re-lists the same five entries. + * 4. dirfd returns a usable descriptor; fdopendir succeeds on a raw + * directory descriptor. + * 5. Level 2: scandir + alphasort return the five entries sorted + * (. .. alpha beta gamma), NULL-terminated, with d_type copied, and + * the caller can free() every element and the array. + * + * The -f mode covers the failure paths, all asserted on return values + * only: opendir of a nonexistent directory returns NULL, fdopendir of a + * regular-file descriptor returns NULL, and a seekdir back to a saved + * telldir location resumes at the entry that originally followed it. The + * library writes errno on these paths, so -f exits through raw + * SYS_exit_group (house pattern); the default mode never triggers an + * errno-writing library path. + * + * All diagnostics go through raw SYS_write (no stdio); the test reads no + * host headers — under -Iinclude the vlibc public headers shadow GCC's + * internal ones. Not part of the library proper; compiled manually for + * this todo (the tests/ + make check wiring is owned by a later todo). + */ + +#include + +#include + +#include + +#include + +#include "../src/internal/syscall.h" + +#define DIRPATH "/tmp/vlibc-t24" + +static const char *const FILES[3] = { + DIRPATH "/alpha", + DIRPATH "/beta", + DIRPATH "/gamma", +}; + +static int failures; + +/* Write a NUL-terminated string to fd via the raw syscall layer. The + * optimize attribute keeps GCC from lowering the length loop into a + * strlen call, which would leave a vlibc-owned symbol undefined in this + * host-linked standalone binary (house idiom, see src/string). */ +static __attribute__((optimize("no-tree-loop-distribute-patterns"))) void +say(int fd, const char *s) +{ + long n = 0; + + while (s[n] != '\0') + { + n++; + } + __syscall3(SYS_write, fd, (long)s, n); +} + +/* Write v in decimal to fd. */ +static void +say_dec(int fd, unsigned long v) // NOLINT(bugprone-easily-swappable-parameters) +{ + char buf[24]; + int i = (int)sizeof(buf); + + buf[--i] = '\0'; + do + { + buf[--i] = (char)('0' + (v % 10)); + v /= 10; + } while (v != 0); + __syscall3(SYS_write, fd, (long)(buf + i), (long)(sizeof(buf) - 1 - i)); +} + +static void +check(int cond, const char *what) +{ + if (cond) + { + say(1, "PASS: "); + say(1, what); + say(1, "\n"); + } + else + { + say(2, "FAIL: "); + say(2, what); + say(2, "\n"); + failures++; + } +} + +/* Drop any leftover scratch dir (files, then the dir itself); errors are + * fine when nothing exists yet. */ +static void +rm_tree(void) +{ + int i; + + for (i = 0; i < 3; i++) + { + __syscall3(SYS_unlinkat, AT_FDCWD, (long)FILES[i], 0); + } + __syscall3(SYS_unlinkat, AT_FDCWD, (long)DIRPATH, AT_REMOVEDIR); +} + +/* Create the scratch directory and its three one-byte files. */ +static int +make_dir(void) +{ + int fd; + int i; + + rm_tree(); + if ((int)__syscall3(SYS_mkdirat, AT_FDCWD, (long)DIRPATH, 0700) != 0) + { + return -1; + } + for (i = 0; i < 3; i++) + { + fd = (int)__syscall4(SYS_openat, AT_FDCWD, (long)FILES[i], + O_WRONLY | O_CREAT | O_TRUNC, 0600); + if (fd < 0) + { + return -1; + } + __syscall3(SYS_write, fd, (long)"x", 1); + __syscall1(SYS_close, fd); + } + return 0; +} + +/* A full listing captured as copied names. */ +struct listing +{ + char names[8][64]; + int count; +}; + +/* Drain dir, copying each entry name into ls. */ +static void +fill_listing(DIR *d, struct listing *ls) +{ + struct dirent *e; + int n = 0; + + while ((e = readdir(d)) != NULL) + { + if (n < 8) + { + strcpy(ls->names[n], e->d_name); + } + n++; + } + ls->count = n; +} + +static int +has_name(const struct listing *ls, const char *name) +{ + int i; + + for (i = 0; i < ls->count; i++) + { + if (strcmp(ls->names[i], name) == 0) + { + return 1; + } + } + return 0; +} + +static int +same_sets(const struct listing *x, const struct listing *y) +{ + int i; + + if (x->count != y->count) + { + return 0; + } + for (i = 0; i < x->count; i++) + { + if (!has_name(y, x->names[i])) + { + return 0; + } + } + return 1; +} + +/* Scenarios 1-4: iteration, exhaustion, rewind, dirfd/fdopendir. */ +static void +basic_scenarios(void) +{ + struct listing first; + struct listing second; + DIR *d; + struct dirent *e; + int n = 0; + + d = opendir(DIRPATH); + check(d != NULL, "opendir on the temp directory succeeds"); + if (d == NULL) + { + return; + } + check(dirfd(d) >= 0, "dirfd on the open stream returns a valid descriptor"); + + first.count = 0; + while ((e = readdir(d)) != NULL) + { + check(e->d_ino != 0, "every entry reports a nonzero d_ino"); + if (strcmp(e->d_name, ".") == 0) + { + check(e->d_type == DT_DIR, "the . entry carries d_type DT_DIR"); + } + if (strcmp(e->d_name, "alpha") == 0) + { + check(e->d_type == DT_REG, "the alpha file carries d_type DT_REG"); + } + if (n < 8) + { + strcpy(first.names[n], e->d_name); + } + n++; + } + first.count = n; + check(first.count == 5, "the first pass lists exactly 5 entries"); + check(readdir(d) == NULL, "readdir at end of directory returns NULL"); + check(readdir(d) == NULL, "a further readdir at end still returns NULL"); + check(has_name(&first, ".") && has_name(&first, "..") && + has_name(&first, "alpha") && has_name(&first, "beta") && + has_name(&first, "gamma"), + "the first pass sees . .. alpha beta gamma"); + + rewinddir(d); + fill_listing(d, &second); + check(second.count == 5, "after rewinddir the second pass lists 5 entries again"); + check(same_sets(&first, &second), "the rewound listing matches the first listing"); + check(closedir(d) == 0, "closedir returns 0"); + + { + int fd = (int)__syscall3(SYS_openat, AT_FDCWD, (long)DIRPATH, + O_RDONLY | O_DIRECTORY); + DIR *dd; + + check(fd >= 0, "raw open of the directory succeeds"); + if (fd < 0) + { + return; + } + dd = fdopendir(fd); + check(dd != NULL, "fdopendir on a directory descriptor succeeds"); + if (dd != NULL) + { + check(closedir(dd) == 0, "closedir after fdopendir returns 0"); + } + else + { + __syscall1(SYS_close, fd); + } + } +} + +#if VLIBC_LEVEL_GE(2) + +/* Scenario 5 (level 2): scandir + alphasort. */ +static void +scandir_scenario(void) +{ + struct dirent **names = NULL; + int n; + int i; + + n = scandir(DIRPATH, &names, NULL, alphasort); + check(n == 5, "scandir returns the 5 entries"); + if (names == NULL) + { + return; + } + check(names[n] == NULL, "the scandir array is NULL-terminated"); + check(strcmp(names[0]->d_name, ".") == 0 && strcmp(names[1]->d_name, "..") == 0, + "alphasort puts . and .. first in that order"); + check(strcmp(names[2]->d_name, "alpha") == 0 && + strcmp(names[3]->d_name, "beta") == 0 && + strcmp(names[4]->d_name, "gamma") == 0, + "alphasort orders the files alpha beta gamma"); + check(names[2]->d_type == DT_REG, "scandir copies d_type into its entries"); + for (i = 0; i < n; i++) + { + free(names[i]); + } + free(names); +} + +#endif /* VLIBC_LEVEL_GE(2) */ + +/* + * Failure scenarios (-f): every assertion is on the return value only. + * opendir/fdopendir write errno on these paths (host-TCB hazard), so the + * process exits through raw SYS_exit_group. + */ +static int +failure_scenarios(void) +{ + struct dirent *e1; + struct dirent *e2; + struct dirent *e3; + int rc = 0; + int fdf; + long loc; + DIR *d; + + if (make_dir() != 0) + { + say(2, "FAIL: cannot set up the temp directory\n"); + return 1; + } + if (opendir("/nonexistent/vlibc-dir") != NULL) + { + say(2, "FAIL: opendir on a nonexistent directory did not return NULL\n"); + rc = 1; + } + else + { + say(1, "PASS: opendir on a nonexistent directory -> NULL\n"); + } + + fdf = (int)__syscall3(SYS_openat, AT_FDCWD, (long)FILES[0], O_RDONLY); + if (fdf >= 0) + { + d = fdopendir(fdf); + if (d != NULL) + { + say(2, "FAIL: fdopendir on a regular-file descriptor did not return NULL\n"); + rc = 1; + closedir(d); + } + else + { + say(1, "PASS: fdopendir on a regular-file descriptor -> NULL\n"); + __syscall1(SYS_close, fdf); + } + } + + d = opendir(DIRPATH); + if (d == NULL) + { + say(2, "FAIL: opendir failed for the seekdir round trip\n"); + rc = 1; + } + else + { + e1 = readdir(d); + loc = telldir(d); + e2 = readdir(d); + if (e1 == NULL || e2 == NULL) + { + say(2, "FAIL: not enough entries for the seekdir round trip\n"); + rc = 1; + } + else + { + seekdir(d, loc); + e3 = readdir(d); + if (e3 != NULL && strcmp(e3->d_name, e2->d_name) == 0) + { + say(1, "PASS: seekdir back to a telldir location resumes at the same entry\n"); + } + else + { + say(2, "FAIL: seekdir did not resume at the telldir location\n"); + rc = 1; + } + } + closedir(d); + } + rm_tree(); + return rc; +} + +int +main(int argc, char **argv) +{ + if (argc == 2 && argv[1][0] == '-' && argv[1][1] == 'f') + { + int rc = failure_scenarios(); + + __syscall1(SYS_exit_group, rc); + return rc; /* not reached */ + } + + if (make_dir() != 0) + { + say(2, "FAIL: cannot set up the temp directory\n"); + return 1; + } + basic_scenarios(); +#if VLIBC_LEVEL_GE(2) + scandir_scenario(); +#endif + rm_tree(); + + if (failures > 0) + { + say(2, "FAILED ("); + say_dec(2, (unsigned long)failures); + say(2, " check(s))\n"); + return 1; + } + say(1, "all dirent tests passed\n"); + return 0; +}