diff --git a/include/stdlib.h b/include/stdlib.h index 44ab589..8a265ec 100644 --- a/include/stdlib.h +++ b/include/stdlib.h @@ -367,6 +367,170 @@ lcong48(unsigned short param[7]); #endif /* VLIBC_LEVEL_GE(2) */ +/* environment, multibyte characters, and temporary files (todo 13) */ + +/* + * getenv/setenv/unsetenv (POSIX base), the C-locale multibyte conversions + * (C23 7.24.7), and mkstemp/mkdtemp are level 1. The XSI/BSD extras — + * putenv, clearenv, mktemp, the PTY helpers, and getsubopt — are level 2. + * + * None of these functions carries an intent attribute. getenv and the + * environment mutators all read or write the shared global environ, so + * pure/const would let the compiler hoist a stale getenv result across a + * setenv call. The multibyte conversions are locale-stateful in general, + * the temporary-file functions create filesystem entries, and ptsname + * writes a shared static buffer — none of them is side-effect-free. + */ + +/* + * The process environment: a NULL-terminated array of "name=value" + * strings. The array is installed by the startup code before main; the + * functions below mutate it (setenv/unsetenv copy strings into malloc'd + * storage). Reassigning environ directly is permitted by POSIX, but + * calling setenv/unsetenv/putenv/clearenv afterwards is undefined, as it + * is everywhere (the array is then no longer guaranteed to be owned by + * the library). + */ +extern char **environ; + +/* + * Return a pointer to the value part (the bytes after '=') of the + * environment entry whose name matches name, or NULL when the variable + * is not set. The pointer is valid until the next setenv, unsetenv, + * putenv, or clearenv call. No intent attribute: the result depends on + * the mutable global environ. + */ +char * +getenv(const char *name); + +/* + * Set name to value. A copy of "name=value" is made, so the caller may + * reuse or free its buffers once the call returns. With overwrite + * nonzero an existing entry is replaced; with overwrite zero an existing + * entry is left unchanged and 0 is returned. Returns -1 with errno + * EINVAL when name is NULL, empty, or contains '=', and -1 with errno + * ENOMEM on allocation failure. A NULL value is treated as the empty + * string. + */ +int +setenv(const char *name, const char *value, int overwrite); + +/* + * Remove every entry whose name is name, compacting the array. Returns + * -1 with errno EINVAL when name is NULL, empty, or contains '='. + */ +int +unsetenv(const char *name); + +/* + * C-locale multibyte conversions (C23 7.24.7). Only the "C" locale + * exists today (locale support is a later todo), where every multibyte + * character is a single byte: byte c corresponds to the wide character + * (unsigned char)c. The encoding is therefore stateless — the s == NULL + * and NULL-destination forms all report "state-independent" — and no + * mbstate_t variant is needed. mbtowc never fails in this locale (every + * byte, including 0x80..0xFF, is a valid character); wctomb fails with + * -1 + errno EILSEQ for any wchar_t outside 0..255. + */ +int +mblen(const char *s, size_t n); + +int +mbtowc(wchar_t *restrict pwc, const char *restrict s, size_t n); + +int +wctomb(char *s, wchar_t wc); + +size_t +mbstowcs(wchar_t *restrict pwcs, const char *restrict s, size_t n); + +size_t +wcstombs(char *restrict s, const wchar_t *restrict pwcs, size_t n); + +/* + * Temporary files and directories. mkstemp replaces the trailing + * "XXXXXX" of the template with random characters, creates the file + * with mode 0600, and returns an open file descriptor (-1 with errno + * EINVAL when the template has no "XXXXXX" suffix). mkdtemp does the + * same replacement, creates a directory with mode 0700, and returns the + * template (NULL on failure). The parameter is unnamed in these + * declarations because POSIX's `template` is a C++ keyword and this + * header is compiled by C++ consumers too. + */ +int +mkstemp(char *); + +char * +mkdtemp(char *); + +#if VLIBC_LEVEL_GE(2) +/* Level 2 (muslmimic): XSI and BSD environment extras. */ + +/* + * Add string — a "name=value" pair in CALLER-OWNED storage — to the + * environment: the caller's buffer becomes part of the environment and + * must remain valid for the life of the process. An existing variable of + * the same name is replaced (the old pointer is dropped); a string + * without '=' removes the variable instead (glibc/musl behavior). + * Returns 0, or -1 with errno ENOMEM. + */ +int +putenv(char *string); + +/* + * Empty the environment, leaving environ as a fresh valid empty array + * (never NULL) so later setenv calls keep working. The previous array + * and its entries are deliberately not freed — entries may be putenv'd + * caller storage or initial-stack strings. Returns 0, or -1 with errno + * ENOMEM. + */ +int +clearenv(void); + +/* + * Obsolescent mkstemp without the file: fills in the trailing "XXXXXX" + * and returns the template, creating nothing. The name is not guaranteed + * unique — nothing exists to race on. NULL with errno EINVAL when the + * template has no "XXXXXX" suffix. + */ +char * +mktemp(char *); + +/* + * XSI PTY helpers. posix_openpt opens the master side of a + * pseudo-terminal via /dev/ptmx (flags, plus O_NOCTTY, are passed to + * open) and returns the fd. grantpt returns 0 without doing anything: + * modern kernels grant the slave when the master is opened. unlockpt + * releases the slave-side lock. ptsname formats the slave path + * "/dev/pts/N" into a shared static buffer — the result is valid until + * the next ptsname call and is not thread-safe. + */ +int +posix_openpt(int flags); + +char * +ptsname(int fd); + +int +grantpt(int fd); + +int +unlockpt(int fd); + +/* + * Parse one comma-separated suboption out of *optionp (POSIX XSI). On + * success the index of the matching token is returned and *valuep points + * to the "=value" part (or is NULL when the suboption has no '='); for + * an unrecognized suboption -1 is returned and *valuep points to the + * whole suboption string; at the end of the list -1 is returned and + * *valuep is NULL. *optionp always advances past the comma, which is + * overwritten with NUL (the string is modified in place). + */ +int +getsubopt(char **optionp, char *const *tokens, char **valuep); + +#endif /* VLIBC_LEVEL_GE(2) */ + #ifdef __cplusplus } #endif diff --git a/src/stdlib/env.c b/src/stdlib/env.c new file mode 100644 index 0000000..c5c158b --- /dev/null +++ b/src/stdlib/env.c @@ -0,0 +1,322 @@ +#ifdef HAVE_CONFIG_H +#include +#endif + +#include +#include +#include + +/* + * Process environment access (todo 13). + * + * getenv/setenv/unsetenv are POSIX base; putenv and clearenv are XSI/BSD + * and gated at level 2. All of them operate on the global environ array + * installed by the startup code (src/start/environ.c): a NULL-terminated + * array of "name=value" strings. + * + * Ownership model (why removed strings are never freed): setenv copies + * the name and value into malloc'd storage, but putenv stores the + * caller's own buffer, and the initial entries live on the + * kernel-provided stack. The library therefore cannot tell which entry + * strings it owns, so unsetenv/clearenv/overwrite drop the pointers + * without freeing them. Repeated setenv/unsetenv cycles leak one small + * string each — permitted by POSIX (the function set that allocates is + * not required to free) and safe for every entry origin. + * + * The pointer ARRAY itself is tracked: env_array_owned says whether + * environ points at an array this file malloc'd. The first append to an + * inherited array copies the pointers into fresh storage instead of + * realloc'ing memory the allocator does not own (an initial-stack or + * caller-provided array), and every later append reallocs in place. + * Reassigning environ directly is POSIX-legal but then makes the + * following functions' behavior undefined, exactly as the header + * documents. + */ + +/* True when environ points at an array this file allocated. */ +static int env_array_owned; + +/* + * Validate name for setenv/unsetenv/getenv: non-NULL, non-empty, no '='. + * On success *len_out receives the length (the bytes before the would-be + * '='). + */ +static int +env_name_ok(const char *name, size_t *len_out) +{ + size_t i = 0; + + if (name == NULL) + { + return 0; + } + while (name[i] != '\0' && name[i] != '=') + { + i++; + } + if (i == 0 || name[i] == '=') + { + return 0; + } + *len_out = i; + return 1; +} + +/* True when the entry "name=value" string has the given name prefix. */ +// NOLINTBEGIN(bugprone-easily-swappable-parameters) +static int +env_entry_matches(const char *entry, const char *name, size_t name_len) +{ + size_t i; + + for (i = 0; i < name_len; i++) + { + if (entry[i] != name[i]) + { + return 0; + } + } + return entry[name_len] == '='; +} +// NOLINTEND(bugprone-easily-swappable-parameters) + +/* Slot in environ holding an entry with the given name, or NULL. */ +static char ** +env_find(const char *name, size_t name_len) +{ + char **p; + + if (environ == 0) + { + return 0; + } + for (p = environ; *p != 0; p++) + { + if (env_entry_matches(*p, name, name_len)) + { + return p; + } + } + return 0; +} + +/* Append entry to environ, growing or creating the pointer array. */ +static int +env_append(char *entry) +{ + char **array; + char **p; + size_t count = 0; + size_t i; + + for (p = environ; p != 0 && *p != 0; p++) + { + count++; + } + if (env_array_owned) + { + array = (char **)realloc((void *)environ, (count + 2) * sizeof(char *)); + } + else + { + array = (char **)malloc((count + 2) * sizeof(char *)); + if (array != 0) + { + for (i = 0; i < count; i++) + { + array[i] = environ[i]; + } + env_array_owned = 1; + } + } + if (array == 0) + { + return -1; + } + array[count] = entry; + array[count + 1] = NULL; + environ = array; + return 0; +} + +char * +getenv(const char *name) +{ + size_t name_len; + char **slot; + + if (!env_name_ok(name, &name_len)) + { + return NULL; + } + slot = env_find(name, name_len); + if (slot == 0) + { + return NULL; + } + return *slot + name_len + 1; +} + +/* + * no-tree-loop-distribute-patterns keeps GCC -O2 from rewriting the + * length scan and the two copy loops below into strlen/memcpy calls: + * the stdlib slice must not pull in the string library (the string.h + * precedent: the copy/scan family carries the same attribute), and a + * standalone test link has no memcpy/strlen objects on the command line. + */ +// NOLINTBEGIN(bugprone-easily-swappable-parameters) +__attribute__((optimize("no-tree-loop-distribute-patterns"))) int +setenv(const char *name, const char *value, int overwrite) +{ + size_t name_len; + size_t value_len; + char **slot; + char *entry; + size_t i; + + if (!env_name_ok(name, &name_len)) + { + errno = EINVAL; + return -1; + } + value_len = 0; + if (value != NULL) + { + while (value[value_len] != '\0') + { + value_len++; + } + } + slot = env_find(name, name_len); + if (slot != 0 && !overwrite) + { + return 0; + } + entry = malloc(name_len + 1 + value_len + 1); + if (entry == NULL) + { + errno = ENOMEM; + return -1; + } + for (i = 0; i < name_len; i++) + { + entry[i] = name[i]; + } + entry[name_len] = '='; + for (i = 0; i < value_len; i++) + { + entry[name_len + 1 + i] = value[i]; + } + entry[name_len + 1 + value_len] = '\0'; + if (slot != 0) + { + /* The old string is not freed: it may be putenv'd caller storage. */ + *slot = entry; + return 0; + } + if (env_append(entry) != 0) + { + free(entry); + errno = ENOMEM; + return -1; + } + return 0; +} +// NOLINTEND(bugprone-easily-swappable-parameters) + +int +unsetenv(const char *name) +{ + size_t name_len; + char **p; + + if (!env_name_ok(name, &name_len)) + { + errno = EINVAL; + return -1; + } + if (environ == 0) + { + return 0; + } + p = environ; + while (*p != 0) + { + if (env_entry_matches(*p, name, name_len)) + { + char **q = p; + + do + { + *q = q[1]; + q++; + } while (q[-1] != NULL); + /* p now holds the next entry; check it too, without advancing. */ + } + else + { + p++; + } + } + return 0; +} + +#if VLIBC_LEVEL_GE(2) + +int +putenv(char *string) +{ + size_t name_len; + char **slot; + + if (string == NULL) + { + errno = EINVAL; + return -1; + } + name_len = 0; + while (string[name_len] != '\0' && string[name_len] != '=') + { + name_len++; + } + if (name_len == 0) + { + errno = EINVAL; + return -1; + } + if (string[name_len] == '\0') + { + /* A bare name removes the variable (glibc/musl behavior). */ + return unsetenv(string); + } + slot = env_find(string, name_len); + if (slot != 0) + { + *slot = string; + return 0; + } + if (env_append(string) != 0) + { + errno = ENOMEM; + return -1; + } + return 0; +} + +int +clearenv(void) +{ + char **empty = (char **)malloc(sizeof(char *)); + + if (empty == 0) + { + errno = ENOMEM; + return -1; + } + empty[0] = NULL; + environ = empty; + env_array_owned = 1; + return 0; +} + +#endif /* VLIBC_LEVEL_GE(2) */ diff --git a/src/stdlib/getsubopt.c b/src/stdlib/getsubopt.c new file mode 100644 index 0000000..4d46e65 --- /dev/null +++ b/src/stdlib/getsubopt.c @@ -0,0 +1,86 @@ +#ifdef HAVE_CONFIG_H +#include +#endif + +#include + +#include + +#if VLIBC_LEVEL_GE(2) + +/* + * vlibc — getsubopt (todo 13). + * + * POSIX XSI comma-separated suboption parser. The whole file self-gates + * at VLIBC_LEVEL >= 2 (the build wiring pass may compile it + * unconditionally): at level 1 this translation unit produces nothing. + * + * Contract (POSIX): *optionp points into a "name[=value][,name[=value]...]" + * string that is modified in place. The comma terminating the parsed + * suboption is overwritten with NUL and *optionp advances past it (or to + * NULL at the end of the list). A recognized name returns its token index + * with *valuep pointing at the value after '=' (the '=' itself stays in + * the string, NUL-terminated by the comma); a matched name without '=' + * sets *valuep to NULL. An unrecognized name returns -1 with *valuep + * pointing at the whole suboption, '=' and value included — the '=' is + * NOT consumed, because *valuep must still be able to see it. + */ + +// NOLINTBEGIN(bugprone-easily-swappable-parameters) +int +getsubopt(char **optionp, char *const *tokens, char **valuep) +{ + char *s = *optionp; + char *end; + int i; + + if (s == NULL) + { + *valuep = NULL; + return -1; + } + end = s; + while (*end != '\0' && *end != ',') + { + end++; + } + if (*end == ',') + { + *end = '\0'; + *optionp = end + 1; + } + else + { + *optionp = NULL; + } + *valuep = s; /* no-match default: the whole suboption */ + for (i = 0; tokens[i] != NULL; i++) + { + const char *key = tokens[i]; + size_t j = 0; + + while (key[j] != '\0' && s[j] == key[j]) + { + j++; + } + if (key[j] != '\0') + { + continue; + } + if (s[j] == '=') + { + *valuep = s + j + 1; + return i; + } + if (s[j] == '\0') + { + *valuep = NULL; + return i; + } + /* s[j] is neither NUL nor '=': key is a prefix, keep scanning. */ + } + return -1; +} +// NOLINTEND(bugprone-easily-swappable-parameters) + +#endif /* VLIBC_LEVEL_GE(2) */ diff --git a/src/stdlib/mkstemp.c b/src/stdlib/mkstemp.c new file mode 100644 index 0000000..6ef0cfc --- /dev/null +++ b/src/stdlib/mkstemp.c @@ -0,0 +1,187 @@ +#ifdef HAVE_CONFIG_H +#include +#endif + +#include +#include + +#include + +#include "../internal/syscall.h" + +/* + * vlibc — temporary files and directories (todo 13). + * + * mkstemp/mkdtemp (POSIX base) replace the six trailing 'X' characters of + * the template with a generated suffix and create the file (mode 0600) or + * directory (mode 0700); mktemp (XSI/BSD, level 2) only fills in the + * suffix, creating nothing — its name is not guaranteed unique, but + * nothing exists to race on. + * + * The suffix comes from a small internal 64-bit LCG seeded from the pid + * (SYS_getpid): deliberately independent of rand() and time(), so the + * temporary-file functions never depend on the PRNG family or a clock + * source, and the sequence is reproducible per process. A colliding name + * (EEXIST) regenerates the suffix, up to 100 attempts. + * + * Creation uses raw syscalls (SYS_openat/SYS_mkdir): the public open and + * mkdir wrappers belong to later todos. The O_ and AT_ constants are + * defined locally for the same reason (include/fcntl.h is owned by todo + * 21); the values are kernel UAPI. + */ + +/* Kernel UAPI fcntl constants (local until include/fcntl.h lands). */ +#define TEMP_AT_FDCWD (-100) +#define TEMP_O_RDWR 0x2 +#define TEMP_O_CREAT 0x40 +#define TEMP_O_EXCL 0x80 + +/* Retry budget for EEXIST collisions (6 chars over a 62-symbol alphabet). */ +#define TEMP_TRIES 100 + +/* The 62-character suffix alphabet: digits, uppercase, lowercase. */ +static const char temp_alphabet[] = + "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"; + +/* Internal LCG state, seeded lazily from the pid on first use. */ +static unsigned long long temp_state; +static int temp_seeded; + +static void +temp_advance(void) +{ + if (!temp_seeded) + { + long pid = __syscall0(SYS_getpid); + + temp_state = ((unsigned long long)pid * 6364136223846793005ULL) + 1ULL; + temp_seeded = 1; + } + else + { + temp_state = (temp_state * 6364136223846793005ULL) + 1ULL; + } +} + +/* Overwrite the six bytes at x with a fresh generated suffix. */ +static void +temp_fill(char *x) +{ + int i; + + for (i = 0; i < 6; i++) + { + temp_advance(); + x[i] = temp_alphabet[(size_t)((temp_state >> 33) % 62)]; + } +} + +/* + * Validate the template: it must end in exactly six 'X' characters. On + * success *suffix_out receives the address of the first 'X'. The template + * is not modified on failure. + */ +static int +temp_suffix(char *template, char **suffix_out) +{ + size_t len = 0; + char *p; + int i; + + while (template[len] != '\0') + { + len++; + } + if (len < 6) + { + return 0; + } + p = template + len - 6; + for (i = 0; i < 6; i++) + { + if (p[i] != 'X') + { + return 0; + } + } + *suffix_out = p; + return 1; +} + +int +mkstemp(char *template) +{ + char *suffix; + int tries; + + if (!temp_suffix(template, &suffix)) + { + errno = EINVAL; + return -1; + } + for (tries = 0; tries < TEMP_TRIES; tries++) + { + int fd; + + temp_fill(suffix); + fd = syscall_ret(__syscall4(SYS_openat, TEMP_AT_FDCWD, (long)template, + TEMP_O_RDWR | TEMP_O_CREAT | TEMP_O_EXCL, 0600)); + if (fd >= 0) + { + return fd; + } + if (errno != EEXIST) + { + return -1; + } + /* Name collision: regenerate the suffix and try again. */ + } + errno = EEXIST; + return -1; +} + +char * +mkdtemp(char *template) +{ + char *suffix; + int tries; + + if (!temp_suffix(template, &suffix)) + { + errno = EINVAL; + return NULL; + } + for (tries = 0; tries < TEMP_TRIES; tries++) + { + temp_fill(suffix); + if (syscall_ret(__syscall2(SYS_mkdir, (long)template, 0700)) == 0) + { + return template; + } + if (errno != EEXIST) + { + return NULL; + } + /* Name collision: regenerate the suffix and try again. */ + } + errno = EEXIST; + return NULL; +} + +#if VLIBC_LEVEL_GE(2) + +char * +mktemp(char *template) +{ + char *suffix; + + if (!temp_suffix(template, &suffix)) + { + errno = EINVAL; + return NULL; + } + temp_fill(suffix); + return template; +} + +#endif /* VLIBC_LEVEL_GE(2) */ diff --git a/src/stdlib/multibyte.c b/src/stdlib/multibyte.c new file mode 100644 index 0000000..ee5a843 --- /dev/null +++ b/src/stdlib/multibyte.c @@ -0,0 +1,137 @@ +#ifdef HAVE_CONFIG_H +#include +#endif + +#include +#include + +#include + +/* + * vlibc — C-locale multibyte conversions (todo 13). + * + * Only the "C" locale exists today (locale support is a later todo), and + * in that locale every multibyte character is exactly one byte: byte c is + * the wide character (unsigned char)c. The encoding is stateless, so the + * s == NULL state-query forms all report "state-independent" (0), and no + * mbstate_t variant is needed. mbtowc never fails here — every byte + * value, including 0x80..0xFF, is a valid character — while wctomb fails + * with -1 + errno EILSEQ for any wide character outside 0..255. + * + * The n limits are observed but cannot truncate in a 1-byte encoding: an + * n of 0 disables conversion (mbtowc returns -1), and any n >= 1 accepts + * one character. mbstowcs/wcstombs stop at an embedded NUL, which is + * CONVERTED into the destination (the terminating L'\0'/'\0' is written) + * but NOT counted in the return value. + */ + +int +mblen(const char *s, size_t n) +{ + (void)n; + if (s == NULL) + { + /* State query: the C-locale encoding has no shift state. */ + return 0; + } + return *s == '\0' ? 0 : 1; +} + +int +mbtowc(wchar_t *restrict pwc, const char *restrict s, size_t n) +{ + if (s == NULL) + { + return 0; + } + if (n == 0) + { + return -1; + } + if (*s == '\0') + { + if (pwc != NULL) + { + *pwc = L'\0'; + } + return 0; + } + if (pwc != NULL) + { + *pwc = (wchar_t)(unsigned char)*s; + } + return 1; +} + +int +wctomb(char *s, wchar_t wc) +{ + if (s == NULL) + { + return 0; + } + if (wc < 0 || wc > 255) + { + errno = EILSEQ; + return -1; + } + s[0] = (char)wc; + return 1; +} + +size_t +mbstowcs(wchar_t *restrict pwcs, const char *restrict s, size_t n) +{ + size_t i; + + for (i = 0; i < n; i++) + { + unsigned char c = (unsigned char)s[i]; + + if (c == '\0') + { + /* The terminating NUL is converted but not counted. */ + if (pwcs != NULL) + { + pwcs[i] = L'\0'; + } + return i; + } + if (pwcs != NULL) + { + pwcs[i] = (wchar_t)c; + } + } + return i; +} + +size_t +wcstombs(char *restrict s, const wchar_t *restrict pwcs, size_t n) +{ + size_t i; + + for (i = 0; i < n; i++) + { + wchar_t wc = pwcs[i]; + + if (wc == L'\0') + { + /* The terminating L'\0' is converted but not counted. */ + if (s != NULL) + { + s[i] = '\0'; + } + return i; + } + if (wc < 0 || wc > 255) + { + errno = EILSEQ; + return (size_t)-1; + } + if (s != NULL) + { + s[i] = (char)wc; + } + } + return i; +} diff --git a/src/stdlib/pty.c b/src/stdlib/pty.c new file mode 100644 index 0000000..741f67d --- /dev/null +++ b/src/stdlib/pty.c @@ -0,0 +1,105 @@ +#ifdef HAVE_CONFIG_H +#include +#endif + +#include +#include + +#include + +#include "../internal/syscall.h" + +#if VLIBC_LEVEL_GE(2) + +/* + * vlibc — XSI PTY helpers (todo 13). + * + * The whole file self-gates at VLIBC_LEVEL >= 2 (the build wiring pass + * may compile it unconditionally): at level 1 this translation unit + * produces nothing, because all four functions are XSI [PTY], not + * POSIX.1-2008 base. + * + * Everything goes through the raw syscall layer: the public open/ioctl + * wrappers belong to later todos, and include/fcntl.h (with the O_* + * constants) is #21's file, so the kernel-UAPI values needed here are + * defined locally. + * + * ptsname formats into a shared static buffer by hand — no snprintf/stdio + * exists in the tree yet. The result is valid until the next ptsname call + * and is not thread-safe, exactly as the header documents. + */ + +/* Kernel UAPI fcntl/ioctl constants (local until #21/#26 land). */ +#define PTY_AT_FDCWD (-100) +#define PTY_O_NOCTTY 0x100 +#define PTY_TIOCGPTN 0x80045430 /* _IOR('T', 0x30, unsigned int) */ +#define PTY_TIOCSPTLCK 0x40045431 /* _IOW('T', 0x31, int) */ + +int +posix_openpt(int flags) +{ + return syscall_ret( + __syscall3(SYS_openat, PTY_AT_FDCWD, (long)"/dev/ptmx", flags | PTY_O_NOCTTY)); +} + +int +grantpt(int fd) +{ + /* + * No-op by design: modern kernels grant the slave pty when the master + * is opened, so there is nothing to chmod or chown. POSIX allows a + * grantpt that always succeeds on such systems. + */ + (void)fd; + return 0; +} + +int +unlockpt(int fd) +{ + int unlock = 0; + + return syscall_ret(__syscall3(SYS_ioctl, fd, PTY_TIOCSPTLCK, (long)&unlock)); +} + +char * +ptsname(int fd) +{ + static char buf[20]; /* "/dev/pts/" (9) + up to 10 digits + NUL */ + static const char prefix[] = "/dev/pts/"; + char digits[11]; + char *p; + char *d; + const char *q; + unsigned int n = 0; /* written by TIOCGPTN; zeroed for the static analyzers */ + + if (syscall_ret(__syscall3(SYS_ioctl, fd, PTY_TIOCGPTN, (long)&n)) != 0) + { + /* syscall_ret left errno (ENOTTY for a non-pty fd). */ + return NULL; + } + /* Build the digits backward into a local, then assemble the result. */ + p = digits + sizeof(digits) - 1; + *p = '\0'; + do + { + p--; + *p = (char)('0' + (n % 10)); + n /= 10; + } while (n != 0); + d = buf; + for (q = prefix; *q != '\0'; q++) + { + *d = *q; + d++; + } + for (q = p; *q != '\0'; q++) + { + *d = *q; + d++; + } + *d = '\0'; + return buf; +} + +#endif /* VLIBC_LEVEL_GE(2) */ diff --git a/tests/test_env.c b/tests/test_env.c new file mode 100644 index 0000000..22bd012 --- /dev/null +++ b/tests/test_env.c @@ -0,0 +1,316 @@ +/* + * vlibc — environment, multibyte, and temporary-file test (todo 13). + * + * Exercises the stdlib environment slice end to end: + * + * 1. setenv("V","1",0) installs; getenv returns the value; setenv with + * overwrite=0 leaves it alone; overwrite=1 replaces it; unsetenv + * removes it (getenv -> NULL). + * 2. setenv with '=' in the name returns -1 (value check only). + * 3. Level 2: putenv stores a caller-owned "A=B" string that getenv + * sees; clearenv empties the environment and a later setenv still + * works. + * 4. C-locale multibyte round-trips: mblen/mbtowc/wctomb state-query and + * conversion forms, embedded-NUL stop (converted, not counted), the + * n clamps, and wctomb/wcstombs of a wide character > 255 failing + * with -1. + * 5. mkstemp on "/tmp/vlibcXXXXXX": fd >= 0, write/lseek/read round-trip + * through raw syscalls, close, unlink. mkdtemp creates a directory + * and rmdir removes it. + * 6. Failure mode (-f): mkstemp on a template without a "XXXXXX" suffix + * returns -1 and leaves the template byte-identical. + * + * errno is never read here. A few negative paths (setenv EINVAL, + * wctomb/wcstombs EILSEQ) make the LIBRARY write errno, and vlibc's errno + * macro addresses %fs:0+8 — under the host libc (this test links against + * glibc) that slot is the TLS dtv pointer, glibc's private state. Those + * calls are therefore bracketed with a save/restore of the slot: between + * the write and the restore only vlibc code runs, so the host TCB is + * intact again before any host code touches it (the tests/syscall_test.c + * fake-TCB discipline). The -f mode makes one such write inside mkstemp + * and then leaves via a raw SYS_exit_group without running host cleanup. + * + * All diagnostics go through raw SYS_write (no stdio, no host headers): + * under -Iinclude the vlibc public headers shadow GCC's internal ones. + * + * Not part of the library proper; compiled manually for this todo (the + * tests/ + make check wiring is owned by a later todo). + */ + +#include +#include + +#include "../src/internal/syscall.h" + +static int failures; + +/* Write a NUL-terminated string to fd via the raw syscall layer. */ +static void +say(int fd, const char *s) +{ + long n = 0; + + while (s[n] != '\0') + { + n++; + } + __syscall3(SYS_write, fd, (long)s, n); +} + +/* Write v in decimal to fd. */ +static void +say_dec(int fd, unsigned long v) // NOLINT(bugprone-easily-swappable-parameters) +{ + char buf[24]; + int i = (int)sizeof(buf); + + buf[--i] = '\0'; + do + { + buf[--i] = (char)('0' + (v % 10)); + v /= 10; + } while (v != 0); + __syscall3(SYS_write, fd, (long)(buf + i), (long)(sizeof(buf) - 1 - i)); +} + +static void +check(int cond, const char *what) +{ + if (cond) + { + say(1, "PASS: "); + say(1, what); + say(1, "\n"); + } + else + { + say(2, "FAIL: "); + say(2, what); + say(2, "\n"); + failures++; + } +} + +/* + * Byte-compare two NUL-terminated strings (vlibc's own — the test stays + * host-header-free, so no ). noipa keeps GCC from folding the + * comparison against a string literal at -O2. + */ +static __attribute__((noipa)) int +str_eq(const char *a, const char *b) +{ + if (a == NULL || b == NULL) + { + return a == b; + } + while (*a == *b && *a != '\0') + { + a++; + b++; + } + return *a == *b; +} + +/* True when s starts with the given prefix. */ +static __attribute__((noipa)) int +prefix_eq(const char *s, const char *prefix) +{ + while (*prefix != '\0') + { + if (*s != *prefix) + { + return 0; + } + s++; + prefix++; + } + return 1; +} + +/* Save/restore the host libc's TCB slot 1 (%fs:0+8), see the banner. */ +static unsigned long +tcb_slot1_save(void) +{ + return *(unsigned long *)((char *)__builtin_thread_pointer() + 8); +} + +static void +tcb_slot1_restore(unsigned long v) +{ + *(unsigned long *)((char *)__builtin_thread_pointer() + 8) = v; +} + +/* 1-3. Environment access: setenv/getenv/unsetenv, putenv, clearenv. */ +static void +env_scenarios(void) +{ + const char *v; + unsigned long saved; +#if VLIBC_LEVEL_GE(2) + static char putenv_buf[] = "A=B"; +#endif + + check(setenv("V", "1", 0) == 0, "setenv(\"V\",\"1\",0) returns 0"); + v = getenv("V"); + check(v != NULL && str_eq(v, "1"), "getenv(\"V\") == \"1\""); + check(setenv("V", "9", 0) == 0, "setenv(\"V\",\"9\",0) no-overwrite returns 0"); + v = getenv("V"); + check(v != NULL && str_eq(v, "1"), "getenv(\"V\") still \"1\" after no-overwrite"); + check(setenv("V", "2", 1) == 0, "setenv(\"V\",\"2\",1) overwrite returns 0"); + v = getenv("V"); + check(v != NULL && str_eq(v, "2"), "getenv(\"V\") == \"2\" after overwrite"); + check(unsetenv("V") == 0, "unsetenv(\"V\") returns 0"); + check(getenv("V") == NULL, "getenv(\"V\") == NULL after unsetenv"); + saved = tcb_slot1_save(); + check(setenv("A=B", "x", 0) == -1, "setenv with '=' in the name returns -1"); + tcb_slot1_restore(saved); +#if VLIBC_LEVEL_GE(2) + check(putenv(putenv_buf) == 0, "putenv(\"A=B\") returns 0"); + v = getenv("A"); + check(v != NULL && str_eq(v, "B"), "getenv(\"A\") == \"B\" after putenv"); + check(clearenv() == 0, "clearenv() returns 0"); + check(getenv("A") == NULL, "getenv(\"A\") == NULL after clearenv"); + check(setenv("V", "3", 0) == 0, "setenv after clearenv returns 0"); + v = getenv("V"); + check(v != NULL && str_eq(v, "3"), "getenv(\"V\") == \"3\" after clearenv + setenv"); +#endif /* VLIBC_LEVEL_GE(2) */ +} + +/* 4. C-locale multibyte conversions. */ +static void +multibyte_scenarios(void) +{ + wchar_t wc; + wchar_t wcs[8]; + char buf[16]; + unsigned long saved; + + check(mblen(NULL, 5) == 0, "mblen(NULL) state query returns 0"); + check(mblen("", 5) == 0, "mblen(\"\") == 0"); + check(mblen("x", 5) == 1, "mblen(\"x\") == 1"); + check(mbtowc(&wc, NULL, 5) == 0, "mbtowc with s == NULL state query returns 0"); + check(mbtowc(NULL, "x", 5) == 1, "mbtowc with pwc == NULL converts but does not store"); + check(mbtowc(&wc, "x", 0) == -1, "mbtowc with n == 0 returns -1"); + wc = L'q'; + check(mbtowc(&wc, "x", 5) == 1, "mbtowc(\"x\") == 1"); + check(wc == L'x', "mbtowc converts 'x' to L'x'"); + check(mbtowc(&wc, "", 5) == 0, "mbtowc(\"\") == 0"); + check(wc == 0, "mbtowc(\"\") stores L'\\0'"); + check(wctomb(NULL, L'x') == 0, "wctomb(NULL) state query returns 0"); + buf[0] = 'q'; + check(wctomb(buf, L'A') == 1, "wctomb(L'A') == 1"); + check(buf[0] == 'A', "wctomb stores 'A'"); + saved = tcb_slot1_save(); + check(wctomb(buf, (wchar_t)0x100) == -1, "wctomb of a wc > 255 returns -1"); + tcb_slot1_restore(saved); + + check(mbstowcs(wcs, "abc", 8) == 3, "mbstowcs(\"abc\") == 3"); + check(wcs[0] == L'a' && wcs[1] == L'b' && wcs[2] == L'c' && wcs[3] == 0, + "mbstowcs converts a/b/c and NUL-terminates"); + check(mbstowcs(wcs, "ab", 8) == 2, "mbstowcs stops at the embedded NUL, uncounted"); + check(wcs[2] == 0, "mbstowcs writes the terminating L'\\0'"); + check(mbstowcs(wcs, "abcd", 2) == 2, "mbstowcs clamps to n"); + check(wcstombs(buf, L"abc", 8) == 3, "wcstombs(L\"abc\") == 3"); + check(str_eq(buf, "abc"), "wcstombs produces \"abc\""); + check(wcstombs(buf, L"", 8) == 0, "wcstombs(L\"\") == 0"); + check(buf[0] == 0, "wcstombs writes the terminating NUL"); + check(wcstombs(buf, L"abcd", 2) == 2, "wcstombs clamps to n"); + wc = (wchar_t)0x100; + saved = tcb_slot1_save(); + check(wcstombs(buf, &wc, 8) == (size_t)-1, "wcstombs of a wc > 255 returns -1"); + tcb_slot1_restore(saved); +} + +/* 5. Temporary files and directories. */ +static void +tempfile_scenarios(void) +{ + char t1[] = "/tmp/vlibcXXXXXX"; + char t2[] = "/tmp/vlibc-dir-XXXXXX"; + char back[4] = {0}; /* written by the raw SYS_read below */ + char *r; + int fd; + int ok; + long got; + + fd = mkstemp(t1); + check(fd >= 0, "mkstemp returns a valid fd"); + check(fd < 0 || prefix_eq(t1, "/tmp/vlibc"), "mkstemp keeps the template prefix"); + if (fd >= 0) + { + __syscall3(SYS_write, fd, (long)"hi42", 4); + __syscall3(SYS_lseek, fd, 0, 0); + got = __syscall3(SYS_read, fd, (long)back, 4); + ok = got == 4 && back[0] == 'h' && back[1] == 'i' && back[2] == '4' && back[3] == '2'; + check(ok, "mkstemp file round-trips write/lseek/read"); + __syscall1(SYS_close, fd); + __syscall1(SYS_unlink, (long)t1); + } + r = mkdtemp(t2); + check(r == t2, "mkdtemp returns the template"); + if (r != NULL) + { + check(prefix_eq(t2, "/tmp/vlibc-dir"), "mkdtemp keeps the template prefix"); + __syscall1(SYS_rmdir, (long)t2); + } +} + +/* Failure scenarios (-f): bad template rejected, template unchanged. */ +static int +failure_scenarios(void) +{ + char bad[] = "no-suffix"; + int rc = mkstemp(bad); + + if (rc != -1) + { + say(2, "FAIL: mkstemp without a XXXXXX suffix returned non-(-1)\n"); + failures++; + } + else + { + say(1, "PASS: mkstemp(\"no-suffix\") -> -1 (EINVAL)\n"); + } + if (bad[0] == 'n' && bad[1] == 'o' && bad[2] == '-' && bad[3] == 's' && bad[4] == 'u' && + bad[5] == 'f' && bad[6] == 'f' && bad[7] == 'i' && bad[8] == 'x' && bad[9] == '\0') + { + say(1, "PASS: mkstemp leaves the invalid template unchanged\n"); + } + else + { + say(2, "FAIL: mkstemp modified the invalid template\n"); + failures++; + } + return failures > 0 ? 1 : 0; +} + +int +main(int argc, char **argv) +{ + if (argc == 2 && argv[1][0] == '-' && argv[1][1] == 'f') + { + /* + * The failure scenario makes the library write errno inside + * mkstemp; leave via the raw syscall so the host cleanup never + * runs after that write (see the banner). + */ + int rc = failure_scenarios(); + + __syscall1(SYS_exit_group, rc); + return rc; /* not reached */ + } + + env_scenarios(); + multibyte_scenarios(); + tempfile_scenarios(); + + if (failures > 0) + { + say(2, "FAILED ("); + say_dec(2, (unsigned long)failures); + say(2, " check(s))\n"); + return 1; + } + say(1, "all env/multibyte/tempfile tests passed\n"); + return 0; +}