feat(dirent): directory iteration

This commit is contained in:
2026-09-05 21:08:57 -04:00
parent a2f7c33601
commit 5af4197ee9
14 changed files with 1139 additions and 0 deletions
+22
View File
@@ -0,0 +1,22 @@
#ifdef HAVE_CONFIG_H
#include <config.h>
#endif
#include <dirent.h>
#include <string.h>
#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) */
+25
View File
@@ -0,0 +1,25 @@
#ifdef HAVE_CONFIG_H
#include <config.h>
#endif
#include <dirent.h>
#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;
}
+57
View File
@@ -0,0 +1,57 @@
#ifndef VLIBC_DIRENT_INTERNAL_H
#define VLIBC_DIRENT_INTERNAL_H
#include <dirent.h>
#include <stddef.h>
/*
* vlibc — internal directory-stream state (todo 24).
*
* struct vlibc_DIR is the completion of the opaque DIR handle from
* <dirent.h>. 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 */
+19
View File
@@ -0,0 +1,19 @@
#ifdef HAVE_CONFIG_H
#include <config.h>
#endif
#include <dirent.h>
#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;
}
+47
View File
@@ -0,0 +1,47 @@
#ifdef HAVE_CONFIG_H
#include <config.h>
#endif
#include <dirent.h>
#include <errno.h>
#include <sys/stat.h>
#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;
}
+37
View File
@@ -0,0 +1,37 @@
#ifdef HAVE_CONFIG_H
#include <config.h>
#endif
#include <dirent.h>
#include <fcntl.h>
#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;
}
+78
View File
@@ -0,0 +1,78 @@
#ifdef HAVE_CONFIG_H
#include <config.h>
#endif
#include <dirent.h>
#include <stddef.h>
#include <string.h>
#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;
}
}
+46
View File
@@ -0,0 +1,46 @@
#ifdef HAVE_CONFIG_H
#include <config.h>
#endif
#include <dirent.h>
#include <errno.h>
#include <string.h>
#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) */
+29
View File
@@ -0,0 +1,29 @@
#ifdef HAVE_CONFIG_H
#include <config.h>
#endif
#include <dirent.h>
#include <unistd.h>
#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;
}
+124
View File
@@ -0,0 +1,124 @@
#ifdef HAVE_CONFIG_H
#include <config.h>
#endif
#include <dirent.h>
#include <errno.h>
#include <stddef.h>
#include <stdlib.h>
#include <string.h>
#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) */
+31
View File
@@ -0,0 +1,31 @@
#ifdef HAVE_CONFIG_H
#include <config.h>
#endif
#include <dirent.h>
#include <unistd.h>
#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;
}
}
+22
View File
@@ -0,0 +1,22 @@
#ifdef HAVE_CONFIG_H
#include <config.h>
#endif
#include <dirent.h>
#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;
}