From 9eb32c40105e7998ac437982f1f2c49000c3adf4 Mon Sep 17 00:00:00 2001 From: huntedbytheirs Date: Sat, 5 Sep 2026 23:32:29 -0400 Subject: [PATCH] feat(unistd): cwd/link/readlink/path ops --- include/libgen.h | 58 ++++++ include/unistd.h | 210 +++++++++++++++++++- src/unistd/basename.c | 63 ++++++ src/unistd/chdir.c | 26 +++ src/unistd/chroot.c | 27 +++ src/unistd/confstr.c | 82 ++++++++ src/unistd/dirname.c | 62 ++++++ src/unistd/getcwd.c | 66 +++++++ src/unistd/getlogin.c | 149 ++++++++++++++ src/unistd/getopt.c | 207 +++++++++++++++++++ src/unistd/getwd.c | 33 ++++ src/unistd/isatty.c | 30 +++ src/unistd/link.c | 29 +++ src/unistd/readlink.c | 28 +++ src/unistd/realpath.c | 273 +++++++++++++++++++++++++ src/unistd/renameat.c | 21 ++ src/unistd/rmdir.c | 18 ++ src/unistd/swab.c | 37 ++++ src/unistd/symlink.c | 26 +++ src/unistd/ttyname.c | 115 +++++++++++ src/unistd/unlink.c | 27 +++ tests/test_unistd_misc.c | 415 +++++++++++++++++++++++++++++++++++++++ 22 files changed, 2001 insertions(+), 1 deletion(-) create mode 100644 include/libgen.h create mode 100644 src/unistd/basename.c create mode 100644 src/unistd/chdir.c create mode 100644 src/unistd/chroot.c create mode 100644 src/unistd/confstr.c create mode 100644 src/unistd/dirname.c create mode 100644 src/unistd/getcwd.c create mode 100644 src/unistd/getlogin.c create mode 100644 src/unistd/getopt.c create mode 100644 src/unistd/getwd.c create mode 100644 src/unistd/isatty.c create mode 100644 src/unistd/link.c create mode 100644 src/unistd/readlink.c create mode 100644 src/unistd/realpath.c create mode 100644 src/unistd/renameat.c create mode 100644 src/unistd/rmdir.c create mode 100644 src/unistd/swab.c create mode 100644 src/unistd/symlink.c create mode 100644 src/unistd/ttyname.c create mode 100644 src/unistd/unlink.c create mode 100644 tests/test_unistd_misc.c diff --git a/include/libgen.h b/include/libgen.h new file mode 100644 index 0000000..8b5d0c5 --- /dev/null +++ b/include/libgen.h @@ -0,0 +1,58 @@ +#ifndef VLIBC_LIBGEN_H +#define VLIBC_LIBGEN_H + +/* + * vlibc — . + * + * basename() and dirname() split a pathname into its final component and + * the directory that contains it (XSI, declared when _XOPEN_SOURCE is + * enabled in glibc — level 2 here). They are the XSI forms: the input + * path is modified in place (callers must pass a writable buffer), and + * both are allowed to return pointers into a shared static area, so + * repeated calls overwrite earlier results. + * + * The two functions are deliberately declared here and nowhere else: + * POSIX places them in only. + * + * Neither declaration carries an intent attribute: both functions write + * to and return pointers into their argument or a shared static buffer, + * so const/pure would be unsound. + */ + +#include + +#ifdef __cplusplus +extern "C" { +#endif + +#if VLIBC_LEVEL_GE(2) + +/* + * Return a pointer to the final component of path, modifying path in + * place: trailing '/' characters (other than a leading root '/') are + * overwritten with NUL so basename("/usr/lib") == "lib", basename("/") + * == "/" and basename("") == ".". The result may be a suffix of path or + * point into a shared static buffer (POSIX permits either); the special + * cases "/" and "" return static strings. + */ +char * +basename(char *path); + +/* + * Return a pointer to the directory portion of path, modifying path in + * place: the last '/' that is not the final character is overwritten + * with NUL so dirname("/usr/lib") == "/usr", dirname("/") == "/" and + * dirname("") == ".". The result may be a suffix of path or point into + * a shared static buffer (POSIX permits either); the special cases + * return static strings. + */ +char * +dirname(char *path); + +#endif /* VLIBC_LEVEL_GE(2) */ + +#ifdef __cplusplus +} +#endif + +#endif /* VLIBC_LIBGEN_H */ diff --git a/include/unistd.h b/include/unistd.h index abb70ec..cad05b8 100644 --- a/include/unistd.h +++ b/include/unistd.h @@ -31,7 +31,21 @@ * configuration keys. * Level 2 (muslmimic): gethostname, sethostname, gethostid (XSI/BSD * system identification — NOT POSIX.1-2008 base), - * and the _CS_* keys for todo 38's confstr(). + * and the _CS_* keys for confstr(). + * + * The current-working-directory, link/symlink/readlink, path-removal and + * *at operations, the terminal-identity and login functions, getopt and + * confstr (todo 38) come after the system-limits block: + * + * Level 1 (onlyposix): getcwd, chdir, fchdir, readlink, symlink, link, + * unlink, rmdir, linkat, readlinkat, renameat, + * symlinkat, unlinkat, isatty, ttyname, ttyname_r, + * getlogin, getlogin_r, getopt plus the optarg/ + * optind/opterr/optopt globals, and confstr (its + * _CS_* key constants are level 2, above). + * Level 2 (muslmimic): getwd (obsolescent), chroot, swab (XSI). + * basename/dirname live in and realpath + * in (their POSIX homes), also XSI. * * uname() and struct utsname live in (todo 32). * @@ -474,9 +488,203 @@ pathconf(const char *path, int name); long fpathconf(int fildes, int name); +/* Level 1 (POSIX base): current directory, links, and path operations. */ + +/* + * Store the absolute pathname of the current working directory into buf. + * When buf is NULL, the path is returned in a malloc'd buffer instead + * (size is then only a hint: 0 grows from a default of 128 bytes, and + * the buffer is enlarged as the kernel reports ERANGE); the caller must + * free it. Return buf (or the malloc'd buffer), or NULL with errno set + * (EINVAL for a non-NULL buf with size 0). + */ +char * +getcwd(char *buf, size_t size); + +/* + * Change the current working directory to path (chdir) or to the + * directory referred to by the open descriptor fildes (fchdir). Return + * 0, or -1 with errno set. + */ +int +chdir(const char *path); + +int +fchdir(int fildes); + +/* + * Create a symbolic link: path2 is created as a link whose contents are + * the string path1 (which need not exist). Return 0, or -1 with errno + * set. + */ +int +symlink(const char *path1, const char *path2); + +/* + * Like symlink(), but the link is created in the directory named by fd + * when path2 is relative (use AT_FDCWD for the current working + * directory). Return 0, or -1 with errno set. + */ +int +symlinkat(const char *path1, int fd, const char *path2); + +/* + * Read the contents of the symbolic link path into buf (bufsize bytes, + * no NUL is appended) and return the number of bytes placed in buf, 0 + * for an empty target, or -1 with errno set. The buffer is not + * terminated: callers sized for bufsize + 1 append the NUL themselves. + */ +ssize_t +readlink(const char *restrict path, char *restrict buf, size_t bufsize); + +/* + * Like readlink(), but path is resolved in the directory named by fd + * when relative. Return the number of bytes placed in buf, or -1 with + * errno set. + */ +ssize_t +readlinkat(int fd, const char *restrict path, char *restrict buf, size_t bufsize); + +/* + * Create a hard link: new is created as a second name for the file old + * (which must not be a directory and must be on the same filesystem). + * Return 0, or -1 with errno set. + */ +int +link(const char *old, const char *new); + +/* + * Like link(), but old and new are resolved against the directories + * named by fd1 and fd2 when relative (AT_FDCWD selects the current + * working directory) and flag may hold AT_SYMLINK_FOLLOW from . + * Return 0, or -1 with errno set. + */ +int +linkat(int fd1, const char *old, int fd2, const char *new, int flag); + +/* + * Remove the name path (a hard link; symbolic links are removed, not + * followed). Return 0, or -1 with errno set. + */ +int +unlink(const char *path); + +/* + * Like unlink(), but path is resolved in the directory named by fd when + * relative and flag may hold AT_REMOVEDIR from (making it + * remove an empty directory instead). Return 0, or -1 with errno set. + */ +int +unlinkat(int fd, const char *path, int flag); + +/* + * Remove the empty directory path. Return 0, or -1 with errno set + * (ENOTEMPTY/EEXIST when the directory is not empty). + */ +int +rmdir(const char *path); + +/* + * Rename old to new within the filesystem: old is resolved against fd1 + * and new against fd2 (AT_FDCWD for the current working directory). Both + * names may be directories. Return 0, or -1 with errno set. + */ +int +renameat(int fd1, const char *old, int fd2, const char *new); + +/* Level 1 (POSIX base): terminal identity, login name, getopt, confstr. */ + +/* + * Test whether fildes refers to a terminal. Return 1 when it does, and + * 0 otherwise (errno may be set — e.g. EBADF — but the caller must not + * rely on it after a 0 return). + */ +int +isatty(int fildes); + +/* + * Return the pathname of the terminal open on fildes in a shared static + * buffer (overwritten by the next call), or NULL with errno set when + * fildes is not a terminal. ttyname_r stores the path in the caller's + * name buffer instead and returns 0, or an error NUMBER (ENOTTY when + * fildes is not a terminal, ERANGE when the name does not fit in + * namesize). + */ +char * +ttyname(int fildes); + +int +ttyname_r(int fildes, char *name, size_t namesize); + +/* + * Return the login name of the user associated with the calling process + * (the owner of its controlling terminal) in a shared static buffer, or + * NULL with errno set when the name cannot be determined. getlogin_r + * stores the name in the caller's buffer and returns 0, or an error + * number (ERANGE when the name does not fit in namesize). + */ +char * +getlogin(void); + +int +getlogin_r(char *name, size_t namesize); + +/* + * Command-line option parsing (POSIX.1-2008). getopt returns the next + * option character from argv per the option characters in optstring; a + * character followed by ':' in optstring takes an argument (delivered in + * optarg). A leading ':' in optstring makes missing-argument errors + * return ':' silently instead of '?' with a diagnostic to stderr. + * Return -1 when all options have been consumed: optind then points at + * the first non-option argument, and "--" terminates the scan. + */ +int +getopt(int argc, char *const argv[], const char *optstring); + +/* The getopt option state (POSIX.1-2008 base). */ +extern char *optarg; /* the argument of the option just returned */ +extern int optind; /* index of the next argument to scan (init 1) */ +extern int opterr; /* nonzero: write diagnostics to stderr (init 1) */ +extern int optopt; /* the option character that caused an error */ + +/* + * Query the implementation-defined string value of configuration name + * (one of the _CS_* keys above). When buf is not NULL and len is large + * enough, the value (NUL-terminated) is copied into buf; the return + * value is the length of the value including the terminating NUL, so a + * return greater than len means the buffer was too small and nothing was + * copied. For an unknown name, 0 is returned and errno is set to EINVAL. + */ +size_t +confstr(int name, char *buf, size_t len); + #if VLIBC_LEVEL_GE(2) /* Level 2 (muslmimic): Linux extensions + XSI + obsolescent. */ +/* + * Store the current working directory into pathname, which must hold at + * least PATH_MAX bytes. Return pathname, or NULL with errno set. + * Obsolescent: prefer getcwd(). + */ +char * +getwd(char *pathname); + +/* + * Change the process root directory to path (used with chdir by + * chroot-style confinement). Requires privilege. Return 0, or -1 with + * errno set. XSI. + */ +int +chroot(const char *path); + +/* + * Copy nbytes bytes from from to to, swapping each adjacent pair of + * bytes (the odd byte of an odd count is not copied). Used for historic + * byte-order conversion; returns nothing. XSI. + */ +void +swab(const void *restrict from, void *restrict to, ssize_t nbytes); + /* * Like dup2(), but with descriptor flags (O_CLOEXEC from ) applied * atomically; return fildes2, or -1 with errno set. Linux-specific. diff --git a/src/unistd/basename.c b/src/unistd/basename.c new file mode 100644 index 0000000..6ec67fc --- /dev/null +++ b/src/unistd/basename.c @@ -0,0 +1,63 @@ +#ifdef HAVE_CONFIG_H +#include +#endif + +#include + +#include + +/* + * vlibc — basename (todo 38), XSI form. + * + * Return the final component of path, modifying path in place: every + * trailing '/' (except one that is the entire name) is overwritten with + * NUL and the result is the text after the last remaining '/'. The two + * degenerate names return static strings — "/" (and "//") yields "/" and + * the empty string yields "." — everything else is a suffix of the + * (modified) argument. Callers must pass a writable buffer and must not + * hold the result across another libgen call, exactly as + * documents. + * + * Whole file gated at level 2: basename/dirname are XSI, and the header + * deliberately declares them nowhere else (POSIX places them in + * only, unlike the GNU string.h variants this libc does not + * provide). + */ + +#if VLIBC_LEVEL_GE(2) + +char * +basename(char *path) +{ + static const char dot[2] = "."; + static const char slash[2] = "/"; + char *end; + char *p; + + if (path == NULL || *path == '\0') + { + return (char *)dot; + } + end = path + strlen(path) - 1; + while (end > path && *end == '/') + { + *end = '\0'; /* strip trailing slashes (never the leading one) */ + end--; + } + if (*path == '\0') + { + return (char *)slash; /* the name was nothing but slashes */ + } + p = strrchr(path, '/'); + if (p == NULL) + { + return path; /* no slash: the whole name is the final component */ + } + if (p[1] == '\0') + { + return (char *)slash; /* p is the lone leading root slash */ + } + return p + 1; +} + +#endif /* VLIBC_LEVEL_GE(2) */ diff --git a/src/unistd/chdir.c b/src/unistd/chdir.c new file mode 100644 index 0000000..980da9e --- /dev/null +++ b/src/unistd/chdir.c @@ -0,0 +1,26 @@ +#ifdef HAVE_CONFIG_H +#include +#endif + +#include + +#include "../internal/syscall.h" + +/* + * chdir/fchdir: change the current working directory. chdir resolves + * path through the filesystem; fchdir switches to the directory the open + * descriptor fildes refers to (it need not have been opened with + * O_DIRECTORY — any descriptor of a directory works). Both report 0 on + * success and -1 with errno set through syscall_ret. + */ +int +chdir(const char *path) +{ + return syscall_ret(__syscall1(SYS_chdir, (long)path)); +} + +int +fchdir(int fildes) +{ + return syscall_ret(__syscall1(SYS_fchdir, fildes)); +} diff --git a/src/unistd/chroot.c b/src/unistd/chroot.c new file mode 100644 index 0000000..6d517d3 --- /dev/null +++ b/src/unistd/chroot.c @@ -0,0 +1,27 @@ +#ifdef HAVE_CONFIG_H +#include +#endif + +#include + +#include "../internal/syscall.h" + +/* + * vlibc — chroot (todo 38). + * + * Change the process' notion of its filesystem root to path, which must + * be a directory. Requires privilege (CAP_SYS_CHROOT); an unprivileged + * call fails with EPERM. The directory becomes the "/" of a confined + * process that can no longer reach anything above it — the classic + * sandbox primitive, XSI. Return 0, or -1 with errno set. + */ + +#if VLIBC_LEVEL_GE(2) + +int +chroot(const char *path) +{ + return syscall_ret(__syscall1(SYS_chroot, (long)path)); +} + +#endif /* VLIBC_LEVEL_GE(2) */ diff --git a/src/unistd/confstr.c b/src/unistd/confstr.c new file mode 100644 index 0000000..d2aabfe --- /dev/null +++ b/src/unistd/confstr.c @@ -0,0 +1,82 @@ +#ifdef HAVE_CONFIG_H +#include +#endif + +#include +#include + +#include + +/* + * vlibc — confstr (todo 38). + * + * Query an implementation-defined string configuration value. The whole + * file is gated at level 2 because every _CS_* key it serves lives in the + * level-2 block of (confstr itself is POSIX base, but it is + * useless below the first key, so the TU is wired to the L2 source list). + * + * Values, per the key table in : + * + * _CS_PATH: a usable default PATH. "/bin:/usr/bin" is the classic + * choice (musl and glibc both report it) and always finds + * the standard utilities on the filesystems this libc runs + * on, so programs get a sane environment when none is set. + * _CS_POSIX_V6/V7_WIDTH_RESTRICTED_ENVS: "1" — the width-restricted + * environment (which reserves argv/envp names for utilities + * that manipulate file descriptors) is provided. + * _CS_LFS_*: "" — 64-bit file offsets are the native ABI here, so no + * compile/link/lint flags are needed (glibc reports the same + * on 64-bit targets). + * _CS_GNU_LIBC_VERSION / _CS_GNU_LIBPTHREAD_VERSION: "" — vlibc is not + * glibc and carries no glibc-version compatibility claim; + * the keys exist so version-probing code gets a clean "not + * glibc" answer instead of garbage. + * + * Return semantics are POSIX: the length of the value including its + * terminating NUL; when buf is large enough the value is copied in. + * An unknown name returns 0 with errno set to EINVAL. + */ + +#if VLIBC_LEVEL_GE(2) + +size_t +confstr(int name, char *buf, size_t len) +{ + const char *value; + size_t need; + + switch (name) + { + case _CS_PATH: + value = "/bin:/usr/bin"; + break; + case _CS_POSIX_V6_WIDTH_RESTRICTED_ENVS: + case _CS_POSIX_V7_WIDTH_RESTRICTED_ENVS: + value = "1"; + break; + case _CS_LFS_CFLAGS: + case _CS_LFS_LDFLAGS: + case _CS_LFS_LIBS: + case _CS_LFS_LINTFLAGS: + case _CS_LFS64_CFLAGS: + case _CS_LFS64_LDFLAGS: + case _CS_LFS64_LIBS: + case _CS_LFS64_LINTFLAGS: + case _CS_GNU_LIBC_VERSION: + case _CS_GNU_LIBPTHREAD_VERSION: + value = ""; + break; + default: + errno = EINVAL; + return 0; + } + + need = strlen(value) + 1; + if (buf != NULL && len >= need) + { + strcpy(buf, value); + } + return need; +} + +#endif /* VLIBC_LEVEL_GE(2) */ diff --git a/src/unistd/dirname.c b/src/unistd/dirname.c new file mode 100644 index 0000000..b9fb4f9 --- /dev/null +++ b/src/unistd/dirname.c @@ -0,0 +1,62 @@ +#ifdef HAVE_CONFIG_H +#include +#endif + +#include + +#include + +/* + * vlibc — dirname (todo 38), XSI form. + * + * Return the directory portion of path, modifying path in place: the + * last '/' that is not the final character is overwritten with NUL, so + * the result is everything up to and including that '/'. A name with no + * '/' yields the static string "."; "/" (and "//") yields "/" and the + * empty string yields ".". Callers must pass a writable buffer and must + * not hold the result across another libgen call, exactly as + * documents. + * + * Whole file gated at level 2: see src/unistd/basename.c. + */ + +#if VLIBC_LEVEL_GE(2) + +char * +dirname(char *path) +{ + static const char dot[2] = "."; + char *end; + char *p; + + if (path == NULL || *path == '\0') + { + return (char *)dot; + } + end = path + strlen(path) - 1; + while (end > path && *end == '/') + { + *end = '\0'; /* strip trailing slashes (never the leading one) */ + end--; + } + if (*path == '\0') + { + /* The name was nothing but slashes: dirname("/") == "/". */ + path[1] = '\0'; + return path; + } + p = strrchr(path, '/'); + if (p == NULL) + { + return (char *)dot; /* a single component has no directory part */ + } + if (p == path) + { + p[1] = '\0'; /* "/name" -> "/" */ + return path; + } + *p = '\0'; /* "/dir/name" -> "/dir" */ + return path; +} + +#endif /* VLIBC_LEVEL_GE(2) */ diff --git a/src/unistd/getcwd.c b/src/unistd/getcwd.c new file mode 100644 index 0000000..0aa00f6 --- /dev/null +++ b/src/unistd/getcwd.c @@ -0,0 +1,66 @@ +#ifdef HAVE_CONFIG_H +#include +#endif + +#include + +#include +#include + +#include "../internal/malloc.h" +#include "../internal/syscall.h" + +/* + * getcwd: the absolute path of the current working directory via + * SYS_getcwd, which copies the path (NUL-terminated) into the caller's + * buffer and returns its length. + * + * Two calling forms: + * - buf != NULL: size must be nonzero (EINVAL otherwise); the kernel + * reports ERANGE when the buffer cannot hold the path. + * - buf == NULL: the path is returned in an allocation from the + * internal allocator (releasable with the public free). size is only + * a first-guess; the buffer doubles on each ERANGE until the path + * fits, starting from a default of 128 when size is 0. + * + * A successful SYS_getcwd result never needs post-processing: the kernel + * guarantees an absolute path with no "." or ".." components. + */ +char * +getcwd(char *buf, size_t size) +{ + if (buf != NULL) + { + if (size == 0) + { + errno = EINVAL; + return NULL; + } + if (syscall_ret(__syscall2(SYS_getcwd, (long)buf, (long)size)) < 0) + { + return NULL; + } + return buf; + } + + for (size_t cap = (size != 0) ? size : 128;;) + { + char *p = __libc_malloc(cap); + + if (p == NULL) + { + return NULL; /* errno ENOMEM from the allocator */ + } + if (syscall_ret(__syscall2(SYS_getcwd, (long)p, (long)cap)) >= 0) + { + return p; + } + if (errno != ERANGE || cap > (size_t)-1 / 2) + { + __libc_free(p); + return NULL; + } + __libc_free(p); + cap *= 2; + } +} diff --git a/src/unistd/getlogin.c b/src/unistd/getlogin.c new file mode 100644 index 0000000..5739034 --- /dev/null +++ b/src/unistd/getlogin.c @@ -0,0 +1,149 @@ +#ifdef HAVE_CONFIG_H +#include +#endif + +#include + +#include +#include + +/* + * getlogin/getlogin_r: the login name associated with the calling + * process — the user who owns its controlling terminal. + * + * POSIX.1-2008: getlogin() returns a pointer to the login name, or NULL + * when the process is not associated with a terminal; getlogin_r may + * fail with ENOTTY when the calling process has no controlling terminal. + * The "controlling terminal" is the terminal of the session the process + * belongs to; Linux reports it as field 7 (tty_nr) of /proc/self/stat + * (0 when the process has none). + * + * Resolving the NAME of that terminal's user requires the session/utmp + * database, which this libc does not yet carry (the passwd/group todo + * owns /etc/passwd and the session records). This stage therefore never + * fabricates a name: it distinguishes the no-terminal case (ENOTTY, + * fully specified by POSIX) from the has-a-terminal-but-no-database + * case (ENOENT), and getlogin() reports both as NULL with errno set. + * The name resolution is completed by the session-database todo. + */ + +/* Static buffer for getlogin(); 256 bytes comfortably holds a login name. */ +#define VLIBC_UNISTD_LOGIN_BUF 256 + +static char getlogin_buf[VLIBC_UNISTD_LOGIN_BUF]; + +/* + * Read field 7 (tty_nr) of /proc/self/stat. After the parenthesized + * comm field — located by scanning back from the end for the last ')' — + * the fields run state(3) ppid(4) pgrp(5) session(6) tty_nr(7), so + * tty_nr is the token after four more whitespace-separated tokens. + * Returns 1 and the value when the file could be read, 0 otherwise. + */ +static int +login_tty_nr(long *tty_out) +{ + char buf[512]; + char *p; + ssize_t n; + int fd; + int tok; + long value = 0; + int sign = 1; + int have_digit = 0; + + fd = open("/proc/self/stat", O_RDONLY); + if (fd < 0) + { + return 0; + } + n = read(fd, buf, sizeof(buf) - 1); + close(fd); + if (n <= 0) + { + return 0; + } + buf[n] = '\0'; + + p = buf + n; + while (p > buf && *p != ')') + { + p--; + } + if (*p != ')') + { + return 0; + } + p++; + + for (tok = 0; tok < 4; tok++) + { + while (*p == ' ' || *p == '\t') + { + p++; + } + while (*p != '\0' && *p != ' ' && *p != '\t' && *p != '\n') + { + p++; + } + if (*p == '\0') + { + return 0; + } + } + while (*p == ' ' || *p == '\t') + { + p++; + } + if (*p == '-') + { + sign = -1; + p++; + } + while (*p >= '0' && *p <= '9') + { + value = value * 10 + (*p - '0'); + p++; + have_digit = 1; + } + if (!have_digit) + { + return 0; + } + *tty_out = sign * value; + return 1; +} + +int +getlogin_r(char *name, size_t namesize) +{ + long tty_nr; + + if (name == NULL || namesize == 0) + { + errno = EINVAL; + return EINVAL; + } + if (!login_tty_nr(&tty_nr) || tty_nr == 0) + { + /* No controlling terminal (or procfs cannot be read). */ + errno = ENOTTY; + return ENOTTY; + } + /* A controlling terminal exists, but no session database to resolve + * its owner's login name yet — never fabricate one. */ + errno = ENOENT; + return ENOENT; +} + +char * +getlogin(void) +{ + int err = getlogin_r(getlogin_buf, sizeof(getlogin_buf)); + + if (err != 0) + { + errno = err; + return NULL; + } + return getlogin_buf; +} diff --git a/src/unistd/getopt.c b/src/unistd/getopt.c new file mode 100644 index 0000000..bfc4d28 --- /dev/null +++ b/src/unistd/getopt.c @@ -0,0 +1,207 @@ +#ifdef HAVE_CONFIG_H +#include +#endif + +#include + +#include + +#include "../internal/syscall.h" + +/* + * vlibc — getopt (todo 38). + * + * POSIX.1-2008 option scanning. getopt returns the next option character + * from argv per the letters of optstring; a letter followed by ':' in + * optstring takes an argument, delivered in optarg. Scanning rules: + * + * - "--" terminates the scan and is itself consumed (optind moves past + * it); a lone "-" or any argument not starting with '-' is a non- + * option argument that stops the scan with optind pointing at it. + * - Options and their arguments may be bundled ("-abc" is -a -b -c); + * an option that takes an argument consumes the rest of its element + * ("-oarg") or, when none remains, the whole next element ("-o arg"). + * - A leading ':' in optstring selects the silent error mode: a missing + * argument returns ':' instead of '?' and no diagnostic is written. + * Otherwise errors return '?' and, when opterr is nonzero, print a + * diagnostic to standard error naming argv[0]. optopt holds the + * character that caused the error. + * - The scan is not permuted: the first non-option argument ends it, + * exactly as POSIX permits. + * + * optind is the index of the next argument to examine, starting at 1. + * This TU owns the four public option globals, which declares + * extern; no other level gate applies — getopt is POSIX base. + * + * Diagnostics go to fd 2 through the raw syscall layer (the house idiom, + * see src/internal/assert_fail.c) so this TU needs no stdio. + */ + +/* The character to return next within the current element, or NULL when + * no element is being scanned (optind then names the next one). */ +static char *getopt_place; + +/* The argument of the option just returned (NULL when it takes none). */ +char *optarg; + +/* Index of the next argument to scan; starts at 1 (argv[0] is the + * program name). */ +int optind = 1; + +/* Nonzero: write a diagnostic to standard error on an error. */ +int opterr = 1; + +/* The option character that caused the most recent error. */ +int optopt; + +/* Append the NUL-terminated string s at p; return the next free slot. */ +static char * +getopt_str(char *p, const char *s, const char *end) +{ + while (*s != '\0' && p < end) + { + *p++ = *s++; + } + return p; +} + +/* + * Write a diagnostic of the standard shape to fd 2: "prog: invalid + * option -- c" or "prog: option requires an argument -- c", plus a + * newline. prog may be NULL (no name prefix then). + */ +static void +getopt_diag(const char *prog, int missing_arg, int c) +{ + char msg[160]; + char *p = msg; + const char *const end = msg + sizeof(msg) - 1; + + if (prog != NULL) + { + p = getopt_str(p, prog, end); + if (p < end - 1) + { + *p++ = ':'; + *p++ = ' '; + } + } + p = getopt_str(p, missing_arg ? "option requires an argument -- " : "invalid option -- ", end); + if (p < end) + { + *p++ = (char)c; + } + if (p < end) + { + *p++ = '\n'; + } + (void)__syscall3(SYS_write, 2, (long)msg, (long)(p - msg)); +} + +int +getopt(int argc, char *const argv[], const char *optstring) +{ + const char *oli; + char *place; + int c; + + if (optstring == NULL) + { + return -1; + } + optarg = NULL; + + /* Begin scanning the element at argv[optind] when the previous one + * (if any) has been fully consumed; the consuming sites below have + * already advanced optind past it. */ + if (getopt_place == NULL) + { + if (optind >= argc) + { + return -1; + } + place = argv[optind]; + if (place == NULL || place[0] != '-' || place[1] == '\0') + { + /* A non-option argument (or a lone "-") ends the scan with + * optind left on it. */ + return -1; + } + if (place[1] == '-' && place[2] == '\0') + { + optind++; /* "--" is consumed and ends the scan */ + return -1; + } + getopt_place = place + 1; /* scan the letters after the '-' */ + } + + place = getopt_place; + c = (unsigned char)*place++; + getopt_place = place; + optopt = c; + + /* Is c a listed option? (':' never is: it is reserved for the + * leading error flag.) */ + if (c == ':') + { + oli = NULL; + } + else + { + oli = strchr(optstring, c); + } + + if (oli == NULL) + { + /* Unrecognized option. */ + if (*place == '\0') + { + optind++; /* the element is fully consumed */ + getopt_place = NULL; + } + if (opterr != 0 && optstring[0] != ':') + { + getopt_diag(argv[0], 0, c); + } + return '?'; + } + + if (oli[1] != ':') + { + /* c takes no argument. */ + if (*place == '\0') + { + optind++; + getopt_place = NULL; + } + return c; + } + + /* c takes an argument. */ + if (*place != '\0') + { + optarg = place; /* the rest of this element is the argument */ + optind++; /* the element is fully consumed */ + getopt_place = NULL; + return c; + } + optind++; /* the option element is consumed; the argument is next */ + if (optind >= argc) + { + /* No argument available. */ + getopt_place = NULL; + if (optstring[0] == ':') + { + return ':'; + } + if (opterr != 0) + { + getopt_diag(argv[0], 1, c); + } + return '?'; + } + optarg = argv[optind]; + optind++; /* the argument element is consumed */ + getopt_place = NULL; + return c; +} diff --git a/src/unistd/getwd.c b/src/unistd/getwd.c new file mode 100644 index 0000000..c721398 --- /dev/null +++ b/src/unistd/getwd.c @@ -0,0 +1,33 @@ +#ifdef HAVE_CONFIG_H +#include +#endif + +#include + +/* + * vlibc — getwd (todo 38). + * + * Obsolescent [OB] form of getcwd: store the current working directory + * into the caller's pathname buffer and return it. XSI and legacy + * programs expect pathname to hold PATH_MAX bytes (this file uses 4096, + * the x86_64 _PC_PATH_MAX value; does not define PATH_MAX + * yet). getcwd performs the actual work and reports failures with NULL + * and errno set. + */ + +#if VLIBC_LEVEL_GE(2) + +/* The caller's buffer is conventionally PATH_MAX bytes. */ +#define VLIBC_GETWD_PATH_MAX 4096 + +char * +getwd(char *pathname) +{ + if (pathname == NULL) + { + return NULL; /* getcwd(NULL, 0) semantics would malloc: do not */ + } + return getcwd(pathname, VLIBC_GETWD_PATH_MAX); +} + +#endif /* VLIBC_LEVEL_GE(2) */ diff --git a/src/unistd/isatty.c b/src/unistd/isatty.c new file mode 100644 index 0000000..1f6173b --- /dev/null +++ b/src/unistd/isatty.c @@ -0,0 +1,30 @@ +#ifdef HAVE_CONFIG_H +#include +#endif + +#include + +#include "../internal/syscall.h" + +/* + * isatty: report whether fildes is associated with a terminal. + * + * The probe is the kernel's own: issue the TCGETS ioctl, which a real + * terminal answers with 0 and everything else rejects with -ENOTTY (a + * regular file, directory, pipe, socket, or a character device that is + * not a terminal such as /dev/null). A char-device fstat check is NOT + * sufficient — /dev/null is a character device but not a tty — so the + * ioctl is the correct test. + * + * The result is derived from the raw syscall return (negative -errno on + * failure), so isatty never writes errno: POSIX permits an errno set on + * a 0 return but does not require one, and skipping the write keeps the + * probe free of thread-local side effects. + */ +int +isatty(int fildes) +{ + struct termios t; + + return __syscall3(SYS_ioctl, fildes, 0x5401 /* TCGETS */, (long)&t) == 0 ? 1 : 0; +} diff --git a/src/unistd/link.c b/src/unistd/link.c new file mode 100644 index 0000000..958159e --- /dev/null +++ b/src/unistd/link.c @@ -0,0 +1,29 @@ +#ifdef HAVE_CONFIG_H +#include +#endif + +#include +#include + +#include "../internal/syscall.h" + +/* + * link/linkat: create a hard link — a second directory entry for the + * same inode. link(old, new) passes both names to the kernel; linkat + * resolves old against fd1 and new against fd2 when they are relative + * (AT_FDCWD for the current working directory) and honors flag + * (AT_SYMLINK_FOLLOW makes the kernel follow an old that is a symbolic + * link; the default 0 links the link itself). Return 0, or -1 with + * errno set. + */ +int +link(const char *old, const char *new) +{ + return syscall_ret(__syscall2(SYS_link, (long)old, (long)new)); +} + +int +linkat(int fd1, const char *old, int fd2, const char *new, int flag) +{ + return syscall_ret(__syscall5(SYS_linkat, fd1, (long)old, fd2, (long)new, flag)); +} diff --git a/src/unistd/readlink.c b/src/unistd/readlink.c new file mode 100644 index 0000000..959d3df --- /dev/null +++ b/src/unistd/readlink.c @@ -0,0 +1,28 @@ +#ifdef HAVE_CONFIG_H +#include +#endif + +#include +#include + +#include "../internal/syscall.h" + +/* + * readlink/readlinkat: copy the contents of the symbolic link path (the + * string the link stores, which may name anything) into buf. readlinkat + * resolves a relative path against the directory fd. The kernel writes + * up to bufsize bytes and never appends a NUL; the return value is the + * byte count written (0 for an empty target), or -1 with errno set when + * path is not a symbolic link. When bufsize is 0, buf may be NULL. + */ +ssize_t +readlink(const char *restrict path, char *restrict buf, size_t bufsize) +{ + return syscall_ret(__syscall3(SYS_readlink, (long)path, (long)buf, (long)bufsize)); +} + +ssize_t +readlinkat(int fd, const char *restrict path, char *restrict buf, size_t bufsize) +{ + return syscall_ret(__syscall4(SYS_readlinkat, fd, (long)path, (long)buf, (long)bufsize)); +} diff --git a/src/unistd/realpath.c b/src/unistd/realpath.c new file mode 100644 index 0000000..9d52fc5 --- /dev/null +++ b/src/unistd/realpath.c @@ -0,0 +1,273 @@ +#ifdef HAVE_CONFIG_H +#include +#endif + +#include +#include +#include +#include +#include + +#include "../internal/malloc.h" + +/* + * vlibc — realpath (todo 38), XSI, declared in . + * + * Resolve path to its canonical absolute form: no symbolic links, no + * "." or ".." components, no trailing slashes (except the root's own). + * + * The resolution walks path one component at a time, building the + * canonical directory prefix in a working buffer and validating every + * component against the kernel with lstat: + * + * - "." components are dropped and ".." pops the last built component + * (popping at the root keeps the root — ".." cannot climb above it); + * - any other component is appended and lstat'ed. A symbolic link is + * read with readlink, the appended component is unwound, and the + * link's target is walked in its place before the rest of path is + * resumed: an absolute target restarts the prefix at the root, a + * relative target extends it. The shared depth counter bounds the + * total links followed at 40 (the _SC_SYMLOOP_MAX value), which also + * caps the recursion. + * + * Every intermediate and final name is therefore stat'ed exactly as the + * kernel would see it, so a missing component fails with the kernel's + * own error (ENOENT, ENOTDIR, ...) and the result always names an + * existing directory chain. + * + * A relative path is anchored at the current working directory (from + * getcwd, itself canonical), so the leading ".." of e.g. "../../x" pops + * correctly against a full canonical prefix. + * + * Memory: resolved_path may be NULL (the result is then returned in a + * malloc'd buffer the caller frees) or a caller buffer of at least + * PATH_MAX bytes. The working buffer holds at most PATH_MAX (4096, the + * x86_64 _PC_PATH_MAX value); a longer intermediate name is reported as + * ENAMETOOLONG. The result is copied into resolved_path only at the end, + * so resolved_path may alias path. + * + * Whole file gated at level 2: realpath is XSI. + */ + +#if VLIBC_LEVEL_GE(2) + +/* The caller's buffer and the working buffer are conventionally + * PATH_MAX bytes; 4096 is the x86_64 _PC_PATH_MAX value. */ +#define VLIBC_REALPATH_MAX 4096 + +/* _SC_SYMLOOP_MAX: the kernel's bound on symlinks followed per name. */ +#define VLIBC_REALPATH_SYMLOOP 40 + +/* + * Drop the final "/component" from the absolute prefix out (length *len, + * which includes the NUL-terminator slot). Popping at the root leaves + * the root. + */ +static void +rp_pop(char *out, size_t *len) +{ + size_t i = *len - 1; + + while (i > 0 && out[i] != '/') + { + i--; + } + if (i == 0) + { + out[1] = '\0'; + *len = 1; + } + else + { + out[i] = '\0'; + *len = i; + } +} + +/* Append "/seg" (seglen bytes, no slash) to the prefix out. The root + * needs no separator. Return 0, or -1 with errno ENAMETOOLONG when the + * growing name would overflow the buffer. */ +static int +rp_append(char *out, size_t *len, const char *seg, size_t seglen, size_t cap) +{ + size_t need = (*len > 1 ? *len + 1 : 1) + seglen; + + if (need + 1 > cap) + { + errno = ENAMETOOLONG; + return -1; + } + if (*len > 1) + { + out[(*len)++] = '/'; + } + memcpy(out + *len, seg, seglen); + *len += seglen; + out[*len] = '\0'; + return 0; +} + +/* + * Consume the components of the path string in, appending the resolved + * result to the absolute prefix out (length *len). depth counts the + * symbolic links followed so far and bounds both the chain length and + * the recursion. Return 0 on success, or -1 with errno set. + */ +static int +rp_walk(char *out, size_t *len, const char *in, int *depth, size_t cap) +{ + const char *p = in; + + while (*p != '\0') + { + const char *seg; + size_t seglen; + + while (*p == '/') + { + p++; + } + seg = p; + while (*p != '\0' && *p != '/') + { + p++; + } + seglen = (size_t)(p - seg); + if (seglen == 0) + { + break; /* nothing but trailing slashes */ + } + if (seglen == 1 && seg[0] == '.') + { + continue; + } + if (seglen == 2 && seg[0] == '.' && seg[1] == '.') + { + rp_pop(out, len); + continue; + } + + { + size_t saved = *len; + struct stat st; + + if (rp_append(out, len, seg, seglen, cap) != 0) + { + return -1; + } + if (lstat(out, &st) != 0) + { + return -1; /* missing or unreadable: errno from the kernel */ + } + if (S_ISLNK(st.st_mode)) + { + char *target = __libc_malloc(cap); + ssize_t n; + + if (target == NULL) + { + return -1; + } + if (*depth >= VLIBC_REALPATH_SYMLOOP) + { + errno = ELOOP; + __libc_free(target); + return -1; + } + (*depth)++; + n = readlink(out, target, cap); + if (n < 0 || (size_t)n >= cap) + { + if (n >= 0) + { + errno = ENAMETOOLONG; + } + __libc_free(target); + return -1; + } + target[n] = '\0'; + + /* Unwind the appended component and walk the target in + * its place; an absolute target restarts the prefix. */ + out[saved] = '\0'; + *len = saved; + if (target[0] == '/') + { + out[0] = '/'; + out[1] = '\0'; + *len = 1; + } + if (rp_walk(out, len, target, depth, cap) != 0) + { + __libc_free(target); + return -1; + } + __libc_free(target); + } + } + } + return 0; +} + +char * +realpath(const char *restrict path, char *restrict resolved_path) +{ + char *work; + size_t len; + int depth = 0; + + if (path == NULL || *path == '\0') + { + errno = path == NULL ? EINVAL : ENOENT; + return NULL; + } + + work = (char *)__libc_malloc(VLIBC_REALPATH_MAX); + if (work == NULL) + { + return NULL; + } + + if (path[0] == '/') + { + work[0] = '/'; + work[1] = '\0'; + len = 1; + } + else + { + char *cwd = getcwd(NULL, 0); + + if (cwd == NULL) + { + __libc_free(work); + return NULL; + } + len = strlen(cwd); + if (len + 1 > VLIBC_REALPATH_MAX) + { + errno = ENAMETOOLONG; + __libc_free(cwd); + __libc_free(work); + return NULL; + } + memcpy(work, cwd, len + 1); + __libc_free(cwd); + } + + if (rp_walk(work, &len, path, &depth, VLIBC_REALPATH_MAX) != 0) + { + __libc_free(work); + return NULL; + } + + if (resolved_path == NULL) + { + return work; /* malloc'd result for the caller to free */ + } + memcpy(resolved_path, work, len + 1); + __libc_free(work); + return resolved_path; +} + +#endif /* VLIBC_LEVEL_GE(2) */ diff --git a/src/unistd/renameat.c b/src/unistd/renameat.c new file mode 100644 index 0000000..7bea5d0 --- /dev/null +++ b/src/unistd/renameat.c @@ -0,0 +1,21 @@ +#ifdef HAVE_CONFIG_H +#include +#endif + +#include +#include + +#include "../internal/syscall.h" + +/* + * renameat: rename old (resolved against fd1 when relative) to new + * (resolved against fd2 when relative); AT_FDCWD selects the current + * working directory. The kernel renames directories too and refuses to + * rename a directory onto a non-directory or across filesystems. Return + * 0, or -1 with errno set. + */ +int +renameat(int fd1, const char *old, int fd2, const char *new) +{ + return syscall_ret(__syscall4(SYS_renameat, fd1, (long)old, fd2, (long)new)); +} diff --git a/src/unistd/rmdir.c b/src/unistd/rmdir.c new file mode 100644 index 0000000..288774b --- /dev/null +++ b/src/unistd/rmdir.c @@ -0,0 +1,18 @@ +#ifdef HAVE_CONFIG_H +#include +#endif + +#include + +#include "../internal/syscall.h" + +/* + * rmdir: remove the empty directory path. The kernel refuses to remove + * a directory that still holds entries (ENOTEMPTY/EEXIST), a "." or + * ".." name, or a non-directory. Return 0, or -1 with errno set. + */ +int +rmdir(const char *path) +{ + return syscall_ret(__syscall1(SYS_rmdir, (long)path)); +} diff --git a/src/unistd/swab.c b/src/unistd/swab.c new file mode 100644 index 0000000..4fd8163 --- /dev/null +++ b/src/unistd/swab.c @@ -0,0 +1,37 @@ +#ifdef HAVE_CONFIG_H +#include +#endif + +#include + +/* + * vlibc — swab (todo 38). + * + * Copy nbytes bytes from from to to, swapping every adjacent pair (byte + * 0 with byte 1, byte 2 with byte 3, ...). A trailing odd byte is not + * copied. The historic byte-order converter is XSI, hence the whole-file + * level-2 gate. + */ + +#if VLIBC_LEVEL_GE(2) + +void +swab(const void *restrict from, void *restrict to, ssize_t nbytes) +{ + const char *f = (const char *)from; + char *t = (char *)to; + + while (nbytes > 1) + { + char a = f[0]; + char b = f[1]; + + t[0] = b; + t[1] = a; + f += 2; + t += 2; + nbytes -= 2; + } +} + +#endif /* VLIBC_LEVEL_GE(2) */ diff --git a/src/unistd/symlink.c b/src/unistd/symlink.c new file mode 100644 index 0000000..dea7eba --- /dev/null +++ b/src/unistd/symlink.c @@ -0,0 +1,26 @@ +#ifdef HAVE_CONFIG_H +#include +#endif + +#include +#include + +#include "../internal/syscall.h" + +/* + * symlink/symlinkat: create path2 (or, for symlinkat, path2 resolved + * against the directory fd when relative) as a symbolic link whose + * contents are the string path1. The kernel stores path1 verbatim; it is + * not required to name anything. Return 0, or -1 with errno set. + */ +int +symlink(const char *path1, const char *path2) +{ + return syscall_ret(__syscall2(SYS_symlink, (long)path1, (long)path2)); +} + +int +symlinkat(const char *path1, int fd, const char *path2) +{ + return syscall_ret(__syscall3(SYS_symlinkat, (long)path1, fd, (long)path2)); +} diff --git a/src/unistd/ttyname.c b/src/unistd/ttyname.c new file mode 100644 index 0000000..118b98a --- /dev/null +++ b/src/unistd/ttyname.c @@ -0,0 +1,115 @@ +#ifdef HAVE_CONFIG_H +#include +#endif + +#include + +#include + +#include + +#include "../internal/syscall.h" + +/* + * ttyname/ttyname_r: the pathname of the terminal open on fildes. + * + * The kernel does not expose an fd's path directly; Linux's procfs + * answers the question through the magic "/proc/self/fd/N" link, which + * readlink resolves to the path the descriptor was opened with (for a + * tty this is the device path such as "/dev/pts/3"). The result is + * verified against the kernel's own stat data: the path read back must + * still name the same device/inode as fildes (guards against the fd + * having been closed and reused between the readlink and the caller's + * use of the name). + * + * A descriptor that is not a terminal is reported with ENOTTY before + * procfs is consulted. Both forms return an errno NUMBER on failure + * (ttyname_r returns it directly; ttyname stores it in errno and + * returns NULL). ttyname's buffer is a shared static array overwritten + * by the next call — POSIX allows this and it is not thread-safe. + */ + +/* Static buffer for ttyname(); 256 covers any sane device path. */ +#define VLIBC_UNISTD_TTYNAME_BUF 256 + +static char ttyname_buf[VLIBC_UNISTD_TTYNAME_BUF]; + +/* + * Format "/proc/self/fd/" into out. fd is non-negative here + * (isatty rejected negative descriptors), so no sign is emitted. + */ +static void +tty_proc_name(char *out, int fd) +{ + static const char prefix[] = "/proc/self/fd/"; + char digits[12]; + int i = 0; + int n = 0; + + while (prefix[n] != '\0') + { + out[n] = prefix[n]; + n++; + } + do + { + digits[i++] = (char)('0' + (fd % 10)); + fd /= 10; + } while (fd != 0); + while (i > 0) + { + out[n++] = digits[--i]; + } + out[n] = '\0'; +} + +int +ttyname_r(int fildes, char *name, size_t namesize) +{ + char procname[sizeof "/proc/self/fd/" + 11 + 1]; + struct stat st_path; + struct stat st_fd; + ssize_t len; + + if (!isatty(fildes)) + { + return ENOTTY; + } + if (namesize == 0) + { + return ERANGE; + } + tty_proc_name(procname, fildes); + len = readlink(procname, name, namesize); + if (len < 0) + { + return errno; /* procfs unavailable or the link vanished */ + } + if ((size_t)len >= namesize) + { + return ERANGE; + } + name[len] = '\0'; + if (stat(name, &st_path) != 0 || fstat(fildes, &st_fd) != 0) + { + return errno; + } + if (st_path.st_dev != st_fd.st_dev || st_path.st_ino != st_fd.st_ino) + { + return ENOENT; /* the path no longer names fildes' terminal */ + } + return 0; +} + +char * +ttyname(int fildes) +{ + int err = ttyname_r(fildes, ttyname_buf, sizeof(ttyname_buf)); + + if (err != 0) + { + errno = err; + return NULL; + } + return ttyname_buf; +} diff --git a/src/unistd/unlink.c b/src/unistd/unlink.c new file mode 100644 index 0000000..dada7d8 --- /dev/null +++ b/src/unistd/unlink.c @@ -0,0 +1,27 @@ +#ifdef HAVE_CONFIG_H +#include +#endif + +#include +#include + +#include "../internal/syscall.h" + +/* + * unlink/unlinkat: remove a directory entry. unlink(path) names the + * entry directly; unlinkat resolves a relative path against the + * directory fd and, when flag is AT_REMOVEDIR, removes the empty + * directory fd/path instead of a non-directory name. Symbolic links are + * themselves removed, never followed. Return 0, or -1 with errno set. + */ +int +unlink(const char *path) +{ + return syscall_ret(__syscall1(SYS_unlink, (long)path)); +} + +int +unlinkat(int fd, const char *path, int flag) +{ + return syscall_ret(__syscall3(SYS_unlinkat, fd, (long)path, flag)); +} diff --git a/tests/test_unistd_misc.c b/tests/test_unistd_misc.c new file mode 100644 index 0000000..aabde59 --- /dev/null +++ b/tests/test_unistd_misc.c @@ -0,0 +1,415 @@ +/* + * vlibc — unistd misc / libgen / realpath / confstr test (todo 38). + * + * Coverage: + * + * 1. getcwd correctness: the result matches the raw SYS_getcwd answer, + * and the getcwd(NULL, 0) malloc form returns the same string; + * 2. terminal/identity probes: isatty on invalid descriptors is 0, + * ttyname(1) is NULL or a "/..." device path, ttyname_r returns 0 + * or ENOTTY; + * 3. getopt round-trips: bundled options, separate and attached + * arguments, "--" termination, silent ':' and '?' error modes, and + * optind landing on the first non-option; + * 4. basename/dirname golden cases including the trailing-slash and + * degenerate "/" / "" names; + * 5. confstr: _CS_PATH queries return a length, copy the value into a + * big buffer and report the same length when the buffer is small; + * _CS_GNU_LIBC_VERSION is the empty string; + * 6. realpath: "." and "/" resolve correctly (also in the malloc'd + * form); getwd matches getcwd; swab swaps adjacent bytes. + * + * -f mode: the failure shapes — realpath(NULL) and of a nonexistent + * name, and getcwd(buf, 0) — return NULL without crashing. + * + * The whole body is level 2 (libgen/realpath/confstr are XSI; only the + * L1 functions appear inside it); at level 1 this TU compiles to a SKIP + * runner. The main always leaves through a raw SYS_exit_group: several + * probes (ttyname, realpath, getcwd failures) write the library's errno + * slot, which collides with the host TCB, so host cleanup must never + * run after them (house pattern). + * + * No host headers are included (-Iinclude shadows them); argv arrays are + * plain writable char*[] so getopt can scan them. + */ + +#include +#include +#include + +#include +#include + +#include + +#if VLIBC_LEVEL_GE(2) + +/* Reset the getopt globals between scenarios. */ +static void +uopt_reset(void) +{ + optarg = NULL; + optind = 1; + opterr = 0; + optopt = 0; +} + +static int +test_getcwd(void) +{ + char kern[4096]; + char mine[4096]; + char *dyn; + long n; + + n = __syscall2(SYS_getcwd, (long)kern, (long)sizeof(kern)); + TEST_ASSERT_TRUE(n > 0); + TEST_ASSERT_TRUE(getcwd(mine, sizeof(mine)) == mine); + TEST_ASSERT_STREQ(mine, kern); + dyn = getcwd(NULL, 0); + TEST_ASSERT_TRUE(dyn != NULL); + if (dyn != NULL) + { + TEST_ASSERT_STREQ(dyn, kern); + free(dyn); + } + return 0; +} + +static int +test_isatty_ttyname(void) +{ + char name[256]; + char *t; + int rc; + + TEST_ASSERT_EQ(isatty(-1), 0); + TEST_ASSERT_EQ(isatty(4096), 0); + /* fd 1 may or may not be a terminal in the harness; the contract is + * only that isatty answers 0 or 1 without crashing. */ + TEST_ASSERT_TRUE(isatty(1) == 0 || isatty(1) == 1); + + rc = ttyname_r(1, name, sizeof(name)); + TEST_ASSERT_TRUE(rc == 0 || rc == ENOTTY || rc == ERANGE); + if (rc == 0) + { + TEST_ASSERT_TRUE(name[0] == '/'); + } + t = ttyname(1); + TEST_ASSERT_TRUE(t == NULL || t[0] == '/'); + return 0; +} + +static int +test_getopt_basic(void) +{ + char *av[] = {"prog", "-a", "-b", "rest", NULL}; + + uopt_reset(); + TEST_ASSERT_EQ(getopt(4, av, "ab"), 'a'); + TEST_ASSERT_EQ(getopt(4, av, "ab"), 'b'); + TEST_ASSERT_EQ(getopt(4, av, "ab"), -1); + TEST_ASSERT_EQ(optind, 3); /* optind sits on the first non-option */ + TEST_ASSERT_TRUE(optarg == NULL); + return 0; +} + +static int +test_getopt_bundled(void) +{ + char *av[] = {"prog", "-ab", "tail", NULL}; + + uopt_reset(); + TEST_ASSERT_EQ(getopt(3, av, "ab"), 'a'); + TEST_ASSERT_EQ(getopt(3, av, "ab"), 'b'); + TEST_ASSERT_EQ(getopt(3, av, "ab"), -1); + TEST_ASSERT_EQ(optind, 2); /* the first non-option is "tail" */ + return 0; +} + +static int +test_getopt_args(void) +{ + char *av1[] = {"prog", "-barg", "tail", NULL}; + char *av2[] = {"prog", "-b", "arg", "tail", NULL}; + + /* Attached argument: "-barg" gives optarg "arg" from the same + * element. */ + uopt_reset(); + TEST_ASSERT_EQ(getopt(3, av1, "b:"), 'b'); + TEST_ASSERT_STREQ(optarg, "arg"); + TEST_ASSERT_EQ(getopt(3, av1, "b:"), -1); + + /* Separate argument: "-b arg". */ + uopt_reset(); + TEST_ASSERT_EQ(getopt(4, av2, "b:"), 'b'); + TEST_ASSERT_STREQ(optarg, "arg"); + TEST_ASSERT_EQ(getopt(4, av2, "b:"), -1); + TEST_ASSERT_EQ(optind, 3); + return 0; +} + +static int +test_getopt_errors(void) +{ + char *av1[] = {"prog", "-b", NULL}; + char *av2[] = {"prog", "-z", NULL}; + char *av3[] = {"prog", "-azb", NULL}; + + /* Missing argument with a leading ':' returns ':' silently. */ + uopt_reset(); + TEST_ASSERT_EQ(getopt(2, av1, ":b:"), ':'); + TEST_ASSERT_EQ(optopt, 'b'); + TEST_ASSERT_EQ(getopt(2, av1, ":b:"), -1); + + /* Unknown option returns '?', optopt carries the character. */ + uopt_reset(); + TEST_ASSERT_EQ(getopt(2, av2, "ab"), '?'); + TEST_ASSERT_EQ(optopt, 'z'); + TEST_ASSERT_EQ(getopt(2, av2, "ab"), -1); + + /* An unknown option in the middle of a bundle does not stop the + * scan: the remaining letters are still returned. */ + uopt_reset(); + TEST_ASSERT_EQ(getopt(2, av3, "ab"), 'a'); + TEST_ASSERT_EQ(getopt(2, av3, "ab"), '?'); + TEST_ASSERT_EQ(optopt, 'z'); + TEST_ASSERT_EQ(getopt(2, av3, "ab"), 'b'); + TEST_ASSERT_EQ(getopt(2, av3, "ab"), -1); + return 0; +} + +static int +test_getopt_dashdash(void) +{ + char *av[] = {"prog", "-a", "--", "-b", NULL}; + + uopt_reset(); + TEST_ASSERT_EQ(getopt(4, av, "ab"), 'a'); + TEST_ASSERT_EQ(getopt(4, av, "ab"), -1); + TEST_ASSERT_EQ(optind, 3); /* "--" consumed; "-b" is the first non-option */ + return 0; +} + +static int +test_basename(void) +{ + char s1[] = "/usr/lib"; + char s2[] = "/usr/lib/"; + char s3[] = "/"; + char s4[] = "//"; + char s5[] = "usr"; + char s6[] = ""; + char s7[] = "/usr"; + + TEST_ASSERT_STREQ(basename(s1), "lib"); + TEST_ASSERT_STREQ(basename(s2), "lib"); + TEST_ASSERT_STREQ(basename(s3), "/"); + TEST_ASSERT_STREQ(basename(s4), "/"); + TEST_ASSERT_STREQ(basename(s5), "usr"); + TEST_ASSERT_STREQ(basename(s6), "."); + TEST_ASSERT_STREQ(basename(s7), "usr"); + return 0; +} + +static int +test_dirname(void) +{ + char s1[] = "/usr/lib"; + char s2[] = "/usr/lib/"; + char s3[] = "/"; + char s4[] = "//"; + char s5[] = "usr"; + char s6[] = ""; + char s7[] = "/usr"; + char s8[] = "usr/"; + + TEST_ASSERT_STREQ(dirname(s1), "/usr"); + TEST_ASSERT_STREQ(dirname(s2), "/usr"); + TEST_ASSERT_STREQ(dirname(s3), "/"); + TEST_ASSERT_STREQ(dirname(s4), "/"); + TEST_ASSERT_STREQ(dirname(s5), "."); + TEST_ASSERT_STREQ(dirname(s6), "."); + TEST_ASSERT_STREQ(dirname(s7), "/"); + TEST_ASSERT_STREQ(dirname(s8), "."); + return 0; +} + +static int +test_confstr(void) +{ + char buf[128]; + char small[8]; + size_t n; + + n = confstr(_CS_PATH, NULL, 0); + TEST_ASSERT_TRUE(n > 1); + TEST_ASSERT_EQ(confstr(_CS_PATH, buf, sizeof(buf)), n); + TEST_ASSERT_TRUE(buf[0] == '/'); + TEST_ASSERT_TRUE(buf[n - 1] == '\0'); + + /* A too-small buffer is left alone but the same length is reported. */ + TEST_ASSERT_EQ(confstr(_CS_PATH, small, 2), n); + + /* Not glibc: the version keys are the empty string. */ + TEST_ASSERT_EQ(confstr(_CS_GNU_LIBC_VERSION, buf, sizeof(buf)), 1); + TEST_ASSERT_STREQ(buf, ""); + return 0; +} + +static int +test_realpath(void) +{ + char here[4096]; + char in[4096]; + char out[4096]; + char parent[4096]; + char *dyn; + char *slash; + + TEST_ASSERT_TRUE(realpath(".", out) == out); + TEST_ASSERT_TRUE(getcwd(here, sizeof(here)) == here); + TEST_ASSERT_STREQ(out, here); + + TEST_ASSERT_STREQ(realpath("/", out), "/"); + + /* "/proc/self/cwd" is a symlink to the working directory: exercises + * readlink plus an absolute link target. NULL only when procfs is + * absent. */ + if (realpath("/proc/self/cwd", out) != NULL) + { + TEST_ASSERT_STREQ(out, here); + } + + /* ".." pops one component: cwd/.. resolves to cwd's parent, found by + * truncating here at its last slash (the root has no parent). The + * input and resolved buffers are distinct: realpath reads the path + * while it rewrites the resolved buffer. */ + slash = strrchr(here, '/'); + if (slash != NULL && slash != here) + { + size_t plen = (size_t)(slash - here); + + memcpy(parent, here, plen); + parent[plen] = '\0'; + strcpy(in, here); + strcpy(in + strlen(in), "/.."); + TEST_ASSERT_TRUE(realpath(in, out) == out); + TEST_ASSERT_STREQ(out, parent); + } + + dyn = realpath(".", NULL); + TEST_ASSERT_TRUE(dyn != NULL); + if (dyn != NULL) + { + TEST_ASSERT_STREQ(dyn, here); + free(dyn); + } + return 0; +} + +static int +test_getwd_swab(void) +{ + char wd[4096]; + char here[4096]; + char out[7] = {0}; + static const char six[7] = "abcdef"; + + TEST_ASSERT_TRUE(getwd(wd) == wd); + TEST_ASSERT_TRUE(getcwd(here, sizeof(here)) == here); + TEST_ASSERT_STREQ(wd, here); + + swab(six, out, 6); + TEST_ASSERT_STREQ(out, "badcfe"); + return 0; +} + +static const struct vlibc_test tests[] = { + {"getcwd", test_getcwd}, + {"isatty-ttyname", test_isatty_ttyname}, + {"getopt-basic", test_getopt_basic}, + {"getopt-bundled", test_getopt_bundled}, + {"getopt-args", test_getopt_args}, + {"getopt-errors", test_getopt_errors}, + {"getopt-dashdash", test_getopt_dashdash}, + {"basename", test_basename}, + {"dirname", test_dirname}, + {"confstr", test_confstr}, + {"realpath", test_realpath}, + {"getwd-swab", test_getwd_swab}, +}; + +/* + * Own main (not TEST_MAIN): supports the -f failure mode and always + * leaves through the raw SYS_exit_group (see the file header). + */ +int +main(int argc, char **argv) +{ + const size_t count = sizeof tests / sizeof tests[0]; + size_t passed = 0; + size_t i; + + if (argc > 1 && argv[1][0] == '-' && argv[1][1] == 'f' && argv[1][2] == '\0') + { + int before = vlibc_test_failures; + char out[64]; + + vlibc_test_say(1, "RUN failure-shapes: "); + TEST_ASSERT_TRUE(realpath(NULL, out) == NULL); + TEST_ASSERT_TRUE(realpath("/no/such/vlibc-xyz", out) == NULL); + TEST_ASSERT_TRUE(getcwd(out, 0) == NULL); + if (vlibc_test_failures == before) + { + vlibc_test_say(1, "PASS\n"); + vlibc_test_say(1, "SUMMARY: 1/1 passed, 0 assertion failure(s)\n"); + } + else + { + vlibc_test_say(1, "FAIL\n"); + } + __syscall1(SYS_exit_group, vlibc_test_failures == before ? 0 : 1); + /* not reached */ + } + + for (i = 0; i < count; i++) + { + int before = vlibc_test_failures; + + vlibc_test_say(1, "RUN "); + vlibc_test_say(1, tests[i].name); + vlibc_test_say(1, ": "); + if (tests[i].run() == 0 && vlibc_test_failures == before) + { + vlibc_test_say(1, "PASS\n"); + passed++; + } + else + { + vlibc_test_say(1, "FAIL\n"); + } + } + + vlibc_test_say(1, "SUMMARY: "); + vlibc_test_say_dec(1, (unsigned long)passed); + vlibc_test_say(1, "/"); + vlibc_test_say_dec(1, (unsigned long)count); + vlibc_test_say(1, " passed, "); + vlibc_test_say_dec(1, (unsigned long)vlibc_test_failures); + vlibc_test_say(1, " assertion failure(s)\n"); + + __syscall1(SYS_exit_group, passed == count ? 0 : 1); + /* not reached */ +} + +#else /* !VLIBC_LEVEL_GE(2) */ + +int +main(void) +{ + vlibc_test_say(1, "SKIP: this surface is level 2, not available here\n"); + return 0; +} + +#endif /* VLIBC_LEVEL_GE(2) */