From 8c94231167d084857c81e5a1b1a94c17aa5b1731 Mon Sep 17 00:00:00 2001 From: Tachikoma Date: Mon, 3 Aug 2026 07:34:16 -0400 Subject: [PATCH 1/3] Wrap file metadata calls Co-authored-by: Andrew Kesterson --- CMakeLists.txt | 2 ++ include/akstdlib.h | 53 ++++++++++++++++++++++++++++ src/stat.c | 51 +++++++++++++++++++++++++++ tests/test_stat.c | 86 ++++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 192 insertions(+) create mode 100644 src/stat.c create mode 100644 tests/test_stat.c diff --git a/CMakeLists.txt b/CMakeLists.txt index b6d9965..3ea02a4 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -207,6 +207,7 @@ add_library(akstdlib SHARED src/stdlib.c src/string.c src/stream.c + src/stat.c src/collections.c ) @@ -317,6 +318,7 @@ set(AKSL_TESTS strbuf stream streamio + stat strhash string strto diff --git a/include/akstdlib.h b/include/akstdlib.h index 8fe8282..84fef89 100644 --- a/include/akstdlib.h +++ b/include/akstdlib.h @@ -76,6 +76,8 @@ #include #include #include +#include +#include /* off_t, for the aksl_fseeko/aksl_ftello pair. POSIX, like aksl_realpath. */ #include @@ -808,6 +810,57 @@ akerr_ErrorContext AKERR_NOIGNORE *aksl_strhash_djb2_str(const char *str, uint32 /** @} */ +/** + * @brief stat(2). + * @param[in] pathname Path to inspect. Required. + * @param[out] dest File metadata. Required. + * @return NULL on success, an error context otherwise. + */ +akerr_ErrorContext AKERR_NOIGNORE *aksl_stat(const char *pathname, struct stat *dest); + +/** + * @brief lstat(2), inspecting a symbolic link itself. + * @param[in] pathname Path to inspect. Required. + * @param[out] dest File metadata. Required. + * @return NULL on success, an error context otherwise. + */ +akerr_ErrorContext AKERR_NOIGNORE *aksl_lstat(const char *pathname, struct stat *dest); + +/** + * @brief fstat(2). + * @param[in] fd Open file descriptor. + * @param[out] dest File metadata. Required. + * @return NULL on success, an error context otherwise. + */ +akerr_ErrorContext AKERR_NOIGNORE *aksl_fstat(int fd, struct stat *dest); + +/** + * @brief fstatat(2). + * @param[in] dirfd Directory descriptor, or AT_FDCWD. + * @param[in] pathname Path to inspect. Required. + * @param[out] dest File metadata. Required. + * @param[in] flags libc fstatat flags, passed unchanged. + * @return NULL on success, an error context otherwise. + */ +akerr_ErrorContext AKERR_NOIGNORE *aksl_fstatat(int dirfd, const char *pathname, struct stat *dest, int flags); + +/** + * @brief statvfs(3). + * @param[in] path Path on the filesystem. Required. + * @param[out] dest Filesystem metadata. Required. + * @return NULL on success, an error context otherwise. + */ +akerr_ErrorContext AKERR_NOIGNORE *aksl_statvfs(const char *path, struct statvfs *dest); + +/** + * @brief fstatvfs(3). + * @param[in] fd Open file descriptor. + * @param[out] dest Filesystem metadata. Required. + * @return NULL on success, an error context otherwise. + */ +akerr_ErrorContext AKERR_NOIGNORE *aksl_fstatvfs(int fd, struct statvfs *dest); + + /* ====================================================================== */ /** @name Streams: open, read, write, close * diff --git a/src/stat.c b/src/stat.c new file mode 100644 index 0000000..92e5ae9 --- /dev/null +++ b/src/stat.c @@ -0,0 +1,51 @@ +/* sys/stat.h and sys/statvfs.h metadata wrappers. */ +#include +#include +#include "aksl_internal.h" + +akerr_ErrorContext AKERR_NOIGNORE *aksl_stat(const char *pathname, struct stat *dest) +{ + PREPARE_ERROR(e); + FAIL_ZERO_RETURN(e, pathname, AKERR_NULLPOINTER, "pathname=%p, dest=%p", (void *)pathname, (void *)dest); + FAIL_ZERO_RETURN(e, dest, AKERR_NULLPOINTER, "pathname=%p, dest=%p", (void *)pathname, (void *)dest); + errno = 0; + FAIL_NONZERO_RETURN(e, stat(pathname, dest), AKSL_ERRNO_OR(AKERR_IO), "pathname=%s", pathname); + SUCCEED_RETURN(e); +} +akerr_ErrorContext AKERR_NOIGNORE *aksl_lstat(const char *pathname, struct stat *dest) +{ + PREPARE_ERROR(e); + FAIL_ZERO_RETURN(e, pathname, AKERR_NULLPOINTER, "pathname=%p, dest=%p", (void *)pathname, (void *)dest); + FAIL_ZERO_RETURN(e, dest, AKERR_NULLPOINTER, "pathname=%p, dest=%p", (void *)pathname, (void *)dest); + errno = 0; + FAIL_NONZERO_RETURN(e, lstat(pathname, dest), AKSL_ERRNO_OR(AKERR_IO), "pathname=%s", pathname); + SUCCEED_RETURN(e); +} +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); +} +akerr_ErrorContext AKERR_NOIGNORE *aksl_fstatat(int dirfd, const char *pathname, struct stat *dest, int flags) +{ + PREPARE_ERROR(e); + FAIL_ZERO_RETURN(e, pathname, AKERR_NULLPOINTER, "dirfd=%d, pathname=%p, dest=%p", dirfd, (void *)pathname, (void *)dest); + FAIL_ZERO_RETURN(e, dest, AKERR_NULLPOINTER, "dirfd=%d, pathname=%p, dest=%p", dirfd, (void *)pathname, (void *)dest); + errno = 0; + 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); +} +akerr_ErrorContext AKERR_NOIGNORE *aksl_statvfs(const char *path, struct statvfs *dest) +{ + PREPARE_ERROR(e); + FAIL_ZERO_RETURN(e, path, AKERR_NULLPOINTER, "path=%p, dest=%p", (void *)path, (void *)dest); + FAIL_ZERO_RETURN(e, dest, AKERR_NULLPOINTER, "path=%p, dest=%p", (void *)path, (void *)dest); + errno = 0; + FAIL_NONZERO_RETURN(e, statvfs(path, dest), AKSL_ERRNO_OR(AKERR_IO), "path=%s", path); + SUCCEED_RETURN(e); +} +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); + errno = 0; FAIL_NONZERO_RETURN(e, fstatvfs(fd, dest), AKSL_ERRNO_OR(AKERR_IO), "fd=%d", fd); SUCCEED_RETURN(e); +} diff --git a/tests/test_stat.c b/tests/test_stat.c new file mode 100644 index 0000000..9c6d254 --- /dev/null +++ b/tests/test_stat.c @@ -0,0 +1,86 @@ +/* File metadata wrapper tests. */ +#include "aksl_capture.h" +#include +#include + +static int test_stat_success_and_fstat(void) +{ + char path[AKSL_TMP_MAX]; + int fd = -1; + struct stat path_dest; + struct stat fd_dest; + struct statvfs vfs_dest; + AKSL_CHECK(aksl_temp_file(path, sizeof(path)) == 0); + fd = open(path, O_RDWR); + AKSL_CHECK(fd >= 0); + AKSL_CHECK(write(fd, "stat", 4) == 4); + AKSL_CHECK_OK(aksl_stat(path, &path_dest)); + AKSL_CHECK(S_ISREG(path_dest.st_mode)); + AKSL_CHECK(path_dest.st_size == 4); + AKSL_CHECK_OK(aksl_fstat(fd, &fd_dest)); + AKSL_CHECK(fd_dest.st_ino == path_dest.st_ino); + AKSL_CHECK_OK(aksl_fstatvfs(fd, &vfs_dest)); + AKSL_CHECK(vfs_dest.f_frsize != 0); + AKSL_CHECK(close(fd) == 0); + AKSL_CHECK_STATUS(aksl_fstat(fd, &fd_dest), EBADF); + AKSL_CHECK(unlink(path) == 0); + return 0; +} + +static int test_stat_paths_and_fstatat(void) +{ + char path[AKSL_TMP_MAX]; + char child[AKSL_TMP_MAX * 2]; + char linkpath[AKSL_TMP_MAX * 2]; + struct stat dest; + struct stat ldest; + struct statvfs vdest; + AKSL_CHECK_STATUS(aksl_stat("/nonexistent/aksl/stat", &dest), ENOENT); + 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); + 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)); + AKSL_CHECK_OK(aksl_fstatat(AT_FDCWD, path, &dest, 0)); + errno = E2BIG; + AKSL_CHECK_STATUS(aksl_fstatat(AT_FDCWD, path, &dest, 0x40000000), EINVAL); + AKSL_CHECK(errno == EINVAL); + AKSL_CHECK_OK(aksl_statvfs(".", &vdest)); + AKSL_CHECK(vdest.f_frsize != 0); + AKSL_CHECK(unlink(linkpath) == 0); + AKSL_CHECK(unlink(path) == 0); + return 0; +} + +static int test_stat_null_arguments(void) +{ + char path[AKSL_TMP_MAX]; + struct stat dest; + struct statvfs vdest; + AKSL_CHECK(aksl_temp_file(path, sizeof(path)) == 0); + 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); + AKSL_CHECK_STATUS(aksl_lstat(path, NULL), AKERR_NULLPOINTER); + AKSL_CHECK_STATUS(aksl_fstat(0, NULL), AKERR_NULLPOINTER); + AKSL_CHECK_STATUS(aksl_fstatat(AT_FDCWD, NULL, &dest, 0), AKERR_NULLPOINTER); + AKSL_CHECK_STATUS(aksl_fstatat(AT_FDCWD, path, NULL, 0), AKERR_NULLPOINTER); + AKSL_CHECK_STATUS(aksl_statvfs(NULL, &vdest), AKERR_NULLPOINTER); + AKSL_CHECK_STATUS(aksl_statvfs(path, NULL), AKERR_NULLPOINTER); + AKSL_CHECK_STATUS(aksl_fstatvfs(0, NULL), AKERR_NULLPOINTER); + AKSL_CHECK(unlink(path) == 0); + return 0; +} + +int main(void) +{ + int failures = 0; + AKSL_RUN(failures, test_stat_success_and_fstat); + AKSL_RUN(failures, test_stat_paths_and_fstatat); + AKSL_RUN(failures, test_stat_null_arguments); + AKSL_REPORT(failures); +} From 01034fc66888d09e7701f79aba972f8c7dce4b4f Mon Sep 17 00:00:00 2001 From: Logikoma Date: Mon, 3 Aug 2026 09:02:19 -0400 Subject: [PATCH 2/3] 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); From 15e9104d9e98c23a0867f65e67c678e2a842f5d7 Mon Sep 17 00:00:00 2001 From: Logikoma Date: Mon, 3 Aug 2026 10:43:47 -0400 Subject: [PATCH 3/3] Test file metadata failure paths Co-authored-by: Andrew Kesterson --- include/akstdlib.h | 32 ++++++++++++++++++++++++++++++++ src/stat.c | 32 ++++++++++++++++++++++++++------ tests/test_stat.c | 37 +++++++++++++++++++++++++++++++++---- 3 files changed, 91 insertions(+), 10 deletions(-) diff --git a/include/akstdlib.h b/include/akstdlib.h index 84fef89..aef0773 100644 --- a/include/akstdlib.h +++ b/include/akstdlib.h @@ -76,6 +76,7 @@ #include #include #include +#include #include #include /* off_t, for the aksl_fseeko/aksl_ftello pair. POSIX, like aksl_realpath. */ @@ -810,10 +811,25 @@ akerr_ErrorContext AKERR_NOIGNORE *aksl_strhash_djb2_str(const char *str, uint32 /** @} */ +/* ====================================================================== */ +/** @name File and filesystem metadata + * + * The stat family reports into caller-owned POSIX structs rather than a smaller + * library-defined copy: the platform owns the fields, and a wrapper should not + * discard a field merely because this library does not currently use it. libc + * failures retain their errno value as the status, so callers can distinguish + * absent paths from inaccessible ones without parsing an error message. + * @{ + */ +/* ====================================================================== */ + /** * @brief stat(2). * @param[in] pathname Path to inspect. Required. * @param[out] dest File metadata. Required. + * @throws AKERR_NULLPOINTER If pathname or dest is NULL. + * @throws AKERR_IO If stat(2) failed and left errno at 0. + * @throws (errno) The errno stat(2) set, reported directly as the status. * @return NULL on success, an error context otherwise. */ akerr_ErrorContext AKERR_NOIGNORE *aksl_stat(const char *pathname, struct stat *dest); @@ -822,6 +838,9 @@ akerr_ErrorContext AKERR_NOIGNORE *aksl_stat(const char *pathname, struct stat * * @brief lstat(2), inspecting a symbolic link itself. * @param[in] pathname Path to inspect. Required. * @param[out] dest File metadata. Required. + * @throws AKERR_NULLPOINTER If pathname or dest is NULL. + * @throws AKERR_IO If lstat(2) failed and left errno at 0. + * @throws (errno) The errno lstat(2) set, reported directly as the status. * @return NULL on success, an error context otherwise. */ akerr_ErrorContext AKERR_NOIGNORE *aksl_lstat(const char *pathname, struct stat *dest); @@ -830,6 +849,9 @@ akerr_ErrorContext AKERR_NOIGNORE *aksl_lstat(const char *pathname, struct stat * @brief fstat(2). * @param[in] fd Open file descriptor. * @param[out] dest File metadata. Required. + * @throws AKERR_NULLPOINTER If dest is NULL. + * @throws AKERR_IO If fstat(2) failed and left errno at 0. + * @throws (errno) The errno fstat(2) set, reported directly as the status. * @return NULL on success, an error context otherwise. */ akerr_ErrorContext AKERR_NOIGNORE *aksl_fstat(int fd, struct stat *dest); @@ -840,6 +862,9 @@ akerr_ErrorContext AKERR_NOIGNORE *aksl_fstat(int fd, struct stat *dest); * @param[in] pathname Path to inspect. Required. * @param[out] dest File metadata. Required. * @param[in] flags libc fstatat flags, passed unchanged. + * @throws AKERR_NULLPOINTER If pathname or dest is NULL. + * @throws AKERR_IO If fstatat(2) failed and left errno at 0. + * @throws (errno) The errno fstatat(2) set, reported directly as the status. * @return NULL on success, an error context otherwise. */ akerr_ErrorContext AKERR_NOIGNORE *aksl_fstatat(int dirfd, const char *pathname, struct stat *dest, int flags); @@ -848,6 +873,9 @@ akerr_ErrorContext AKERR_NOIGNORE *aksl_fstatat(int dirfd, const char *pathname, * @brief statvfs(3). * @param[in] path Path on the filesystem. Required. * @param[out] dest Filesystem metadata. Required. + * @throws AKERR_NULLPOINTER If path or dest is NULL. + * @throws AKERR_IO If statvfs(3) failed and left errno at 0. + * @throws (errno) The errno statvfs(3) set, reported directly as the status. * @return NULL on success, an error context otherwise. */ akerr_ErrorContext AKERR_NOIGNORE *aksl_statvfs(const char *path, struct statvfs *dest); @@ -856,11 +884,15 @@ akerr_ErrorContext AKERR_NOIGNORE *aksl_statvfs(const char *path, struct statvfs * @brief fstatvfs(3). * @param[in] fd Open file descriptor. * @param[out] dest Filesystem metadata. Required. + * @throws AKERR_NULLPOINTER If dest is NULL. + * @throws AKERR_IO If fstatvfs(3) failed and left errno at 0. + * @throws (errno) The errno fstatvfs(3) set, reported directly as the status. * @return NULL on success, an error context otherwise. */ akerr_ErrorContext AKERR_NOIGNORE *aksl_fstatvfs(int fd, struct statvfs *dest); +/** @} */ /* ====================================================================== */ /** @name Streams: open, read, write, close * diff --git a/src/stat.c b/src/stat.c index 29e799c..82f3b6f 100644 --- a/src/stat.c +++ b/src/stat.c @@ -1,6 +1,20 @@ -/* sys/stat.h and sys/statvfs.h metadata wrappers. */ +/* + * sys/stat.h and sys/statvfs.h metadata wrappers. + * + * These calls make information about a file or filesystem available only by a + * return value that must be checked alongside a caller-owned POSIX struct. The + * wrappers put failure in the return value, where AKERR_NOIGNORE prevents it + * from being silently dropped, while preserving the errno that distinguishes + * missing paths, inaccessible paths, and invalid descriptors. errno is cleared + * immediately before each libc call, so a broken libc that reports failure + * without setting errno is still an AKERR_IO failure rather than status 0. + */ #include + #include +#include +#include + #include "aksl_internal.h" /* @@ -41,8 +55,11 @@ akerr_ErrorContext AKERR_NOIGNORE *aksl_lstat(const char *pathname, struct stat */ 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); + 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); } /* @@ -57,7 +74,7 @@ akerr_ErrorContext AKERR_NOIGNORE *aksl_fstatat(int dirfd, const char *pathname, FAIL_ZERO_RETURN(e, pathname, AKERR_NULLPOINTER, "dirfd=%d, pathname=%p, dest=%p", dirfd, (void *)pathname, (void *)dest); FAIL_ZERO_RETURN(e, dest, AKERR_NULLPOINTER, "dirfd=%d, pathname=%p, dest=%p", dirfd, (void *)pathname, (void *)dest); errno = 0; - FAIL_NONZERO_RETURN(e, fstatat(dirfd, pathname, dest, flags), AKSL_ERRNO_OR(AKERR_IO), "dirfd=%d pathname=%s flags=%d", dirfd, pathname, flags); + FAIL_NONZERO_RETURN(e, fstatat(dirfd, pathname, dest, flags), AKSL_ERRNO_OR(AKERR_IO), "dirfd=%d, pathname=%s, flags=0x%x", dirfd, pathname, flags); SUCCEED_RETURN(e); } @@ -83,6 +100,9 @@ akerr_ErrorContext AKERR_NOIGNORE *aksl_statvfs(const char *path, struct statvfs */ 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); - errno = 0; FAIL_NONZERO_RETURN(e, fstatvfs(fd, dest), AKSL_ERRNO_OR(AKERR_IO), "fd=%d", fd); SUCCEED_RETURN(e); + PREPARE_ERROR(e); + FAIL_ZERO_RETURN(e, dest, AKERR_NULLPOINTER, "fd=%d, dest=%p", fd, (void *)dest); + errno = 0; + FAIL_NONZERO_RETURN(e, fstatvfs(fd, dest), AKSL_ERRNO_OR(AKERR_IO), "fd=%d", fd); + SUCCEED_RETURN(e); } diff --git a/tests/test_stat.c b/tests/test_stat.c index 14d4244..b7d7950 100644 --- a/tests/test_stat.c +++ b/tests/test_stat.c @@ -2,6 +2,14 @@ #include "aksl_capture.h" #include #include +#include + +/* + * A bit libc has never assigned in the AT_ space. fstatat(2) must reject it + * with EINVAL rather than ignoring it, which is what proves flags reach libc + * unmasked. Re-check this constant if AT_ ever grows into 0x40000000. + */ +#define AKSL_TEST_AT_INVALID 0x40000000 static int test_stat_success_and_fstat(void) { @@ -30,6 +38,7 @@ static int test_stat_success_and_fstat(void) /* A descriptor that was just closed must surface libc's EBADF. */ AKSL_CHECK_STATUS(aksl_fstat(fd, &fd_dest), EBADF); + AKSL_CHECK_STATUS(aksl_fstatvfs(fd, &vfs_dest), EBADF); AKSL_CHECK(unlink(path) == 0); return 0; } @@ -38,15 +47,21 @@ static int test_stat_paths_and_fstatat(void) { char path[AKSL_TMP_MAX]; char child[AKSL_TMP_MAX * 2]; + char directory[AKSL_TMP_MAX]; char linkpath[AKSL_TMP_MAX * 2]; + char *basename = NULL; + int dirfd = -1; struct stat dest; struct stat ldest; + struct stat path_dest; 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); + AKSL_CHECK_STATUS(aksl_lstat("/nonexistent/aksl/stat", &ldest), 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_OK(aksl_stat(path, &path_dest)); 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); @@ -57,13 +72,27 @@ static int test_stat_paths_and_fstatat(void) 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)); + /* A real dirfd must resolve a relative name against that directory. */ + AKSL_CHECK(snprintf(directory, sizeof(directory), "%s", path) < (int)sizeof(directory)); + basename = strrchr(directory, '/'); + AKSL_CHECK(basename != NULL); + *basename = '\0'; + dirfd = open(directory, O_RDONLY | O_DIRECTORY); + AKSL_CHECK(dirfd >= 0); + AKSL_CHECK_OK(aksl_fstatat(dirfd, strrchr(path, '/') + 1, &dest, 0)); + AKSL_CHECK(dest.st_ino == path_dest.st_ino); + AKSL_CHECK(close(dirfd) == 0); + + /* A valid non-zero flag must reach libc, making this call an lstat. */ + AKSL_CHECK_OK(aksl_fstatat(AT_FDCWD, linkpath, &dest, AT_SYMLINK_NOFOLLOW)); + AKSL_CHECK(S_ISLNK(dest.st_mode)); /* 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); + AKSL_CHECK_STATUS(aksl_fstatat(AT_FDCWD, path, &dest, AKSL_TEST_AT_INVALID), EINVAL); + + /* statvfs reports ENOENT before it can describe a missing filesystem path. */ + AKSL_CHECK_STATUS(aksl_statvfs("/nonexistent/aksl/stat", &vdest), ENOENT); /* statvfs reports the filesystem containing the current directory. */ AKSL_CHECK_OK(aksl_statvfs(".", &vdest));