diff --git a/include/grp.h b/include/grp.h new file mode 100644 index 0000000..5a5c77e --- /dev/null +++ b/include/grp.h @@ -0,0 +1,103 @@ +#ifndef VLIBC_GRP_H +#define VLIBC_GRP_H + +/* + * vlibc — . + * + * The group database (todo 37). struct group mirrors /etc/group, whose + * records are name:passwd:gid:member,member,... — the member list is a + * comma-separated tail, empty when the group has no members. Everything + * here is POSIX.1-2008 base (Level 1). + * + * The non-reentrant forms getgrnam/getgrgid/getgrent return a pointer to a + * static structure whose contents — including the strings and the member + * array it points at — are overwritten by the next call to any of the + * three. getgrnam/getgrgid open and scan the whole file per call; getgrent + * walks a cursor that setgrent rewinds and endgrent closes. + * + * The _r forms behave like their counterparts: the entry is written + * into caller storage (the gr_mem pointer array and the member name strings + * both live inside the supplied buffer) and the return value is 0 on + * success, ERANGE when the buffer is too small, or 0 with *result NULL when + * no entry matches. errno is never touched: the error number IS the return + * value. + */ + +#include + +#include +#include + +#ifdef __cplusplus +extern "C" { +#endif + +/* One group database entry (/etc/group record). */ +struct group +{ + char *gr_name; /* group name */ + char *gr_passwd; /* encrypted password; usually empty */ + gid_t gr_gid; /* numeric group id */ + char **gr_mem; /* NULL-terminated member-name array */ +}; + +/* + * Look up the first entry whose name matches; NULL when absent. The result + * points into shared static storage valid only until the next grp call. + */ +struct group * +getgrnam(const char *name); + +/* + * Look up the first entry whose gid matches; NULL when absent. Result + * storage is shared with getgrnam/getgrent as described above. + */ +struct group * +getgrgid(gid_t gid); + +/* + * Return the next entry from the group stream, opening it on the first + * call; NULL at end of file. The stream cursor advances per call. + */ +struct group * +getgrent(void); + +/* Rewind the group stream to its first entry. */ +void +setgrent(void); + +/* Close the group stream; a later getgrent reopens from the start. */ +void +endgrent(void); + +int +getgrnam_r(const char *name, struct group *gr, char *buf, size_t bufsize, struct group **result); + +int +getgrgid_r(gid_t gid, struct group *gr, char *buf, size_t bufsize, struct group **result); + +int +getgrent_r(struct group *gr, char *buf, size_t bufsize, struct group **result); + +#if VLIBC_LEVEL_GE(2) +/* Level 2 (muslmimic): XSI. */ + +#include /* FILE */ + +/* + * Return the next entry read from the given open stream, or NULL at end of + * file or on a malformed/unopenable record; the entry is parsed as an + * /etc/group record. Result storage is shared with getgrnam/getgrgid/ + * getgrent as described above, so the stream must not be closed until the + * caller has copied the result. XSI. + */ +struct group * +fgetgrent(FILE *stream); + +#endif /* VLIBC_LEVEL_GE(2) */ + +#ifdef __cplusplus +} +#endif + +#endif /* VLIBC_GRP_H */ diff --git a/include/pwd.h b/include/pwd.h new file mode 100644 index 0000000..095c4cd --- /dev/null +++ b/include/pwd.h @@ -0,0 +1,90 @@ +#ifndef VLIBC_PWD_H +#define VLIBC_PWD_H + +/* + * vlibc — . + * + * The password database (todo 37). struct passwd mirrors /etc/passwd, whose + * records are name:passwd:uid:gid:gecos:dir:shell — seven ':'-separated + * fields. Everything here is POSIX.1-2008 base (Level 1). + * + * The non-reentrant forms getpwnam/getpwuid/getpwent return a pointer to a + * static structure whose contents — including the strings it points at — + * are overwritten by the next call to any of the three, so the result must + * be copied out before the next database call. getpwnam/getpwuid open and + * scan the whole file per call; getpwent walks a cursor that setpwent + * rewinds and endpwent closes. + * + * The getpwnam_r/getpwuid_r/getpwent_r forms write the entry into + * caller-supplied storage (struct, char buffer of bufsize bytes) and + * return 0 on success with *result pointing at the filled struct, or an + * error number as the return value with *result left NULL: ERANGE when the + * buffer is too small for the entry, 0 with *result NULL when no entry + * matches (or the stream is exhausted). errno is never touched by any _r + * function: the error number IS the return value. + */ + +#include + +#include +#include + +#ifdef __cplusplus +extern "C" { +#endif + +/* One password database entry (/etc/passwd record). */ +struct passwd +{ + char *pw_name; /* user's login name */ + char *pw_passwd; /* encrypted password; often "x" with shadow files */ + uid_t pw_uid; /* numeric user id */ + gid_t pw_gid; /* numeric group id */ + char *pw_gecos; /* user information (comment) field */ + char *pw_dir; /* home directory */ + char *pw_shell; /* login shell */ +}; + +/* + * Look up the first entry whose name matches; NULL when absent. The result + * points into shared static storage valid only until the next pwd call. + */ +struct passwd * +getpwnam(const char *name); + +/* + * Look up the first entry whose uid matches; NULL when absent. Result + * storage is shared with getpwnam/getpwent as described above. + */ +struct passwd * +getpwuid(uid_t uid); + +/* + * Return the next entry from the password stream, opening it on the first + * call; NULL at end of file. The stream cursor advances per call. + */ +struct passwd * +getpwent(void); + +/* Rewind the password stream to its first entry. */ +void +setpwent(void); + +/* Close the password stream; a later getpwent reopens from the start. */ +void +endpwent(void); + +int +getpwnam_r(const char *name, struct passwd *pw, char *buf, size_t bufsize, struct passwd **result); + +int +getpwuid_r(uid_t uid, struct passwd *pw, char *buf, size_t bufsize, struct passwd **result); + +int +getpwent_r(struct passwd *pw, char *buf, size_t bufsize, struct passwd **result); + +#ifdef __cplusplus +} +#endif + +#endif /* VLIBC_PWD_H */ diff --git a/include/stdlib.h b/include/stdlib.h index 45ce218..b97b211 100644 --- a/include/stdlib.h +++ b/include/stdlib.h @@ -544,6 +544,21 @@ getsubopt(char **optionp, char *const *tokens, char **valuep); #endif /* VLIBC_LEVEL_GE(2) */ +#if VLIBC_LEVEL_GE(2) + +/* + * Resolve path to an absolute, NUL-terminated canonical pathname with no + * symbolic links, "." or ".." components (todo 38). resolved_path may be + * NULL, in which case the canonical path is returned in a malloc'd + * buffer the caller must free; otherwise it must hold at least PATH_MAX + * bytes. Returns resolved_path (or the malloc'd buffer), or NULL with + * errno set. XSI. + */ +char * +realpath(const char *restrict path, char *restrict resolved_path); + +#endif /* VLIBC_LEVEL_GE(2) */ + #ifdef __cplusplus } #endif diff --git a/src/group/group.c b/src/group/group.c new file mode 100644 index 0000000..eded8e7 --- /dev/null +++ b/src/group/group.c @@ -0,0 +1,565 @@ +#ifdef HAVE_CONFIG_H +#include +#endif + +#include +#include +#include +#include +#include + +#include + +/* + * vlibc — group database (todo 37). + * + * getgrnam/getgrgid scan /etc/group with a fresh fopen per call (no + * persistent cursor); getgrent walks one FILE kept open between calls, + * which setgrent rewinds and endgrent closes. Every line is read with + * getline, one at a time — the whole file is never cached. fgetgrent is + * the XSI variant that parses the next record from an arbitrary caller- + * supplied stream; it is gated at level 2 (whole-level-gate rule: XSI is + * not POSIX.1-2008 base), everything else here is level 1. + * + * Storage model: two static buffers back every non-reentrant result. A + * successful call returns a struct whose string members and gr_mem + * entries point into a private result copy; the getline scan that found + * the match ran in a separate buffer, so a caller may feed the returned + * record's own name back into a later lookup (the + * getgrnam(getgrgid(x)->gr_name) round-trip) without the scan clobbering + * its key. The result copy is replaced by the next call to any of + * getgrnam/getgrgid/getgrent/fgetgrent (or a realloc of the copy), + * matching POSIX result-lifetime rules. The _r forms never use this + * static storage for their result: the caller's struct and buffer + * receive the record, and the caller's *result is how the outcome is + * reported. + * + * Record syntax: name:passwd:gid:member,member,... — the first three + * fields are ':'-separated and the member list is the comma-separated + * tail (empty when the group has no members). Blank or comment-style + * lines and records whose gid is not a decimal number are skipped like + * malformed input. + */ + +/* The database path; not overridable (no NSS, no shadow, POSIX only). */ +#define GRP_PATH "/etc/group" + +/* Non-reentrant result: struct, the private copy it points into, and the + * member pointer array whose entries point into that same copy. */ +static struct group gr_result; +static char *gr_out; +static size_t gr_out_cap; +static char **gr_mem; +static size_t gr_mem_cap; + +/* The getline scan buffer shared by every lookup and stream walk. */ +static char *gr_line; +static size_t gr_cap; + +/* The persistent getgrent stream; NULL when closed. */ +static FILE *gr_stream; + +/* + * Parse the leading digit string of s as an unsigned id. Accepts only + * plain decimal digits and rejects empty fields and overflow; returns 0 + * on success. + */ +static int +gr_parse_id(const char *s, unsigned *out) +{ + unsigned long v = 0; + const char *p; + + if (*s == '\0') + { + return -1; + } + for (p = s; *p != '\0'; p++) + { + if (*p < '0' || *p > '9') + { + return -1; + } + v = v * 10 + (unsigned long)(*p - '0'); + if (v > 0xffffffffUL) + { + return -1; + } + } + *out = (unsigned)v; + return 0; +} + +/* Grow the static member pointer array to hold cnt entries plus the + * terminating NULL; return 0 on success, -1 when realloc fails. */ +static int +gr_reserve_mem(size_t cnt) +{ + char **newmem; + size_t ncap; + + if (gr_mem_cap >= cnt + 1) + { + return 0; + } + ncap = gr_mem_cap == 0 ? 8 : gr_mem_cap * 2; + while (ncap < cnt + 1) + { + ncap *= 2; + } + newmem = (char **)realloc((void *)gr_mem, ncap * sizeof(*newmem)); + if (newmem == NULL) + { + return -1; + } + gr_mem = newmem; + gr_mem_cap = ncap; + return 0; +} + +/* + * Split the comma-separated member tail in place: every name becomes a + * NUL-terminated substring of line, member[i] points at the i-th one and + * member[cnt] is NULL. An empty tail yields a single NULL entry. The + * parsed group's gr_mem is set to the shared member array. + */ +static int +gr_split_members(char *tail, struct group *gr) +{ + size_t cnt = 0; + char *p; + + if (*tail != '\0') + { + cnt = 1; + for (p = tail; *p != '\0'; p++) + { + if (*p == ',') + { + cnt++; + } + } + } + if (gr_reserve_mem(cnt) != 0) + { + return -1; + } + p = tail; + for (size_t i = 0; i < cnt; i++) + { + char *comma = strchr(p, ','); + + if (comma != NULL) + { + *comma = '\0'; + } + gr_mem[i] = p; + p = comma == NULL ? p + strlen(p) : comma + 1; + } + gr_mem[cnt] = NULL; + gr->gr_mem = gr_mem; + return 0; +} + +/* + * Split the line (newline already stripped) into its fields in place, + * replacing the first three ':' separators with NULs. On success the + * struct fields point into line and the member list has been split in + * place, and 0 is returned; a record with fewer than three separators or + * a non-numeric gid yields -1. + */ +static int +gr_split(struct group *gr, char *line) +{ + char *fields[4]; + char *p; + unsigned gid; + int i; + + p = line; + for (i = 0; i < 3; i++) + { + fields[i] = p; + p = strchr(p, ':'); + if (p == NULL) + { + return -1; + } + *p = '\0'; + p++; + } + fields[3] = p; + if (gr_parse_id(fields[2], &gid) != 0) + { + return -1; + } + if (gr_split_members(fields[3], gr) != 0) + { + return -1; + } + gr->gr_name = fields[0]; + gr->gr_passwd = fields[1]; + gr->gr_gid = (gid_t)gid; + return 0; +} + +/* Strip a trailing newline (and CR) from the line just read by getline. */ +static void +gr_chomp(char *line, ssize_t len) +{ + while (len > 0 && (line[len - 1] == '\n' || line[len - 1] == '\r')) + { + line[--len] = '\0'; + } +} + +/* + * Make line — a chomped, already-split record in the scan buffer — the + * non-reentrant result: copy it into the private result buffer and + * re-split it there, so the returned struct's strings and member + * pointers reference storage the scanner never touches again. Returns + * the result struct, or NULL if the result copy could not be grown. + */ +static struct group * +gr_publish(char *line) +{ + size_t len = strlen(line) + 1; + + if (len > gr_out_cap) + { + char *nb = (char *)realloc(gr_out, len); + + if (nb == NULL) + { + return NULL; + } + gr_out = nb; + gr_out_cap = len; + } + strcpy(gr_out, line); + if (gr_split(&gr_result, gr_out) != 0) + { + return NULL; + } + return &gr_result; +} + +/* Match predicate over a parsed record, keyed by name or gid. */ +typedef int (*gr_matcher)(const struct group *gr, const void *key); + +static int +gr_match_name(const struct group *gr, const void *key) +{ + return strcmp(gr->gr_name, (const char *)key) == 0; +} + +static int +gr_match_gid(const struct group *gr, const void *key) +{ + return gr->gr_gid == *(const gid_t *)key; +} + +/* + * Non-reentrant lookup core: open the database, scan with getline until + * the predicate matches, and hand back a pointer into the shared static + * line buffer. NULL means "no such entry" (or an unopenable database). + */ +static struct group * +gr_find(gr_matcher match, const void *key) +{ + FILE *f = fopen(GRP_PATH, "r"); + struct group cur; + ssize_t got; + + if (f == NULL) + { + return NULL; + } + for (;;) + { + got = getline(&gr_line, &gr_cap, f); + if (got < 0) + { + break; + } + gr_chomp(gr_line, got); + if (gr_split(&cur, gr_line) == 0 && match(&cur, key)) + { + (void)fclose(f); + return gr_publish(gr_line); + } + } + (void)fclose(f); + return NULL; +} + +/* + * Reentrant serializer: copy the parsed record into the caller's struct + * and buffer. The string bytes go first; the NULL-terminated member + * pointer array follows on a pointer-aligned boundary. Returns 0 on + * success, or ERANGE (as the POSIX _r functions return, never touching + * errno) when the record needs more than bufsize bytes. + */ +static int +gr_store(const struct group *in, struct group *gr, char *buf, size_t bufsize) +{ + size_t nmem = 0; + size_t strneed; + size_t ptoff; + char *s; + char **mem; + size_t i; + + while (in->gr_mem != NULL && in->gr_mem[nmem] != NULL) + { + nmem++; + } + strneed = strlen(in->gr_name) + 1; + strneed += strlen(in->gr_passwd) + 1; + for (i = 0; i < nmem; i++) + { + strneed += strlen(in->gr_mem[i]) + 1; + } + ptoff = (strneed + sizeof(char *) - 1) & ~(sizeof(char *) - 1); + if (bufsize < ptoff + (nmem + 1) * sizeof(char *)) + { + return ERANGE; + } + gr->gr_gid = in->gr_gid; + mem = (char **)(void *)(buf + ptoff); + gr->gr_mem = mem; + s = buf; + gr->gr_name = s; + s += strlen(in->gr_name) + 1; + gr->gr_passwd = s; + s += strlen(in->gr_passwd) + 1; + for (i = 0; i < nmem; i++) + { + size_t len = strlen(in->gr_mem[i]) + 1; + + gr->gr_mem[i] = s; + s += len; + } + gr->gr_mem[nmem] = NULL; + strcpy(gr->gr_name, in->gr_name); + strcpy(gr->gr_passwd, in->gr_passwd); + for (i = 0; i < nmem; i++) + { + size_t len = strlen(in->gr_mem[i]) + 1; + + __builtin_memcpy(gr->gr_mem[i], in->gr_mem[i], len); + } + return 0; +} + +struct group * +getgrnam(const char *name) +{ + if (name == NULL) + { + return NULL; + } + return gr_find(gr_match_name, name); +} + +struct group * +getgrgid(gid_t gid) +{ + return gr_find(gr_match_gid, &gid); +} + +struct group * +getgrent(void) +{ + struct group cur; + ssize_t got; + + if (gr_stream == NULL) + { + gr_stream = fopen(GRP_PATH, "r"); + if (gr_stream == NULL) + { + return NULL; + } + } + for (;;) + { + got = getline(&gr_line, &gr_cap, gr_stream); + if (got < 0) + { + return NULL; + } + gr_chomp(gr_line, got); + if (gr_split(&cur, gr_line) == 0) + { + return gr_publish(gr_line); + } + } +} + +void +setgrent(void) +{ + if (gr_stream != NULL) + { + rewind(gr_stream); + } +} + +void +endgrent(void) +{ + if (gr_stream != NULL) + { + (void)fclose(gr_stream); + gr_stream = NULL; + } +} + +/* + * Reentrant lookup core: scan for the first matching record and serialize + * it into the caller's storage. Returns 0 with *result pointing at the + * filled struct on success, 0 with *result NULL when no record matches, + * ERANGE when the record does not fit bufsize, and EINVAL for a NULL + * result pointer. + */ +static int +gr_find_r(gr_matcher match, const void *key, struct group *gr, char *buf, size_t bufsize, + struct group **result) +{ + FILE *f; + struct group cur; + ssize_t got; + int rc; + + if (result == NULL || gr == NULL) + { + return EINVAL; + } + if (buf == NULL && bufsize != 0) + { + return EINVAL; + } + *result = NULL; + f = fopen(GRP_PATH, "r"); + if (f == NULL) + { + return 0; + } + for (;;) + { + got = getline(&gr_line, &gr_cap, f); + if (got < 0) + { + break; + } + gr_chomp(gr_line, got); + if (gr_split(&cur, gr_line) != 0 || !match(&cur, key)) + { + continue; + } + rc = gr_store(&cur, gr, buf, bufsize); + (void)fclose(f); + if (rc == 0) + { + *result = gr; + } + return rc; + } + (void)fclose(f); + return 0; +} + +int +getgrnam_r(const char *name, struct group *gr, char *buf, size_t bufsize, struct group **result) +{ + if (name == NULL) + { + if (result != NULL) + { + *result = NULL; + } + return EINVAL; + } + return gr_find_r(gr_match_name, name, gr, buf, bufsize, result); +} + +int +getgrgid_r(gid_t gid, struct group *gr, char *buf, size_t bufsize, struct group **result) +{ + return gr_find_r(gr_match_gid, &gid, gr, buf, bufsize, result); +} + +int +getgrent_r(struct group *gr, char *buf, size_t bufsize, struct group **result) +{ + struct group cur; + ssize_t got; + int rc; + + if (result == NULL || gr == NULL) + { + return EINVAL; + } + if (buf == NULL && bufsize != 0) + { + return EINVAL; + } + *result = NULL; + if (gr_stream == NULL) + { + gr_stream = fopen(GRP_PATH, "r"); + if (gr_stream == NULL) + { + return 0; + } + } + for (;;) + { + got = getline(&gr_line, &gr_cap, gr_stream); + if (got < 0) + { + return 0; + } + gr_chomp(gr_line, got); + if (gr_split(&cur, gr_line) != 0) + { + continue; + } + rc = gr_store(&cur, gr, buf, bufsize); + if (rc == 0) + { + *result = gr; + } + return rc; + } +} + +#if VLIBC_LEVEL_GE(2) + +struct group * +fgetgrent(FILE *stream) +{ + struct group cur; + ssize_t got; + + if (stream == NULL) + { + return NULL; + } + for (;;) + { + got = getline(&gr_line, &gr_cap, stream); + if (got < 0) + { + return NULL; + } + gr_chomp(gr_line, got); + if (gr_split(&cur, gr_line) == 0) + { + return gr_publish(gr_line); + } + } +} + +#endif /* VLIBC_LEVEL_GE(2) */ diff --git a/src/passwd/passwd.c b/src/passwd/passwd.c new file mode 100644 index 0000000..0227aeb --- /dev/null +++ b/src/passwd/passwd.c @@ -0,0 +1,433 @@ +#ifdef HAVE_CONFIG_H +#include +#endif + +#include +#include +#include +#include +#include + +#include + +/* + * vlibc — password database (todo 37). + * + * getpwnam/getpwuid scan /etc/passwd with a fresh fopen per call (no + * persistent cursor); getpwent walks one FILE kept open between calls, + * which setpwent rewinds and endpwent closes. Every line is read with + * getline, one at a time — the whole file is never cached. + * + * Storage model: two static buffers back every non-reentrant result. A + * successful call returns a struct whose string members point into a + * private result copy; the getline scan that produced the match ran in a + * separate buffer, so a caller may feed the returned record's own name + * back into a later lookup (the getpwnam(getpwuid(x)->pw_name) + * round-trip) without the scan clobbering its key. The result copy is + * replaced by the next call to any of getpwnam/getpwuid/getpwent (or a + * realloc of the copy), matching POSIX result-lifetime rules. The _r + * forms never use this static storage for their result: the caller's + * struct and buffer receive the record, and the caller's *result is how + * the outcome is reported. + * + * Record syntax: seven ':'-separated fields, name:passwd:uid:gid:gecos: + * dir:shell. The last field (shell) keeps any further ':' characters as + * data. Blank or comment-style lines and records whose uid/gid are not + * decimal numbers are skipped like malformed input. + */ + +/* The database path; not overridable (no NSS, no shadow, POSIX only). */ +#define PWD_PATH "/etc/passwd" + +/* Non-reentrant result: struct plus the private copy it points into. */ +static struct passwd pw_result; +static char *pw_out; +static size_t pw_out_cap; + +/* The getline scan buffer shared by every lookup and stream walk. */ +static char *pw_line; +static size_t pw_cap; + +/* The persistent getpwent stream; NULL when closed. */ +static FILE *pw_stream; + +/* + * Parse the leading digit string of s as an unsigned id. Accepts only + * plain decimal digits and rejects empty fields and overflow; returns 0 + * on success. + */ +static int +pw_parse_id(const char *s, unsigned *out) +{ + unsigned long v = 0; + const char *p; + + if (*s == '\0') + { + return -1; + } + for (p = s; *p != '\0'; p++) + { + if (*p < '0' || *p > '9') + { + return -1; + } + v = v * 10 + (unsigned long)(*p - '0'); + if (v > 0xffffffffUL) + { + return -1; + } + } + *out = (unsigned)v; + return 0; +} + +/* + * Split the line (newline already stripped) into its seven fields in + * place, replacing the first six ':' separators with NULs. On success the + * struct fields point into line and 0 is returned; a record with fewer + * than six separators or a non-numeric uid/gid yields -1. + */ +static int +pw_split(struct passwd *pw, char *line) +{ + char *fields[7]; + char *p; + int i; + + p = line; + for (i = 0; i < 6; i++) + { + fields[i] = p; + p = strchr(p, ':'); + if (p == NULL) + { + return -1; + } + *p = '\0'; + p++; + } + fields[6] = p; + if (pw_parse_id(fields[2], &pw->pw_uid) != 0 || pw_parse_id(fields[3], &pw->pw_gid) != 0) + { + return -1; + } + pw->pw_name = fields[0]; + pw->pw_passwd = fields[1]; + pw->pw_gecos = fields[4]; + pw->pw_dir = fields[5]; + pw->pw_shell = fields[6]; + return 0; +} + +/* Strip a trailing newline (and CR) from the line just read by getline. */ +static void +pw_chomp(char *line, ssize_t len) +{ + while (len > 0 && (line[len - 1] == '\n' || line[len - 1] == '\r')) + { + line[--len] = '\0'; + } +} + +/* + * Make line — a chomped, already-split record in the scan buffer — the + * non-reentrant result: copy it into the private result buffer and + * re-split it there, so the returned struct's strings point into storage + * the scanner never touches again. Returns the result struct, or NULL if + * the result copy could not be grown. + */ +static struct passwd * +pw_publish(char *line) +{ + size_t len = strlen(line) + 1; + + if (len > pw_out_cap) + { + char *nb = (char *)realloc(pw_out, len); + + if (nb == NULL) + { + return NULL; + } + pw_out = nb; + pw_out_cap = len; + } + strcpy(pw_out, line); + if (pw_split(&pw_result, pw_out) != 0) + { + return NULL; + } + return &pw_result; +} + +/* Match predicate over a parsed record, keyed by name or uid. */ +typedef int (*pw_matcher)(const struct passwd *pw, const void *key); + +static int +pw_match_name(const struct passwd *pw, const void *key) +{ + return strcmp(pw->pw_name, (const char *)key) == 0; +} + +static int +pw_match_uid(const struct passwd *pw, const void *key) +{ + return pw->pw_uid == *(const uid_t *)key; +} + +/* + * Non-reentrant lookup core: open the database, scan with getline until + * the predicate matches, and hand back a pointer into the shared static + * line buffer. NULL means "no such entry" (or an unopenable database). + */ +static struct passwd * +pw_find(pw_matcher match, const void *key) +{ + FILE *f = fopen(PWD_PATH, "r"); + struct passwd cur; + ssize_t got; + + if (f == NULL) + { + return NULL; + } + for (;;) + { + got = getline(&pw_line, &pw_cap, f); + if (got < 0) + { + break; + } + pw_chomp(pw_line, got); + if (pw_split(&cur, pw_line) == 0 && match(&cur, key)) + { + (void)fclose(f); + return pw_publish(pw_line); + } + } + (void)fclose(f); + return NULL; +} + +/* + * Reentrant serializer: copy the parsed record into the caller's struct + * and buffer. Returns 0 on success, or ERANGE (as the POSIX _r functions + * return, never touching errno) when the record needs more than bufsize + * bytes. + */ +static int +pw_store(const struct passwd *in, struct passwd *pw, char *buf, size_t bufsize) +{ + const char *fields[5] = { + in->pw_name, in->pw_passwd, in->pw_gecos, in->pw_dir, in->pw_shell, + }; + char *p = buf; + size_t need = 0; + size_t i; + + for (i = 0; i < 5; i++) + { + need += strlen(fields[i]) + 1; + } + if (bufsize < need) + { + return ERANGE; + } + pw->pw_uid = in->pw_uid; + pw->pw_gid = in->pw_gid; + pw->pw_name = p; + p += strlen(pw->pw_name) + 1; + pw->pw_passwd = p; + p += strlen(pw->pw_passwd) + 1; + pw->pw_gecos = p; + p += strlen(pw->pw_gecos) + 1; + pw->pw_dir = p; + p += strlen(pw->pw_dir) + 1; + pw->pw_shell = p; + strcpy(pw->pw_shell, in->pw_shell); + return 0; +} + +struct passwd * +getpwnam(const char *name) +{ + if (name == NULL) + { + return NULL; + } + return pw_find(pw_match_name, name); +} + +struct passwd * +getpwuid(uid_t uid) +{ + return pw_find(pw_match_uid, &uid); +} + +struct passwd * +getpwent(void) +{ + struct passwd cur; + ssize_t got; + + if (pw_stream == NULL) + { + pw_stream = fopen(PWD_PATH, "r"); + if (pw_stream == NULL) + { + return NULL; + } + } + for (;;) + { + got = getline(&pw_line, &pw_cap, pw_stream); + if (got < 0) + { + return NULL; + } + pw_chomp(pw_line, got); + if (pw_split(&cur, pw_line) == 0) + { + return pw_publish(pw_line); + } + } +} + +void +setpwent(void) +{ + if (pw_stream != NULL) + { + rewind(pw_stream); + } +} + +void +endpwent(void) +{ + if (pw_stream != NULL) + { + (void)fclose(pw_stream); + pw_stream = NULL; + } +} + +/* + * Reentrant lookup core: scan for the first matching record and serialize + * it into the caller's storage. Returns 0 with *result pointing at the + * filled struct on success, 0 with *result NULL when no record matches, + * ERANGE when the record does not fit bufsize, and EINVAL for a NULL + * result pointer. + */ +static int +pw_find_r(pw_matcher match, const void *key, struct passwd *pw, char *buf, size_t bufsize, + struct passwd **result) +{ + FILE *f; + struct passwd cur; + ssize_t got; + int rc; + + if (result == NULL || pw == NULL) + { + return EINVAL; + } + if (buf == NULL && bufsize != 0) + { + return EINVAL; + } + *result = NULL; + f = fopen(PWD_PATH, "r"); + if (f == NULL) + { + return 0; + } + for (;;) + { + got = getline(&pw_line, &pw_cap, f); + if (got < 0) + { + break; + } + pw_chomp(pw_line, got); + if (pw_split(&cur, pw_line) != 0 || !match(&cur, key)) + { + continue; + } + rc = pw_store(&cur, pw, buf, bufsize); + (void)fclose(f); + if (rc == 0) + { + *result = pw; + } + return rc; + } + (void)fclose(f); + return 0; +} + +int +getpwnam_r(const char *name, struct passwd *pw, char *buf, size_t bufsize, struct passwd **result) +{ + if (name == NULL) + { + if (result != NULL) + { + *result = NULL; + } + return EINVAL; + } + return pw_find_r(pw_match_name, name, pw, buf, bufsize, result); +} + +int +getpwuid_r(uid_t uid, struct passwd *pw, char *buf, size_t bufsize, struct passwd **result) +{ + return pw_find_r(pw_match_uid, &uid, pw, buf, bufsize, result); +} + +int +getpwent_r(struct passwd *pw, char *buf, size_t bufsize, struct passwd **result) +{ + struct passwd cur; + ssize_t got; + int rc; + + if (result == NULL || pw == NULL) + { + return EINVAL; + } + if (buf == NULL && bufsize != 0) + { + return EINVAL; + } + *result = NULL; + if (pw_stream == NULL) + { + pw_stream = fopen(PWD_PATH, "r"); + if (pw_stream == NULL) + { + return 0; + } + } + for (;;) + { + got = getline(&pw_line, &pw_cap, pw_stream); + if (got < 0) + { + return 0; + } + pw_chomp(pw_line, got); + if (pw_split(&cur, pw_line) != 0) + { + continue; + } + rc = pw_store(&cur, pw, buf, bufsize); + if (rc == 0) + { + *result = pw; + } + return rc; + } +} diff --git a/tests/test_pwdgrp.c b/tests/test_pwdgrp.c new file mode 100644 index 0000000..cf0c898 --- /dev/null +++ b/tests/test_pwdgrp.c @@ -0,0 +1,343 @@ +/* + * vlibc — pwd.h/grp.h test (todo 37). + * + * Exercises the password and group databases end to end without assuming + * anything about their CONTENTS: every lookup is asserted either to + * return a sane record (string fields non-NULL, ids echo the key) or + * NULL, and every stream walk is asserted only to terminate cleanly. The + * one environment precondition is that /etc/passwd and /etc/group EXIST + * and are readable; when either is absent the whole test skips (rc 0), + * detected through a raw SYS_faccessat so no vlibc errno slot is touched + * on the skip path. + * + * Scenarios (all content-independent): + * + * 1. current-uid: getpwuid(getuid()) is NULL or a record whose pw_uid + * echoes the key and whose pw_name is set; when a record comes back, + * getpwnam(pw_name) finds it again (name round-trip). + * 2. current-gid: the same pair against the group database. + * 3. stream walk: setpwent/getpwent/endpwent; two consecutive full + * walks return the same count (the stream really rewinds), every + * record is sane, and the walk terminates. + * 4. group members: every parsed record has a non-NULL gr_mem array + * (the empty list is a single NULL entry), and member arrays + * terminate. + * 5. _r forms: getpwuid_r/getgrgid_r into a 4096-byte buffer return 0 + * and either *result == NULL (key absent) or a filled struct echoing + * the key; getpwent_r/getgrent_r likewise fill one record. + * 6. NULL keys: getpwnam(NULL)/getgrnam(NULL) return NULL (defensive + * contract, no crash). + * 7. -f mode: impossible keys (uid/gid 2147483647, a name no real + * database carries) come back NULL without crashing, then the test + * leaves via a raw SYS_exit_group (house pattern: after a vlibc + * errno-capable call the host TCB may be dirty). + * 8. fgetgrent (level 2 only): parse one record from an explicitly + * fopen'd stream. + * + * Host headers are never included; // are + * vlibc's own (-Iinclude shadows the system copies). getuid/getgid come + * from src/process/uid.c. + */ + +#include +#include +#include + +#include + +/* Local kernel-ABI constants for the skip probe (no public header pulled). */ +#define PG_AT_FDCWD (-100) +#define PG_R_OK 4 + +/* Both databases readable (the only environment assumption). */ +static int +pg_db_present(void) +{ + return __syscall4(SYS_faccessat, PG_AT_FDCWD, (long)"/etc/passwd", PG_R_OK, 0) == 0 && + __syscall4(SYS_faccessat, PG_AT_FDCWD, (long)"/etc/group", PG_R_OK, 0) == 0; +} + +static int +test_current_uid(void) +{ + uid_t uid = getuid(); + struct passwd *p = getpwuid(uid); + + if (p == NULL) + { + return 0; /* key absent from the database: acceptable */ + } + TEST_ASSERT_TRUE(p->pw_name != NULL); + TEST_ASSERT_EQ((unsigned long)p->pw_uid, (unsigned long)uid); + if (p->pw_name != NULL) + { + struct passwd *q = getpwnam(p->pw_name); + + TEST_ASSERT_TRUE(q != NULL); + if (q != NULL) + { + TEST_ASSERT_EQ((unsigned long)q->pw_uid, (unsigned long)uid); + } + } + return 0; +} + +static int +test_current_gid(void) +{ + gid_t gid = getgid(); + struct group *g = getgrgid(gid); + + if (g == NULL) + { + return 0; /* key absent from the database: acceptable */ + } + TEST_ASSERT_TRUE(g->gr_name != NULL); + TEST_ASSERT_EQ((unsigned long)g->gr_gid, (unsigned long)gid); + if (g->gr_name != NULL) + { + struct group *h = getgrnam(g->gr_name); + + TEST_ASSERT_TRUE(h != NULL); + if (h != NULL) + { + TEST_ASSERT_EQ((unsigned long)h->gr_gid, (unsigned long)gid); + } + } + return 0; +} + +/* Count the records of one full pwd walk, sanity-checking each. */ +static unsigned long +pg_pwd_walk_count(void) +{ + struct passwd *p; + unsigned long n = 0; + + setpwent(); + while ((p = getpwent()) != NULL) + { + TEST_ASSERT_TRUE(p->pw_name != NULL); + if (++n > 100000UL) + { + break; /* runaway guard: the walk must terminate */ + } + } + endpwent(); + return n; +} + +static int +test_pwd_stream(void) +{ + unsigned long first = pg_pwd_walk_count(); + unsigned long second = pg_pwd_walk_count(); + + /* The walk terminates and setpwent really rewinds (0 == 0 for an + * empty database is still a valid equality). */ + TEST_ASSERT_TRUE(first < 100000UL); + TEST_ASSERT_EQ(first, second); + + /* After endpwent a fresh getpwent reopens from the start: the result + * is the first record, or NULL only when the database is empty. */ + if (first != 0) + { + struct passwd *p = getpwent(); + + TEST_ASSERT_TRUE(p != NULL); + endpwent(); + } + return 0; +} + +/* Every parsed group has a terminating gr_mem array; empty lists appear + * as a single NULL entry, so gr_mem itself is never NULL. */ +static int +test_group_members(void) +{ + struct group *g; + unsigned long n = 0; + + setgrent(); + while ((g = getgrent()) != NULL) + { + char **m; + unsigned long members = 0; + + TEST_ASSERT_TRUE(g->gr_name != NULL); + TEST_ASSERT_TRUE(g->gr_mem != NULL); + for (m = g->gr_mem; m != NULL && *m != NULL; m++) + { + TEST_ASSERT_TRUE(**m != '\0'); + if (++members > 100000UL) + { + break; /* runaway guard: member arrays must terminate */ + } + } + TEST_ASSERT_TRUE(members < 100000UL); + if (++n > 100000UL) + { + break; + } + } + endgrent(); + TEST_ASSERT_TRUE(n < 100000UL); + return 0; +} + +static int +test_pwd_r(void) +{ + struct passwd pw; + struct passwd *res = NULL; + char buf[4096]; + int rc = getpwuid_r(getuid(), &pw, buf, sizeof buf, &res); + + TEST_ASSERT_EQ(rc, 0); + TEST_ASSERT_TRUE(res == NULL || res == &pw); + if (res == &pw) + { + TEST_ASSERT_EQ((unsigned long)pw.pw_uid, (unsigned long)getuid()); + TEST_ASSERT_TRUE(pw.pw_name != NULL); + } + return 0; +} + +static int +test_grp_r(void) +{ + struct group gr; + struct group *res = NULL; + char buf[4096]; + int rc = getgrgid_r(getgid(), &gr, buf, sizeof buf, &res); + + TEST_ASSERT_EQ(rc, 0); + TEST_ASSERT_TRUE(res == NULL || res == &gr); + if (res == &gr) + { + TEST_ASSERT_EQ((unsigned long)gr.gr_gid, (unsigned long)getgid()); + TEST_ASSERT_TRUE(gr.gr_name != NULL); + TEST_ASSERT_TRUE(gr.gr_mem != NULL); + } + return 0; +} + +static int +test_null_keys(void) +{ + TEST_ASSERT_TRUE(getpwnam(NULL) == NULL); + TEST_ASSERT_TRUE(getgrnam(NULL) == NULL); + return 0; +} + +#if VLIBC_LEVEL_GE(2) + +static int +test_fgetgrent(void) +{ + FILE *f = fopen("/etc/group", "r"); + struct group *g; + + if (f == NULL) + { + return 0; /* opened by the caller, closed by the caller */ + } + g = fgetgrent(f); + if (g != NULL) + { + TEST_ASSERT_TRUE(g->gr_name != NULL); + TEST_ASSERT_TRUE(g->gr_mem != NULL); + } + (void)fclose(f); + return 0; +} + +#endif /* VLIBC_LEVEL_GE(2) */ + +static const struct vlibc_test tests[] = { + {"current-uid", test_current_uid}, + {"current-gid", test_current_gid}, + {"pwd-stream", test_pwd_stream}, + {"group-members", test_group_members}, + {"pwd-r", test_pwd_r}, + {"grp-r", test_grp_r}, + {"null-keys", test_null_keys}, +#if VLIBC_LEVEL_GE(2) + {"fgetgrent", test_fgetgrent}, +#endif +}; + +/* + * Own main (not TEST_MAIN): supports the -f failure mode. Everything else + * follows the TEST_MAIN contract — same output shape, 0 on all-pass. + */ +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') + { + /* Impossible keys must come back NULL without crashing. */ + int before = vlibc_test_failures; + uid_t uid = (uid_t)2147483647UL; + gid_t gid = (gid_t)2147483647UL; + + vlibc_test_say(1, "RUN missing-lookups: "); + TEST_ASSERT_TRUE(getpwuid(uid) == NULL); + TEST_ASSERT_TRUE(getgrgid(gid) == NULL); + TEST_ASSERT_TRUE(getpwnam("vlibc-no-such-user-xyz") == NULL); + TEST_ASSERT_TRUE(getgrnam("vlibc-no-such-group-xyz") == 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"); + } + + /* Raw exit: the NULL-return paths may have written the library's + * errno slot, which collides with the host TCB (house pattern). */ + __syscall1(SYS_exit_group, vlibc_test_failures == before ? 0 : 1); + /* not reached */ + } + + if (!pg_db_present()) + { + vlibc_test_say(1, "SKIP: /etc/passwd or /etc/group absent, nothing to look up\n"); + __syscall1(SYS_exit_group, 0); + /* 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"); + + return passed == count ? 0 : 1; +}