From a2f7c33601adb72cd27f35974e8acb1bfb1bf570 Mon Sep 17 00:00:00 2001 From: huntedbytheirs Date: Sat, 5 Sep 2026 17:18:18 -0400 Subject: [PATCH] feat(stdio): getline/getdelim/stream locking --- include/stdio.h | 82 ++++++ src/stdio/fmemopen.c | 503 +++++++++++++++++++++++++++++++++ src/stdio/getline.c | 142 ++++++++++ src/stdio/locking.c | 178 ++++++++++++ src/stdio/stdio.c | 47 +++- src/stdio/stdio_impl.h | 75 ++++- tests/test_getline.c | 611 +++++++++++++++++++++++++++++++++++++++++ 7 files changed, 1625 insertions(+), 13 deletions(-) create mode 100644 src/stdio/fmemopen.c create mode 100644 src/stdio/getline.c create mode 100644 src/stdio/locking.c create mode 100644 tests/test_getline.c diff --git a/include/stdio.h b/include/stdio.h index ab20c2a..18c5421 100644 --- a/include/stdio.h +++ b/include/stdio.h @@ -347,4 +347,86 @@ int vasprintf(char **restrict strp, const char *restrict format, va_list ap); #endif +/* line input, stream locking, and memory streams (todo 18) */ + +/* + * Read one line from stream into a malloc'd, NUL-terminated buffer. On the + * first call *lineptr may be NULL (a buffer is allocated); *n is the buffer + * capacity and grows as needed. The newline is kept in the output; embedded + * NUL bytes are preserved (the length comes from the return value, not + * strlen). Returns the number of bytes read including the delimiter, or -1 + * at end of file (nothing read) or on error. + */ +ssize_t +getline(char **restrict lineptr, size_t *restrict n, FILE *restrict stream); + +/* + * Like getline, but reads up to the given delimiter byte (which is kept in + * the output). Reading stops at end of file without the delimiter; a partial + * final line is still returned with its byte count. + */ +ssize_t +getdelim(char **restrict lineptr, size_t *restrict n, int delim, FILE *restrict stream); + +/* Write s to stdout followed by a newline. Returns a non-negative value, + * or EOF on error. */ +int +puts(const char *s); + +/* + * Per-stream advisory locks (POSIX.1-2008 base). flockfile is recursive: + * the owning thread may lock the same stream any number of times, and + * funlockfile must be called once per successful flockfile. ftrylockfile + * acquires without blocking: 0 on success, nonzero when the stream is + * already locked. Single-threaded today; the lock fields become real + * blocking locks when the thread runtime lands. + */ +void +flockfile(FILE *stream); + +int +ftrylockfile(FILE *stream); + +void +funlockfile(FILE *stream); + +/* + * Unlocked character I/O: the stream must already be locked by the caller + * (see flockfile). Identical to fgetc/fputc over stdin/stdout without + * taking the per-stream lock. + */ +int +getc_unlocked(FILE *stream); + +int +getchar_unlocked(void); + +int +putc_unlocked(int c, FILE *stream); + +int +putchar_unlocked(int c); + +/* + * Open a stream over the memory region buf of size bytes. With buf == NULL + * the stream allocates and owns a growable buffer of its own that is freed + * on fclose. Modes are fopen-like ('r', 'w', 'a', optional '+' and 'b', + * where 'b' is accepted but on Linux only disables the string semantics): + * 'w' truncates the current length to zero, 'a' positions at the end of the + * data currently in the buffer (the first NUL byte in string mode), and + * writing past the buffer size is an error. Returns the stream, or NULL + * with errno set. + */ +FILE * +fmemopen(void *buf, size_t size, const char *mode); + +/* + * Open a write-only dynamic memory stream. The stream owns a growable + * buffer; after fflush or fclose the buffer is NUL-terminated at the current + * data length, *ptr points at it (it may have moved), and *sizeloc holds the + * length. The caller releases the buffer with free() after fclose. + */ +FILE * +open_memstream(char **ptr, size_t *sizeloc); + #endif /* VLIBC_STDIO_H */ diff --git a/src/stdio/fmemopen.c b/src/stdio/fmemopen.c new file mode 100644 index 0000000..46685ea --- /dev/null +++ b/src/stdio/fmemopen.c @@ -0,0 +1,503 @@ +#ifdef HAVE_CONFIG_H +#include +#endif + +#include +#include +#include +#include + +#include "../internal/malloc.h" +#include "stdio_impl.h" + +/* + * vlibc — memory streams (todo 18): fmemopen and open_memstream. + * + * Both build a normal buffered vlibc_FILE whose backing "device" is a + * memory region instead of a descriptor: the FILE's ->mem points at the + * backend struct below, and the stdio core (stdio_flush/stdio_refill/ + * stdio_discard_read/fseeko/fclose) diverts its descriptor operations to + * the stdio_mem_* hooks at the bottom of this file. The buffered data + * path (cache buffer, mode switches, position invariant) is unchanged; + * only the device is synthetic. + * + * Backend state: `data` is the region, `size` its capacity, `len` the + * current data length (bytes at or beyond len read as end of file), and + * `dpos` the device-head position — the analog of the kernel file offset, + * which for a real stream the kernel tracks and stdio re-anchors around. + * Keeping dpos explicit (rather than deriving it from FILE state) is what + * keeps memory streams exact even when reads span several cache refills. + * + * Two flavors: + * + * - fmemopen(buf, size, mode) over a caller buffer. The stream does NOT + * own buf unless buf is NULL, in which case it allocates a growable + * zeroed region of its own and releases it on fclose. Modes follow + * C23 7.23.5.6: 'r' reads the whole region (length = size), 'w' + * truncates the current length to zero, 'a' starts at the current + * data length (the first NUL byte in string mode, i.e. without 'b'). + * A '+' enables both directions. Writes past a fixed buffer's size + * fail (partial-write discipline like a full disk). + * - open_memstream(ptr, sizeloc): a write-only stream over a growable + * buffer it owns. After fflush or fclose the buffer is NUL-terminated + * at the data length, *ptr names it (realloc may move it), and + * *sizeloc holds the length; the caller frees *ptr after fclose. + * + * Limits: memory streams are not registered with the stdio open_list + * (that registry is private to stdio.c), so fflush(NULL) does not reach + * them; an explicit fflush(stream) works. They are also not thread-safe + * beyond what the single-threaded FILE core already is. + */ + +/* Initial capacity for an open_memstream buffer. */ +#define OMS_INITIAL 128 + +struct vlibc_memstream +{ + FILE f; /* embedded stream; the public FILE * */ + unsigned char *data; /* backing region */ + size_t size; /* capacity of data */ + size_t len; /* current data length (excludes any NUL) */ + size_t dpos; /* device-head position in the region */ + int fixed; /* writes past size fail (caller buffer) */ + int own_data; /* release data on close (fmemopen NULL buf) */ + int string_mode; /* no 'b': keep a NUL terminator at len */ + char **user_ptr; /* open_memstream: caller's buffer slot */ + size_t *user_len; /* open_memstream: caller's length slot */ +}; + +/* + * Build the embedded FILE: cache buffer, flags, cursors — the same shape + * stdio_alloc_file produces for a descriptor stream, but with fd == -1 + * and ->mem pointing back at the backend. Not registered in open_list + * (see the banner). The caller fills the region fields and ->pos. + */ +static struct vlibc_memstream * +ms_alloc(int m) +{ + struct vlibc_memstream *ms = (struct vlibc_memstream *)__libc_malloc(sizeof(*ms)); + + if (ms == NULL) + { + return NULL; + } + ms->f.buf = (unsigned char *)__libc_malloc(BUFSIZ); + if (ms->f.buf == NULL) + { + __libc_free(ms); + return NULL; + } + ms->f.buf_size = BUFSIZ; + ms->f.rpos = ms->f.rstop = ms->f.buf; + ms->f.wpos = ms->f.buf; + ms->f.wstop = ms->f.buf + BUFSIZ; + ms->f.fd = -1; + ms->f.flags = m | F_OWNBUF | F_HEAP; + if ((m & F_READ) && (m & F_WRITE)) + { + ms->f.flags |= F_RDWR; + } + ms->f.pos = 0; + ms->f.ungot = 0; + ms->f.lock = 0; + ms->f.lock_owner = NULL; + ms->f.next = NULL; + ms->f.mem = ms; + ms->data = NULL; + ms->size = 0; + ms->len = 0; + ms->dpos = 0; + ms->fixed = 0; + ms->own_data = 0; + ms->string_mode = 0; + ms->user_ptr = NULL; + ms->user_len = NULL; + return ms; +} + +/* Release the FILE block (cache buffer + struct) on a construction error. */ +static void +ms_free_file(struct vlibc_memstream *ms) +{ + __libc_free(ms->f.buf); + __libc_free(ms); +} + +/* Index of the first NUL byte in data[0, size), or size when none. */ +static size_t +ms_nul_index(const struct vlibc_memstream *ms) +{ + size_t i; + + for (i = 0; i < ms->size; i++) + { + if (ms->data[i] == 0) + { + return i; + } + } + return ms->size; +} + +/* Grow the owned region until it holds `need` bytes (doubling). */ +static int +ms_grow(struct vlibc_memstream *ms, size_t need) +{ + size_t ncap; + unsigned char *nd; + + if (need <= ms->size) + { + return 1; + } + ncap = (ms->size > (size_t)-1 / 2) ? need : ms->size * 2; + if (ncap < need) + { + ncap = need; + } + nd = (unsigned char *)realloc(ms->data, ncap); + if (nd == NULL) + { + return 0; /* errno ENOMEM from realloc */ + } + ms->data = nd; + ms->size = ncap; + return 1; +} + +/* + * Copy n bytes from src into the region at offset at. Fixed regions take + * what fits (up to size - at); owned regions grow on demand. Extends len + * when the write passes the current end; owned regions zero the hole a + * seek past the end left behind, so stale allocator bytes never show up + * between the old length and the write. Returns the bytes written. + */ +static size_t +ms_write(struct vlibc_memstream *ms, size_t at, const unsigned char *src, size_t n) +{ + size_t end; + + if (ms->fixed) + { + if (at >= ms->size) + { + return 0; + } + if (n > ms->size - at) + { + n = ms->size - at; + } + } + else if (!ms_grow(ms, at + n + 1)) + { + return 0; + } + if (at > ms->len && !ms->fixed) + { + __builtin_memset(ms->data + ms->len, 0, at - ms->len); + } + __builtin_memcpy(ms->data + at, src, n); + end = at + n; + if (end > ms->len) + { + ms->len = end; + } + return n; +} + +/* Copy up to n bytes from offset at; nothing (EOF) once past len. */ +static size_t +ms_read(struct vlibc_memstream *ms, size_t at, unsigned char *dst, size_t n) +{ + if (at >= ms->len) + { + return 0; + } + if (n > ms->len - at) + { + n = ms->len - at; + } + __builtin_memcpy(dst, ms->data + at, n); + return n; +} + +/* Post-flush sync: open_memstream publishes the (possibly moved) buffer + * and its NUL-terminated length; string-mode fmemopen keeps the caller's + * region readable as a string. */ +static void +ms_sync(struct vlibc_memstream *ms) +{ + if (ms->user_ptr != NULL) + { + ms->data[ms->len] = 0; + *ms->user_ptr = (char *)ms->data; + *ms->user_len = ms->len; + } + else if (ms->string_mode && ms->len < ms->size) + { + ms->data[ms->len] = 0; + } +} + +/* + * Device hooks called by the stdio core (see stdio_impl.h). Each mirrors + * the descriptor half of the corresponding core helper, updating dpos the + * way a kernel would. + */ + +int +stdio_mem_flush(FILE *f) +{ + struct vlibc_memstream *ms = f->mem; + size_t n = (size_t)(f->wpos - f->buf); + + if (!(f->flags & F_WRITE)) + { + return 0; + } + if (n != 0) + { + size_t at = (f->flags & F_APPEND) ? ms->len : ms->dpos; + size_t w = ms_write(ms, at, f->buf, n); + + if (w < n) + { + /* Fixed region full or out of memory: keep the unwritten + * remainder buffered and report the error, exactly like a + * partial descriptor write. */ + size_t keep = n - w; + + __builtin_memmove(f->buf, f->buf + w, keep); + f->wpos = f->buf + keep; + f->pos = (off_t)(at + w); + ms->dpos = (size_t)f->pos; + f->flags |= F_ERR; + return -1; + } + f->pos = (off_t)(at + n); + ms->dpos = (size_t)f->pos; + } + f->wpos = f->buf; + ms_sync(ms); + return 0; +} + +void +stdio_mem_refill(FILE *f) +{ + struct vlibc_memstream *ms = f->mem; + size_t at = ms->dpos; + size_t n; + + f->rpos = f->rstop = f->buf; + f->pos = (off_t)at; /* re-anchor buf[0] at the device head */ + n = ms_read(ms, at, f->buf, f->buf_size); + if (n != 0) + { + f->rstop = f->buf + n; + ms->dpos = at + n; /* device read ahead of the logical position */ + } + else + { + f->flags |= F_EOF; + } +} + +int +stdio_mem_discard(FILE *f) +{ + struct vlibc_memstream *ms = f->mem; + + if (f->rpos < f->rstop) + { + /* Unread cached data: rewind the device head to the logical + * position (the byte at rpos) and re-anchor pos there. */ + f->pos += (off_t)(f->rpos - f->buf); + ms->dpos = (size_t)f->pos; + } + f->rpos = f->rstop = f->buf; + return 0; +} + +// NOLINTBEGIN(bugprone-easily-swappable-parameters) +off_t +stdio_mem_lseek(FILE *f, off_t off, int whence) +{ + struct vlibc_memstream *ms = f->mem; + off_t target; + + if (whence == SEEK_SET) + { + target = off; + } + else if (whence == SEEK_END) + { + target = (off_t)ms->len + off; + } + else + { + errno = EINVAL; + return (off_t)-1; + } + if (target < 0) + { + errno = EINVAL; + return (off_t)-1; + } + ms->dpos = (size_t)target; + return target; +} +// NOLINTEND(bugprone-easily-swappable-parameters) + +void +stdio_mem_close(FILE *f) +{ + struct vlibc_memstream *ms = f->mem; + + if (ms->user_ptr != NULL) + { + /* open_memstream: one last publish so the caller's pointer and + * size slots are current even without an explicit fflush + * (idempotent after a flush). */ + ms->data[ms->len] = 0; + *ms->user_ptr = (char *)ms->data; + *ms->user_len = ms->len; + } + if (ms->own_data) + { + free(ms->data); + ms->data = NULL; + ms->own_data = 0; + } +} + +// NOLINTBEGIN(bugprone-easily-swappable-parameters) +FILE * +fmemopen(void *buf, size_t size, const char *mode) +{ + int m = 0; + int update = 0; + int binary = 0; + size_t len = 0; + size_t i; + struct vlibc_memstream *ms; + + if (mode == NULL || size == 0) + { + errno = EINVAL; + return NULL; + } + switch (mode[0]) + { + case 'r': + m |= F_READ; + break; + case 'w': + m |= F_WRITE; + break; + case 'a': + m |= F_WRITE | F_APPEND; + break; + default: + errno = EINVAL; + return NULL; + } + for (i = 1; mode[i] != '\0'; i++) + { + switch (mode[i]) + { + case '+': + update = 1; + break; + case 'b': + binary = 1; + break; + default: + errno = EINVAL; + return NULL; + } + } + if (update) + { + m |= F_READ | F_WRITE; + } + + ms = ms_alloc(m); + if (ms == NULL) + { + return NULL; + } + if (buf == NULL) + { + /* No caller buffer: allocate a zeroed, growable region of our + * own (released on fclose, see stdio_mem_close). */ + ms->data = (unsigned char *)malloc(size); + if (ms->data == NULL) + { + ms_free_file(ms); + return NULL; + } + __builtin_memset(ms->data, 0, size); + ms->fixed = 0; + ms->own_data = 1; + } + else + { + ms->data = (unsigned char *)buf; + ms->fixed = 1; + ms->own_data = 0; + } + ms->size = size; + ms->string_mode = !binary; + + /* Current data length per the open mode (see the banner). */ + switch (mode[0]) + { + case 'a': + len = binary ? size : ms_nul_index(ms); + break; + case 'r': + len = size; + break; + default: + len = 0; /* 'w': truncated */ + break; + } + ms->len = len; + ms->dpos = (mode[0] == 'a') ? len : 0; + ms->f.pos = (off_t)ms->dpos; + return &ms->f; +} +// NOLINTEND(bugprone-easily-swappable-parameters) + +FILE * +open_memstream(char **ptr, size_t *sizeloc) +{ + struct vlibc_memstream *ms; + + if (ptr == NULL || sizeloc == NULL) + { + errno = EINVAL; + return NULL; + } + ms = ms_alloc(F_WRITE); + if (ms == NULL) + { + return NULL; + } + ms->data = (unsigned char *)malloc(OMS_INITIAL); + if (ms->data == NULL) + { + ms_free_file(ms); + return NULL; + } + ms->data[0] = 0; /* the buffer starts as the empty string */ + ms->size = OMS_INITIAL; + ms->fixed = 0; + ms->own_data = 0; /* the caller takes the region after fclose */ + ms->user_ptr = ptr; + ms->user_len = sizeloc; + *ptr = (char *)ms->data; + *sizeloc = 0; + return &ms->f; +} diff --git a/src/stdio/getline.c b/src/stdio/getline.c new file mode 100644 index 0000000..9fe3df4 --- /dev/null +++ b/src/stdio/getline.c @@ -0,0 +1,142 @@ +#ifdef HAVE_CONFIG_H +#include +#endif + +#include +#include +#include +#include + +#include "stdio_impl.h" + +/* + * vlibc — line input (todo 18): getdelim, getline, puts. + * + * getdelim reads one line (or one delimiter-terminated field) into a + * growable caller buffer. The buffer is owned by the caller and must be + * released with free(); the stream may reallocate it as the line grows, + * updating *lineptr and *n on every move so the caller's pointer never + * dangles, even when a later realloc fails and getdelim returns -1. + * + * Character storage uses the byte count, not strlen: embedded NUL bytes + * are preserved in the line, and the returned length is what matters (the + * buffer is still NUL-terminated after the last byte read). + * + * Reads go through fgetc, so a partial final line without the delimiter + * is returned with its length at end of file, exactly like fgets; a pure + * end-of-file (or an error) before any byte is -1. Distinguishing the two + * at a mid-line stop follows the fgets discipline: a genuine EOF keeps the + * partial line, a read error (F_ERR) discards it. + * + * puts is the one plain stdout line helper not owned by the todo-15 core + * (which took fputs): write s, then a newline. Returned value is + * non-negative on success and EOF on error, matching the C standard. + */ + +/* Initial capacity for a fresh getdelim buffer (both the *lineptr == NULL + * malloc case and the *n == 0 realloc-minimal case). */ +#define GETDELIM_INITIAL 128 + +ssize_t +getdelim(char **restrict lineptr, size_t *restrict n, int delim, FILE *restrict stream) +{ + char *buf; + size_t cap; + size_t used = 0; + int c; + + if (lineptr == NULL || n == NULL || stream == NULL) + { + errno = EINVAL; + return -1; + } + buf = *lineptr; + cap = *n; + if (buf == NULL) + { + /* No buffer yet: a fresh allocation, published right away so an + * early -1 (immediate end of file) still hands the caller a + * usable, freeable buffer. */ + buf = malloc(GETDELIM_INITIAL); + if (buf == NULL) + { + return -1; /* errno ENOMEM from the allocator */ + } + cap = GETDELIM_INITIAL; + *lineptr = buf; + *n = cap; + } + else if (cap == 0) + { + /* A zero-sized buffer is unusable: start it at the minimum. */ + char *nbuf = realloc(buf, GETDELIM_INITIAL); + + if (nbuf == NULL) + { + return -1; + } + buf = nbuf; + cap = GETDELIM_INITIAL; + *lineptr = buf; + *n = cap; + } + + for (;;) + { + if (used + 1 >= cap) + { + /* Room for one more byte plus the trailing NUL. Publish the + * grown buffer immediately so *lineptr never names a block + * this call has already freed. */ + char *grown = realloc(buf, cap * 2); + + if (grown == NULL) + { + *lineptr = buf; + *n = cap; + return -1; + } + buf = grown; + cap *= 2; + *lineptr = buf; + *n = cap; + } + c = fgetc(stream); + if (c == EOF) + { + if (used == 0 || (stream->flags & F_ERR)) + { + /* End of file (or an error) before any byte, or a read + * error in the middle of the line. */ + return -1; + } + /* Mid-line end of file: the partial line is the result. */ + break; + } + buf[used++] = (char)c; + if (c == delim) + { + break; + } + } + buf[used] = '\0'; + *lineptr = buf; + *n = cap; + return (ssize_t)used; +} + +ssize_t +getline(char **restrict lineptr, size_t *restrict n, FILE *restrict stream) +{ + return getdelim(lineptr, n, '\n', stream); +} + +int +puts(const char *s) +{ + if (fputs(s, stdout) == EOF) + { + return EOF; + } + return fputc('\n', stdout); +} diff --git a/src/stdio/locking.c b/src/stdio/locking.c new file mode 100644 index 0000000..66f8dc1 --- /dev/null +++ b/src/stdio/locking.c @@ -0,0 +1,178 @@ +#ifdef HAVE_CONFIG_H +#include +#endif + +#include +#include +#include + +#include "../internal/syscall.h" +#include "stdio_impl.h" + +/* + * vlibc — per-stream locking and unlocked character I/O (todo 18). + * + * flockfile/ftrylockfile/funlockfile are POSIX.1-2008 base. The process is + * single-threaded until the thread todo (#45) lands, so these maintain the + * ownership state that a real lock needs — recursion depth in ->lock, the + * owning thread's FS base in ->lock_owner — without any kernel primitive. + * flockfile is recursive: the same thread may lock a stream repeatedly and + * must unlock as many times. Once real threads exist, the #46 mutex/cond + * work makes flockfile block on ->lock when another thread owns it; the + * owner/count fields here already make the recursion decision. + * + * getc_unlocked/getchar_unlocked/putc_unlocked/putchar_unlocked are the + * four POSIX.1-2008 base _unlocked forms (the remaining GNU _unlocked + * forms are L3 and deliberately not provided). They assume the caller + * holds the stream lock and run the same buffered read/write core the + * locked fgetc/fputc use, via the hidden helpers of stdio_impl.h, without + * taking the lock themselves. + */ + +void +flockfile(FILE *stream) +{ + void *self = __builtin_thread_pointer(); + + if (stream->lock == 0 || stream->lock_owner != self) + { + /* Fresh acquisition, or (once threads exist) a foreign owner: + * the #46 per-stream lock will block here until the stream is + * free. The single running thread is always the owner. */ + stream->lock_owner = self; + stream->lock = 0; + } + stream->lock++; +} + +int +ftrylockfile(FILE *stream) +{ + if (stream->lock > 0) + { + /* Already owned (recursively or by another thread): the #46 real + * lock would fail here without blocking. */ + return EBUSY; + } + stream->lock_owner = __builtin_thread_pointer(); + stream->lock = 1; + return 0; +} + +void +funlockfile(FILE *stream) +{ + if (stream->lock > 0) + { + stream->lock--; + if (stream->lock == 0) + { + stream->lock_owner = NULL; + } + } +} + +int +getc_unlocked(FILE *stream) +{ + stdio_init_if_needed(stream); + if (stream->flags & F_PUSHED) + { + stream->flags &= ~F_PUSHED; + return stream->ungot; + } + if (stream->flags & F_WRITE) + { + if (!(stream->flags & F_READ)) + { + /* Reading a write-only stream. */ + stream->flags |= F_ERR; + return EOF; + } + if (stdio_flush(stream) < 0) + { + return EOF; + } + stream->flags &= ~F_WRITE; + stream->flags |= F_READ; + } + stream->flags |= F_READ; + if (stream->rpos >= stream->rstop) + { + stdio_refill(stream); + } + if (stream->rpos >= stream->rstop) + { + return EOF; /* F_EOF or F_ERR set by refill */ + } + return *stream->rpos++; +} + +int +getchar_unlocked(void) +{ + return getc_unlocked(stdin); +} + +// NOLINTBEGIN(clang-analyzer-security.insecureAPI.DeprecatedOrUnsafeBufferHandling) +int +putc_unlocked(int c, FILE *stream) +{ + stdio_init_if_needed(stream); + if (!(stream->flags & F_WRITE) && !(stream->flags & F_RDWR)) + { + /* The open mode does not allow writes. */ + stream->flags |= F_ERR; + return EOF; + } + if (stream->rpos < stream->rstop) + { + /* Pending read data: discard it before writing (the read -> write + * mode switch on an update stream). */ + if (stdio_discard_read(stream) < 0) + { + return EOF; + } + } + stream->flags |= F_WRITE; + if (stream->flags & F_UNBUF) + { + unsigned char ch = (unsigned char)c; + long r = __syscall3(SYS_write, stream->fd, (long)&ch, 1); + + if (r != 1) + { + if (r < 0) + { + (void)syscall_ret(r); /* sets errno */ + } + stream->flags |= F_ERR; + return EOF; + } + stream->pos += 1; + return ch; + } + if (stream->wpos >= stream->wstop) + { + if (stdio_flush(stream) < 0) + { + return EOF; + } + } + *stream->wpos++ = (unsigned char)c; + if ((stream->flags & F_LINEBUF) && c == '\n') + { + if (stdio_flush(stream) < 0) + { + return EOF; + } + } + return (unsigned char)c; +} +// NOLINTEND(clang-analyzer-security.insecureAPI.DeprecatedOrUnsafeBufferHandling) + +int +putchar_unlocked(int c) +{ + return putc_unlocked(c, stdout); +} diff --git a/src/stdio/stdio.c b/src/stdio/stdio.c index f082f3f..37b1282 100644 --- a/src/stdio/stdio.c +++ b/src/stdio/stdio.c @@ -284,6 +284,9 @@ stdio_alloc_file(int fd, int m) } f->pos = 0; f->ungot = 0; + f->lock = 0; + f->lock_owner = NULL; + f->mem = NULL; f->next = open_list; open_list = f; return f; @@ -295,6 +298,13 @@ stdio_refill(FILE *f) { long n; + if (f->mem != NULL) + { + /* Memory stream: read from the backing region instead of the + * descriptor (see the stdio_impl.h memstream contract). */ + stdio_mem_refill(f); + return; + } f->rpos = f->rstop = f->buf; n = __syscall3(SYS_read, f->fd, (long)f->buf, (long)f->buf_size); if (n > 0) @@ -315,6 +325,13 @@ stdio_refill(FILE *f) int stdio_discard_read(FILE *f) { + if (f->mem != NULL) + { + /* Memory stream: discarding unread data only rewinds the backend + * position; there is no kernel position to seek (see the + * stdio_impl.h memstream contract). */ + return stdio_mem_discard(f); + } if (f->rpos < f->rstop) { /* Unread buffered data: move the kernel position back to the @@ -340,6 +357,13 @@ stdio_flush(FILE *f) size_t n; long r; + if (f->mem != NULL) + { + /* Memory stream: the backend's flush does its own device write + * (and open_memstream's size sync); the partial-write discipline + * below is mirrored there. */ + return stdio_mem_flush(f); + } if (!(f->flags & F_WRITE)) { return 0; @@ -522,6 +546,13 @@ fclose(FILE *stream) { rc = EOF; } + if (stream->mem != NULL) + { + /* Memory stream: finalize the backend (open_memstream's last + * NUL/size sync, fmemopen's owned-buffer release) before the + * FILE block below is freed. */ + stdio_mem_close(stream); + } if (stream->fd >= 0 && syscall_ret(__syscall1(SYS_close, stream->fd)) < 0) { rc = EOF; @@ -946,8 +977,20 @@ fseeko(FILE *stream, off_t offset, int whence) } if (target != (off_t)-1) { - /* SEEK_SET/SEEK_CUR: seek the kernel to the absolute target. */ - stream->pos = stdio_lseek(stream->fd, target, SEEK_SET); + /* SEEK_SET/SEEK_CUR: seek the kernel to the absolute target. + * Memory streams resolve the target against their region. */ + if (stream->mem != NULL) + { + stream->pos = stdio_mem_lseek(stream, target, SEEK_SET); + } + else + { + stream->pos = stdio_lseek(stream->fd, target, SEEK_SET); + } + } + else if (stream->mem != NULL) + { + stream->pos = stdio_mem_lseek(stream, offset, SEEK_END); } else { diff --git a/src/stdio/stdio_impl.h b/src/stdio/stdio_impl.h index 94a1fbc..4d03c9b 100644 --- a/src/stdio/stdio_impl.h +++ b/src/stdio/stdio_impl.h @@ -25,25 +25,48 @@ * * The helper prototypes below are shared with the formatted-I/O todos * (16: vfprintf, 17: vfscanf) so they operate on the same buffer state. + * + * Locking (todo 18): the process is single-threaded until the thread + * todo (#45) lands, so flockfile/ftrylockfile/funlockfile only track + * ownership state here. `lock` is the recursion depth (0 = unlocked), + * `lock_owner` the owning thread's FS base while locked. When real + * threads arrive, the same todo turns `lock` into the actual per-stream + * lock word (blocking acquisition, mutex/cond state) and these two + * fields become the bookkeeping that makes flockfile recursive. + * + * Memory streams (todo 18): fmemopen/open_memstream build a FILE whose + * `mem` points at an opaque backend (defined in fmemopen.c) instead of a + * descriptor. Whenever `mem` is non-NULL the buffered core below calls + * back into fmemopen.c's stdio_mem_* hooks in place of every descriptor + * syscall, so the position invariant above is unchanged: the backend + * simply acts as an in-memory device with the same kernel-position + * semantics (reads advance it past the read-ahead, writes advance it on + * flush, discards rewind it to the logical position). */ #include #include "../internal/libc.h" +/* Opaque memory-stream backend (fmemopen/open_memstream, todo 18). */ +struct vlibc_memstream; + struct vlibc_FILE { - unsigned char *buf; /* buffer base: library- or user-supplied */ - unsigned char *rpos; /* read mode: next byte to deliver */ - unsigned char *rstop; /* read mode: end of valid buffered data */ - unsigned char *wpos; /* write mode: next byte to store */ - unsigned char *wstop; /* write mode: end of the buffer */ - int fd; /* underlying descriptor; -1 when closed */ - int flags; /* F_* bits below */ - size_t buf_size; /* usable buffer size; 0 = not yet sized */ - off_t pos; /* position per the invariant above */ - struct vlibc_FILE *next; /* registry link for fflush(NULL) */ - unsigned char ungot; /* pushed-back byte when F_PUSHED */ + unsigned char *buf; /* buffer base: library- or user-supplied */ + unsigned char *rpos; /* read mode: next byte to deliver */ + unsigned char *rstop; /* read mode: end of valid buffered data */ + unsigned char *wpos; /* write mode: next byte to store */ + unsigned char *wstop; /* write mode: end of the buffer */ + int fd; /* underlying descriptor; -1 when closed */ + int flags; /* F_* bits below */ + size_t buf_size; /* usable buffer size; 0 = not yet sized */ + off_t pos; /* position per the invariant above */ + struct vlibc_FILE *next; /* registry link for fflush(NULL) */ + unsigned char ungot; /* pushed-back byte when F_PUSHED */ + int lock; /* flockfile recursion depth; 0 = unlocked */ + void *lock_owner; /* owning thread's FS base while locked */ + struct vlibc_memstream *mem; /* memory-stream backend, or NULL */ }; /* Stream flags. */ @@ -94,4 +117,34 @@ stdio_parse_mode(const char *mode, int *m, int *oflags); hidden off_t stdio_lseek(int fd, off_t off, int whence); +/* + * Memory-stream device hooks (fmemopen.c, todo 18). The stdio core calls + * these in place of the descriptor syscalls whenever f->mem is set: + * + * - stdio_mem_flush: write the pending cache to the backing region + * (stdio_flush's device-write half, F_APPEND included). + * - stdio_mem_refill: read the next cache-full from the region + * (stdio_refill's device-read half). + * - stdio_mem_discard: rewind the device to the logical position, + * discarding unread cached data (stdio_discard_read's seek half). + * - stdio_mem_lseek: resolve a SEEK_SET/SEEK_END target against the + * region (used by fseeko instead of stdio_lseek on memory streams). + * - stdio_mem_close: run on fclose before the FILE is released + * (open_memstream's final NUL/size sync, owned-buffer release). + */ +hidden int +stdio_mem_flush(FILE *f); + +hidden void +stdio_mem_refill(FILE *f); + +hidden int +stdio_mem_discard(FILE *f); + +hidden off_t +stdio_mem_lseek(FILE *f, off_t off, int whence); + +hidden void +stdio_mem_close(FILE *f); + #endif /* VLIBC_STDIO_STDIO_IMPL_H */ diff --git a/tests/test_getline.c b/tests/test_getline.c new file mode 100644 index 0000000..ae8b6c4 --- /dev/null +++ b/tests/test_getline.c @@ -0,0 +1,611 @@ +/* + * vlibc — getline/getdelim, stream locking, memory streams test (todo 18). + * + * Exercises the todo-18 additions end to end: + * + * 1. getline on a 1 MiB line returns 1000001 (delimiter included) with a + * NUL terminator, growing a NULL/zero-capacity buffer from scratch; + * the next call at end of file returns -1. Reuse keeps the grown + * buffer. + * 2. getdelim with ',' over "a,b,c\n" yields "a,", "b,", then the + * partial final field "c\n" at end of file, then -1. + * 3. Embedded NUL bytes survive: getline on "ab\0cd\n" returns 6 with + * the NUL at line[2] intact (length, not strlen). + * 4. A small preallocated buffer (malloc(4)) grows to fit a long line. + * 5. flockfile recursion: two nested flockfile calls, ftrylockfile + * failing (nonzero) under the recursion and succeeding (0) when the + * stream is free again. + * 6. getc_unlocked/putc_unlocked under a held flockfile move real data; + * getchar_unlocked/putchar_unlocked and puts work over freopen'd + * stdin/stdout. + * 7. fmemopen over a fixed buffer: write, rewind, read back + * byte-identical; seeking past the size reads EOF (no error); + * fmemopen(NULL, ...) grows and is freed by fclose (leak-checked); + * "a" starts at the current data length. + * 8. open_memstream: after fflush/fclose *sizeloc is the length and the + * buffer is NUL-terminated; *ptr tracks the (possibly moved) buffer. + * 9. fclose over many fmemopen/open_memstream cycles leaks nothing. + * + * Failure mode (-f): getline on a stream whose underlying descriptor was + * closed underneath it returns -1 (the read hits EBADF); the return value + * is asserted, never errno. -f exits via a raw SYS_exit_group before any + * host-libc cleanup runs (the tests/syscall_test.c discipline). + * + * errno is never READ here; the default mode keeps every library errno + * write out of the exercised paths. All diagnostics go through raw + * SYS_write and the only headers are vlibc's own. + * + * Not part of the library proper; compiled manually for this todo. + */ + +#include +#include + +#include "../src/internal/syscall.h" + +/* The allocator's heap-walk consistency probe (hidden; linked in via + * malloc.c). Returns the live block count, or (size_t)-1 on disagreement. */ +extern size_t +__vlibc_malloc_check(void); // NOLINT(bugprone-reserved-identifier) + +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 buffers. noipa keeps -O2 from folding the check. */ +// NOLINTBEGIN(clang-analyzer-unix.Stream) +static __attribute__((noipa)) int +mem_eq(const unsigned char *a, const unsigned char *b, unsigned long n) +{ + unsigned long i; + + for (i = 0; i < n; i++) + { + if (a[i] != b[i]) + { + return 0; + } + } + return 1; +} + +/* Byte-compare two NUL-terminated strings. */ +static __attribute__((noipa)) int +str_eq(const char *a, const char *b) +{ + while (*a == *b && *a != '\0') + { + a++; + b++; + } + return *a == *b; +} + +/* Read a whole small file through raw syscalls into buf; returns size. */ +static long +raw_read_all(const char *path, char *buf, unsigned long cap) +{ + long fd = __syscall4(SYS_openat, -100, (long)path, 0x0, 0); + long got = -1; + long total = 0; + + if (fd < 0) + { + return -1; + } + while (total < (long)cap) + { + got = __syscall3(SYS_read, fd, (long)(buf + total), (long)(cap - (unsigned long)total)); + if (got <= 0) + { + break; + } + total += got; + } + __syscall1(SYS_close, fd); + return got < 0 ? -1 : total; +} + +/* Write path bytes to a fresh file, then reopen it for reading. */ +// NOLINTBEGIN(bugprone-easily-swappable-parameters) +static FILE * +write_file(const char *path, const char *data, unsigned long n) +{ + FILE *f = fopen(path, "w"); + + if (f == NULL) + { + return NULL; + } + if (fwrite(data, 1, n, f) != n) + { + (void)fclose(f); + return NULL; + } + if (fclose(f) != 0) + { + return NULL; + } + f = fopen(path, "r"); + if (f == NULL) + { + say(2, "FAIL: write_file reopen\n"); + failures++; + } + return f; +} +// NOLINTEND(bugprone-easily-swappable-parameters) + +static char onemeg[1000001]; + +/* 1/3/4: getline length, growth, EOF, embedded NUL. */ +static void +line_scenario(void) +{ + const char bigpath[] = "/tmp/vlibc-test-t18-bigline.txt"; + const char nulpath[] = "/tmp/vlibc-test-t18-nul.txt"; + const char longpath[] = "/tmp/vlibc-test-t18-long.txt"; + const char longline[] = "this line is way longer than sixteen bytes\n"; + FILE *f; + char *line; + size_t n; + ssize_t got; + unsigned long i; + + for (i = 0; i < 1000000; i++) + { + onemeg[i] = 'x'; + } + onemeg[1000000] = '\n'; + f = fopen(bigpath, "w"); + check(f != NULL, "bigline fopen w succeeds"); + if (f == NULL) + { + return; + } + check(fwrite(onemeg, 1, sizeof(onemeg), f) == sizeof(onemeg), "bigline fwrite 1 MiB + newline"); + check(fclose(f) == 0, "bigline fclose writer"); + + f = fopen(bigpath, "r"); + check(f != NULL, "bigline fopen r succeeds"); + line = NULL; + n = 0; + got = getline(&line, &n, f); + check(got == 1000001, "getline on the 1 MiB line returns 1000001"); + check(line != NULL && n >= 1000002, "getline grew the buffer past 1 MiB + NUL"); + if (line != NULL) + { + int allx = 1; + + for (i = 0; i < 1000000; i++) + { + if (line[i] != 'x') + { + allx = 0; + break; + } + } + check(allx != 0, "the 1 MiB line content is intact"); + check(line[1000000] == '\n', "the newline is kept at line[1000000]"); + check(line[1000001] == '\0', "the line is NUL-terminated"); + } + got = getline(&line, &n, f); + check(got == -1, "getline at end of file returns -1"); + free(line); + check(fclose(f) == 0, "bigline fclose reader"); + check(remove(bigpath) == 0, "bigline remove"); + + /* Embedded NUL bytes are data, not terminators. */ + f = write_file(nulpath, "ab\0cd\n", 6); + if (f != NULL) + { + line = NULL; + n = 0; + got = getline(&line, &n, f); + check(got == 6, "getline over \"ab\\0cd\\n\" returns 6 bytes"); + check(line != NULL && line[0] == 'a' && line[1] == 'b' && line[2] == '\0' && + line[3] == 'c' && line[4] == 'd' && line[5] == '\n' && line[6] == '\0', + "embedded NUL survives and the line is still NUL-terminated"); + free(line); + check(fclose(f) == 0, "nul fclose"); + check(remove(nulpath) == 0, "nul remove"); + } + + /* A small preallocated buffer grows past its initial capacity. */ + f = write_file(longpath, longline, sizeof(longline) - 1); + if (f != NULL) + { + line = malloc(4); + check(line != NULL, "small-buffer malloc"); + n = 4; + got = getline(&line, &n, f); + check(got == (ssize_t)(sizeof(longline) - 1), + "getline with a 4-byte buffer reads the whole long line"); + check(n > sizeof(longline) - 1, "the capacity grew past the line length"); + check(line != NULL && str_eq(line, longline), "grown small buffer holds the line"); + free(line); + check(fclose(f) == 0, "long fclose"); + check(remove(longpath) == 0, "long remove"); + } +} + +/* 2: getdelim with a custom delimiter. */ +static void +delim_scenario(void) +{ + const char path[] = "/tmp/vlibc-test-t18-delim.txt"; + FILE *f; + char *line; + size_t n; + ssize_t got; + + f = write_file(path, "a,b,c\n", 6); + check(f != NULL, "delim file setup"); + if (f == NULL) + { + return; + } + line = NULL; + n = 0; + got = getdelim(&line, &n, ',', f); + check(got == 2 && line != NULL && str_eq(line, "a,"), "getdelim first field is \"a,\""); + got = getdelim(&line, &n, ',', f); + check(got == 2 && line != NULL && str_eq(line, "b,"), "getdelim second field is \"b,\""); + got = getdelim(&line, &n, ',', f); + check(got == 2 && line != NULL && str_eq(line, "c\n"), + "getdelim returns the partial final field at end of file"); + got = getdelim(&line, &n, ',', f); + check(got == -1, "getdelim at end of file returns -1"); + free(line); + check(fclose(f) == 0, "delim fclose"); + check(remove(path) == 0, "delim remove"); +} + +/* 5/6: stream locking and the four _unlocked forms. */ +static void +lock_scenario(void) +{ + const char rpath[] = "/tmp/vlibc-test-t18-lockr.txt"; + const char wpath[] = "/tmp/vlibc-test-t18-lockw.txt"; + char got[8]; + FILE *f; + int r0; + int r1; + + f = fopen(rpath, "w+"); + check(f != NULL, "lock fopen w+ succeeds"); + if (f == NULL) + { + return; + } + r0 = ftrylockfile(f); + check(r0 == 0, "ftrylockfile on a free stream returns 0"); + flockfile(f); /* recursion depth 2 */ + flockfile(f); /* recursion depth 3 */ + r1 = ftrylockfile(f); + check(r1 != 0, "ftrylockfile under the recursion returns nonzero"); + funlockfile(f); + funlockfile(f); + funlockfile(f); + r0 = ftrylockfile(f); + check(r0 == 0, "ftrylockfile succeeds again after all unlocks"); + funlockfile(f); + check(fclose(f) == 0, "lock fclose"); + + f = write_file(rpath, "MNO", 3); + check(f != NULL, "unlocked-read setup"); + if (f != NULL) + { + flockfile(f); + check(getc_unlocked(f) == 'M', "getc_unlocked reads 'M' under the lock"); + check(getc_unlocked(f) == 'N', "getc_unlocked reads 'N' under the lock"); + funlockfile(f); + check(fgetc(f) == 'O', "the locked fgetc still reads 'O' after unlock"); + check(fclose(f) == 0, "unlocked-read fclose"); + check(remove(rpath) == 0, "unlocked-read remove"); + } + + f = fopen(wpath, "w"); + check(f != NULL, "unlocked-write setup"); + if (f != NULL) + { + flockfile(f); + check(putc_unlocked('Q', f) == 'Q', "putc_unlocked writes 'Q' under the lock"); + check(putc_unlocked('R', f) == 'R', "putc_unlocked writes 'R' under the lock"); + funlockfile(f); + check(fclose(f) == 0, "unlocked-write fclose"); + check(raw_read_all(wpath, got, sizeof(got)) == 2 && got[0] == 'Q' && got[1] == 'R', + "putc_unlocked data reached the file"); + check(remove(wpath) == 0, "unlocked-write remove"); + } +} + +/* 7: fmemopen over a fixed buffer, past-size EOF, NULL-buffer growth, 'a'. */ +static void +fmemopen_scenario(void) +{ + char buf[64]; + char out[64]; + FILE *f; + size_t r; + int c; + + f = fmemopen(buf, sizeof(buf), "w+"); + check(f != NULL, "fmemopen w+ succeeds"); + if (f == NULL) + { + return; + } + check(fputs("hello fmemopen", f) >= 0, "fputs into the fixed buffer"); + check(ftello(f) == 14, "ftello == 14 after the write"); + check(fseek(f, 0, SEEK_SET) == 0, "rewind via fseek(0, SEEK_SET)"); + r = fread(out, 1, sizeof(out), f); + check(r == 14 && + mem_eq((const unsigned char *)out, (const unsigned char *)"hello fmemopen", 14), + "fmemopen write/rewind/read is byte-identical"); + c = fgetc(f); + check(c == EOF && feof(f), "fmemopen read past the data returns EOF"); + check(fclose(f) == 0, "fmemopen w+ fclose"); + + /* Seek beyond the buffer size, then read: EOF, not an error. */ + f = fmemopen(buf, sizeof(buf), "w+"); + if (f != NULL) + { + check(fputs("xyz", f) >= 0, "fmemopen setup writes xyz"); + check(fseek(f, 100, SEEK_SET) == 0, "fseek past the size succeeds"); + c = fgetc(f); + check(c == EOF && feof(f) && ferror(f) == 0, "reading past the size is EOF with no error"); + check(ftello(f) == 100, "ftello stays at the seek target"); + check(fclose(f) == 0, "fmemopen past-size fclose"); + } + + /* 'a' positions and appends at the current data length. */ + { + char abuf[64] = "hello"; + FILE *g = fmemopen(abuf, sizeof(abuf), "a"); + + check(g != NULL, "fmemopen a succeeds"); + if (g != NULL) + { + check(ftello(g) == 5, "fmemopen a starts at the current length"); + check(fputs("XY", g) >= 0, "fmemopen a appends"); + check(fclose(g) == 0, "fmemopen a fclose flushes"); + check(mem_eq((const unsigned char *)abuf, (const unsigned char *)"helloXY", 7), + "the appended data landed in the caller buffer"); + check(abuf[7] == '\0', "string-mode fmemopen NUL-terminates at the length"); + } + } +} + +/* open_memstream: publish on flush, grow, NUL-terminate, free on close. */ +static void +memstream_scenario(void) +{ + char *p = NULL; + size_t z = 0; + FILE *f; + + f = open_memstream(&p, &z); + check(f != NULL && p != NULL && z == 0, "open_memstream returns a stream and an empty buffer"); + if (f == NULL) + { + return; + } + check(fputs("hello", f) >= 0, "open_memstream write 1"); + check(fflush(f) == 0, "open_memstream fflush 1"); + check(z == 5, "*sizeloc is the length after fflush"); + check(p != NULL && p[5] == '\0' && + mem_eq((const unsigned char *)p, (const unsigned char *)"hello", 5), + "the flushed buffer is NUL-terminated at the length"); + check(fputs(" world", f) >= 0, "open_memstream write 2"); + check(fflush(f) == 0, "open_memstream fflush 2"); + check(z == 11 && p != NULL && p[11] == '\0', + "the grown length and NUL are published after the second fflush"); + check(fclose(f) == 0, "open_memstream fclose"); + check(z == 11 && p != NULL && p[11] == '\0', "fclose keeps the NUL-terminated result"); + check(mem_eq((const unsigned char *)p, (const unsigned char *)"hello world", 11), + "the open_memstream content is correct"); + free(p); +} + +/* 9: no leaks across repeated memory-stream open/close cycles. */ +static void +memstream_leak_scenario(void) +{ + const char *data = "0123456789abcdef0123456789abcdef0123456789abcdef"; + size_t before; + size_t after; + int i; + + before = __vlibc_malloc_check(); + check(before != (size_t)-1, "allocator heap walk is consistent before the cycles"); + if (before == (size_t)-1) + { + return; + } + for (i = 0; i < 100; i++) + { + FILE *g; + char *p = NULL; + size_t z = 0; + + g = fmemopen(NULL, 8, "w"); + if (g == NULL || fputs(data, g) == EOF || fclose(g) != 0) + { + say(2, "FAIL: fmemopen cycle failed\n"); + failures++; + return; + } + g = open_memstream(&p, &z); + if (g == NULL || fputs(data, g) == EOF || fclose(g) != 0) + { + say(2, "FAIL: open_memstream cycle failed\n"); + failures++; + return; + } + free(p); + } + after = __vlibc_malloc_check(); + check(after == before, + "fclose releases memory-stream FILEs and owned buffers (no leak over 100 cycles)"); +} + +/* puts, putchar_unlocked, getchar_unlocked over redirected std streams. */ +static void +stream_redirect_scenario(void) +{ + const char outpath[] = "/tmp/vlibc-test-t18-stdout.txt"; + const char inpath[] = "/tmp/vlibc-test-t18-stdin.txt"; + char buf[32]; + long saved_out; + long saved_in; + long got; + int r1; + int r2; + int r3; + int r4; + int c; + + /* stdout: no check() output on fd 1 while it names the file. */ + saved_out = __syscall1(SYS_dup, 1); + check(saved_out >= 0, "dup(1) saves the stdout descriptor"); + r1 = (freopen(outpath, "w", stdout) == stdout); + r2 = (puts("AA") >= 0); + r3 = (putchar_unlocked('B') == 'B'); + r4 = (fflush(stdout) == 0); + __syscall2(SYS_dup2, saved_out, 1); + __syscall1(SYS_close, saved_out); + check(r1, "freopen stdout to a file"); + check(r2, "puts writes through stdout"); + check(r3, "putchar_unlocked writes through stdout"); + check(r4, "fflush(stdout) after the freopen"); + got = raw_read_all(outpath, buf, sizeof(buf)); + check(got == 4 && buf[0] == 'A' && buf[1] == 'A' && buf[2] == '\n' && buf[3] == 'B', + "puts + putchar_unlocked content reached the file as \"AA\\nB\""); + check(remove(outpath) == 0, "remove the stdout file"); + + /* stdin: seed a file, rebind stdin, read a char, restore. */ + { + FILE *seed = write_file(inpath, "K", 1); + + check(seed != NULL, "stdin seed file written"); + if (seed != NULL) + { + (void)fclose(seed); + } + saved_in = __syscall1(SYS_dup, 0); + check(saved_in >= 0, "dup(0) saves the stdin descriptor"); + r1 = (freopen(inpath, "r", stdin) == stdin); + c = getchar_unlocked(); + r2 = (c == 'K'); + __syscall2(SYS_dup2, saved_in, 0); + __syscall1(SYS_close, saved_in); + check(r1, "freopen stdin from the seed file"); + check(r2, "getchar_unlocked reads the seed character"); + check(remove(inpath) == 0, "remove the stdin file"); + } +} + +/* Failure scenarios (-f). */ +static int +failure_scenarios(void) +{ + const char path[] = "/tmp/vlibc-test-t18-fail.txt"; + FILE *f; + char *line = NULL; + size_t n = 0; + ssize_t got; + + f = fopen(path, "w+"); + if (f == NULL) + { + say(2, "FAIL: -f setup fopen failed\n"); + failures++; + return 1; + } + check(fputs("data-data-data", f) >= 0, "-f setup fputs"); + check(fflush(f) == 0, "-f setup fflush"); + (void)__syscall1(SYS_close, fileno(f)); /* close the fd underneath */ + got = getline(&line, &n, f); + check(got == -1, "getline on a closed-underneath stream returns -1"); + free(line); + (void)fclose(f); + check(remove(path) == 0, "-f remove"); + return failures > 0 ? 1 : 0; +} +// NOLINTEND(clang-analyzer-unix.Stream) + +int +main(int argc, char **argv) +{ + if (argc == 2 && argv[1][0] == '-' && argv[1][1] == 'f') + { + int rc = failure_scenarios(); + + __syscall1(SYS_exit_group, rc); + return rc; /* not reached */ + } + + line_scenario(); + delim_scenario(); + lock_scenario(); + fmemopen_scenario(); + memstream_scenario(); + memstream_leak_scenario(); + stream_redirect_scenario(); + + if (failures > 0) + { + say(2, "FAILED ("); + say_dec(2, (unsigned long)failures); + say(2, " check(s))\n"); + return 1; + } + say(1, "all getline/locking/memstream tests passed\n"); + return 0; +}