From 01034fc66888d09e7701f79aba972f8c7dce4b4f Mon Sep 17 00:00:00 2001 From: Logikoma Date: Mon, 3 Aug 2026 09:02:19 -0400 Subject: [PATCH] Document file metadata wrapper behavior Co-authored-by: Andrew Kesterson --- src/stat.c | 37 +++++++++++++++++++++++++++++++++++++ tests/test_stat.c | 19 +++++++++++++++++++ 2 files changed, 56 insertions(+) diff --git a/src/stat.c b/src/stat.c index 92e5ae9..29e799c 100644 --- a/src/stat.c +++ b/src/stat.c @@ -3,6 +3,12 @@ #include #include "aksl_internal.h" +/* + * stat(2) follows pathname through any symbolic links and writes the target's + * metadata into the caller-owned struct stat. A file that is gone, cannot be + * searched, or lives below a non-directory component is reported as the errno + * from libc rather than as a library-specific status. + */ akerr_ErrorContext AKERR_NOIGNORE *aksl_stat(const char *pathname, struct stat *dest) { PREPARE_ERROR(e); @@ -12,6 +18,12 @@ akerr_ErrorContext AKERR_NOIGNORE *aksl_stat(const char *pathname, struct stat * FAIL_NONZERO_RETURN(e, stat(pathname, dest), AKSL_ERRNO_OR(AKERR_IO), "pathname=%s", pathname); SUCCEED_RETURN(e); } + +/* + * lstat(2) is stat(2) without the final symbolic-link traversal. That is the + * difference a caller needs when it is deciding whether a path is a link or + * when the link's ownership and mode are the metadata of interest. + */ akerr_ErrorContext AKERR_NOIGNORE *aksl_lstat(const char *pathname, struct stat *dest) { PREPARE_ERROR(e); @@ -21,11 +33,24 @@ akerr_ErrorContext AKERR_NOIGNORE *aksl_lstat(const char *pathname, struct stat FAIL_NONZERO_RETURN(e, lstat(pathname, dest), AKSL_ERRNO_OR(AKERR_IO), "pathname=%s", pathname); SUCCEED_RETURN(e); } + +/* + * fstat(2) gets metadata from an already-open descriptor, so it has no path + * lookup race and remains useful after the file has been renamed or unlinked. + * A closed or otherwise invalid descriptor reports EBADF from libc. + */ akerr_ErrorContext AKERR_NOIGNORE *aksl_fstat(int fd, struct stat *dest) { PREPARE_ERROR(e); FAIL_ZERO_RETURN(e, dest, AKERR_NULLPOINTER, "fd=%d, dest=%p", fd, (void *)dest); errno = 0; FAIL_NONZERO_RETURN(e, fstat(fd, dest), AKSL_ERRNO_OR(AKERR_IO), "fd=%d", fd); SUCCEED_RETURN(e); } + +/* + * fstatat(2) is the directory-descriptor form of stat(2). pathname is resolved + * relative to dirfd unless it is absolute, and flags retain the libc choices + * such as inspecting a link itself. Keeping those flags unchanged prevents this + * wrapper from inventing a smaller policy than the POSIX call already exposes. + */ akerr_ErrorContext AKERR_NOIGNORE *aksl_fstatat(int dirfd, const char *pathname, struct stat *dest, int flags) { PREPARE_ERROR(e); @@ -35,6 +60,12 @@ akerr_ErrorContext AKERR_NOIGNORE *aksl_fstatat(int dirfd, const char *pathname, FAIL_NONZERO_RETURN(e, fstatat(dirfd, pathname, dest, flags), AKSL_ERRNO_OR(AKERR_IO), "dirfd=%d pathname=%s flags=%d", dirfd, pathname, flags); SUCCEED_RETURN(e); } + +/* + * statvfs(3) writes information about the mounted filesystem containing path, + * not merely the named file. The result includes the filesystem block sizes and + * available space the caller needs before it decides whether an operation fits. + */ akerr_ErrorContext AKERR_NOIGNORE *aksl_statvfs(const char *path, struct statvfs *dest) { PREPARE_ERROR(e); @@ -44,6 +75,12 @@ akerr_ErrorContext AKERR_NOIGNORE *aksl_statvfs(const char *path, struct statvfs FAIL_NONZERO_RETURN(e, statvfs(path, dest), AKSL_ERRNO_OR(AKERR_IO), "path=%s", path); SUCCEED_RETURN(e); } + +/* + * fstatvfs(3) is the descriptor form of statvfs(3). It asks the filesystem that + * owns fd for the same capacity and flag information without resolving a path + * again, and reports a bad descriptor through the errno libc supplies. + */ akerr_ErrorContext AKERR_NOIGNORE *aksl_fstatvfs(int fd, struct statvfs *dest) { PREPARE_ERROR(e); FAIL_ZERO_RETURN(e, dest, AKERR_NULLPOINTER, "fd=%d, dest=%p", fd, (void *)dest); diff --git a/tests/test_stat.c b/tests/test_stat.c index 9c6d254..14d4244 100644 --- a/tests/test_stat.c +++ b/tests/test_stat.c @@ -10,6 +10,7 @@ static int test_stat_success_and_fstat(void) struct stat path_dest; struct stat fd_dest; struct statvfs vfs_dest; + /* A real file makes the mode and four-byte size observable to stat(2). */ AKSL_CHECK(aksl_temp_file(path, sizeof(path)) == 0); fd = open(path, O_RDWR); AKSL_CHECK(fd >= 0); @@ -17,11 +18,17 @@ static int test_stat_success_and_fstat(void) AKSL_CHECK_OK(aksl_stat(path, &path_dest)); AKSL_CHECK(S_ISREG(path_dest.st_mode)); AKSL_CHECK(path_dest.st_size == 4); + + /* fstat(2) addresses the same opened inode, not a second pathname lookup. */ AKSL_CHECK_OK(aksl_fstat(fd, &fd_dest)); AKSL_CHECK(fd_dest.st_ino == path_dest.st_ino); + + /* fstatvfs(3) describes the backing filesystem and has a usable block size. */ AKSL_CHECK_OK(aksl_fstatvfs(fd, &vfs_dest)); AKSL_CHECK(vfs_dest.f_frsize != 0); AKSL_CHECK(close(fd) == 0); + + /* A descriptor that was just closed must surface libc's EBADF. */ AKSL_CHECK_STATUS(aksl_fstat(fd, &fd_dest), EBADF); AKSL_CHECK(unlink(path) == 0); return 0; @@ -35,20 +42,30 @@ static int test_stat_paths_and_fstatat(void) struct stat dest; struct stat ldest; struct statvfs vdest; + /* Missing paths and a regular file used as a directory preserve errno. */ AKSL_CHECK_STATUS(aksl_stat("/nonexistent/aksl/stat", &dest), ENOENT); + /* The temporary path is a regular file, so adding a child tests ENOTDIR. */ AKSL_CHECK(aksl_temp_file(path, sizeof(path)) == 0); AKSL_CHECK(snprintf(child, sizeof(child), "%s/child", path) < (int)sizeof(child)); AKSL_CHECK_STATUS(aksl_stat(child, &dest), ENOTDIR); AKSL_CHECK(snprintf(linkpath, sizeof(linkpath), "%s.link", path) < (int)sizeof(linkpath)); AKSL_CHECK(symlink(path, linkpath) == 0); + + /* stat follows the link while lstat reports the link object itself. */ AKSL_CHECK_OK(aksl_stat(linkpath, &dest)); AKSL_CHECK_OK(aksl_lstat(linkpath, &ldest)); AKSL_CHECK(S_ISREG(dest.st_mode)); AKSL_CHECK(S_ISLNK(ldest.st_mode)); + + /* AT_FDCWD makes fstatat resolve this path from the current directory. */ AKSL_CHECK_OK(aksl_fstatat(AT_FDCWD, path, &dest, 0)); + + /* Invalid flags must replace stale errno with the EINVAL libc reports. */ errno = E2BIG; AKSL_CHECK_STATUS(aksl_fstatat(AT_FDCWD, path, &dest, 0x40000000), EINVAL); AKSL_CHECK(errno == EINVAL); + + /* statvfs reports the filesystem containing the current directory. */ AKSL_CHECK_OK(aksl_statvfs(".", &vdest)); AKSL_CHECK(vdest.f_frsize != 0); AKSL_CHECK(unlink(linkpath) == 0); @@ -61,7 +78,9 @@ static int test_stat_null_arguments(void) char path[AKSL_TMP_MAX]; struct stat dest; struct statvfs vdest; + /* Create a valid path so each failure below isolates a NULL argument. */ AKSL_CHECK(aksl_temp_file(path, sizeof(path)) == 0); + /* Every wrapper refuses either missing caller-owned input or output storage. */ AKSL_CHECK_STATUS(aksl_stat(NULL, &dest), AKERR_NULLPOINTER); AKSL_CHECK_STATUS(aksl_stat(path, NULL), AKERR_NULLPOINTER); AKSL_CHECK_STATUS(aksl_lstat(NULL, &dest), AKERR_NULLPOINTER);