From 22587bd5459d76da546d9ad4a03b2485bc18bc38 Mon Sep 17 00:00:00 2001 From: huntedbytheirs Date: Fri, 4 Sep 2026 23:27:27 -0400 Subject: [PATCH] feat(stdio): FILE buffering core and stream I/O --- include/stdio.h | 296 ++++++++++ src/stdio/stdio.c | 1168 ++++++++++++++++++++++++++++++++++++++++ src/stdio/stdio_impl.h | 94 ++++ tests/test_stdio.c | 718 ++++++++++++++++++++++++ 4 files changed, 2276 insertions(+) create mode 100644 include/stdio.h create mode 100644 src/stdio/stdio.c create mode 100644 src/stdio/stdio_impl.h create mode 100644 tests/test_stdio.c diff --git a/include/stdio.h b/include/stdio.h new file mode 100644 index 0000000..8472133 --- /dev/null +++ b/include/stdio.h @@ -0,0 +1,296 @@ +#ifndef VLIBC_STDIO_H +#define VLIBC_STDIO_H + +/* + * vlibc — . + * + * Buffered stream I/O (todo 15). The FILE object is opaque to consumers; + * the layout lives in src/stdio/stdio_impl.h. stdin/stdout/stderr are + * pre-wired streams over fds 0/1/2 and are initialized lazily on first + * use. On Linux there is no text/binary distinction, so the 'b' mode + * character is accepted and ignored. + * + * Buffering modes for setvbuf: _IOFBF (fully buffered), _IOLBF (line + * buffered: flush on newline), _IONBF (unbuffered). A stream opened on a + * terminal defaults to line buffering for stdout and full buffering for + * stdin; stderr is always unbuffered. + * + * getc/putc/getchar/putchar are declared as functions (address-taking and + * #undef work) and additionally defined as macros over fgetc/fputc; the + * macro arguments are evaluated exactly once. + * + * Level 2 (muslmimic) adds tmpnam/ctermid/setbuffer/setlinebuf and the + * fopen64 name alias (identical ABI on LP64). + */ + +#include + +#include +#include + +#ifdef __cplusplus +extern "C" { +#endif + +/* + * The opaque stream object. The underlying struct tag is vlibc_FILE (see + * src/stdio/stdio_impl.h); consumers only ever use FILE *. + */ +typedef struct vlibc_FILE FILE; + +/* The three pre-wired standard streams (fds 0, 1, 2). */ +extern FILE *stdin; +extern FILE *stdout; +extern FILE *stderr; + +/* End of file indicator for character functions. */ +#define EOF (-1) + +/* Minimum number of simultaneously open files. */ +#define FOPEN_MAX 16 + +/* Default buffer size for setvbuf/setbuf. */ +#define BUFSIZ 8192 + +/* Maximum length of a path argument for stdio functions. */ +#define FILENAME_MAX 4096 + +/* Minimum number of distinct tmpnam-generated names. */ +#define TMP_MAX 238328 + +/* Buffer sizes for tmpnam and ctermid results. */ +#define L_tmpnam 20 +#define L_ctermid 9 + +/* Seek positions for fseek/fseeko. */ +#define SEEK_SET 0 +#define SEEK_CUR 1 +#define SEEK_END 2 + +/* setvbuf modes. */ +#define _IOFBF 0 // NOLINT(bugprone-reserved-identifier) +#define _IOLBF 1 // NOLINT(bugprone-reserved-identifier) +#define _IONBF 2 // NOLINT(bugprone-reserved-identifier) + +/* Opaque file position type for fgetpos/fsetpos. */ +typedef off_t fpos_t; + +/* + * Open the file at path with the given mode. The mode is 'r', 'w', or 'a', + * optionally followed by '+' (update: read and write), 'b' (ignored on + * Linux), and/or 'x' (exclusive create, C11). Returns NULL with errno set + * on failure. + */ +FILE * +fopen(const char *restrict path, const char *restrict mode); + +/* + * Wrap an existing descriptor in a stream. The requested mode must be + * compatible with the descriptor's access mode (basic check via fcntl). + * The descriptor is not duplicated; fclose closes it. + */ +FILE * +fdopen(int fd, const char *mode); + +/* + * Rebind stream to path. The old descriptor is flushed and closed first. + * With path == NULL only the mode changes and the descriptor stays open. + * Returns NULL with errno set on failure. + */ +FILE * +freopen(const char *restrict path, const char *restrict mode, FILE *restrict stream); + +/* + * Flush pending output, close the descriptor, and release the stream. + * Returns EOF if flushing or closing failed. + */ +int +fclose(FILE *stream); + +/* + * Flush pending output of stream. fflush(NULL) flushes all open streams + * with pending output. On a stream with no pending writes (read mode) the + * unread buffered data is discarded and the position rewound; this never + * corrupts the stream. Returns EOF on error. + */ +int +fflush(FILE *stream); + +/* + * Set the buffer of stream to buf. buf == NULL selects unbuffered I/O; + * otherwise the buffer is used with full buffering and BUFSIZ size. Must + * be called before the first operation on the stream. + */ +void +setbuf(FILE *restrict stream, char *restrict buf); + +/* + * Set the buffering mode of stream: _IOFBF, _IOLBF, or _IONBF. With + * buf != NULL the caller supplies size bytes of storage; with buf == NULL + * the buffer is allocated lazily on first use (size is then ignored, + * except that _IONBF needs no buffer). Returns 0, or -1 with errno EINVAL + * for an invalid mode or a non-NULL buf with size 0 (except _IONBF). + */ +int +setvbuf(FILE *restrict stream, char *restrict buf, int mode, size_t size); + +/* + * Read up to size * nmemb bytes in items of size bytes each. Returns the + * number of complete items read; fewer than nmemb means end of file or an + * error (distinguishable via feof/ferror). size or nmemb zero returns 0 + * without touching the stream. + */ +size_t +fread(void *restrict ptr, size_t size, size_t nmemb, FILE *restrict stream); + +/* + * Write size * nmemb bytes in items of size bytes each. Returns the number + * of complete items written; fewer than nmemb means an error (see + * ferror). + */ +size_t +fwrite(const void *restrict ptr, size_t size, size_t nmemb, FILE *restrict stream); + +/* Read the next character as unsigned char, or EOF. */ +int +fgetc(FILE *stream); + +/* Write c as unsigned char; returns it, or EOF on error. */ +int +fputc(int c, FILE *stream); + +/* Same as fgetc/fputc; also provided as macros (arguments evaluated once). */ +int +getc(FILE *stream); + +int +putc(int c, FILE *stream); + +int +getchar(void); + +int +putchar(int c); + +/* + * Read at most n-1 characters into s, stopping after (and keeping) a + * newline, then NUL-terminate. Returns s, or NULL if no character was read + * (end of file or error). + */ +char * +fgets(char *restrict s, int n, FILE *restrict stream); + +/* + * Write the NUL-terminated string s to stream (no trailing newline is + * added). Returns a non-negative value, or EOF on error. + */ +int +fputs(const char *restrict s, FILE *restrict stream); + +/* + * Push c back onto the input stream; the next read returns it again. One + * byte of pushback is guaranteed. Returns c, or EOF on error (also for + * ungetc(EOF)). A successful seek discards the pushed-back character. + */ +int +ungetc(int c, FILE *stream); + +/* Position the stream (see SEEK_SET/SEEK_CUR/SEEK_END); clears feof. */ +int +fseek(FILE *stream, long offset, int whence); + +int +fseeko(FILE *stream, off_t offset, int whence); + +/* Current stream position, -1 with errno set on error. */ +long +ftell(FILE *stream); + +__attribute__((pure)) off_t +ftello(FILE *stream); + +/* Rewind to the start and clear feof/ferror (equivalent to + * fseeko(stream, 0, SEEK_SET) + clearerr). */ +void +rewind(FILE *stream); + +/* Get/set the opaque position via fpos_t. */ +int +fgetpos(FILE *restrict stream, fpos_t *restrict pos); + +int +fsetpos(FILE *stream, const fpos_t *pos); + +/* End-of-file and error indicators. */ +__attribute__((pure)) int +feof(FILE *stream); + +__attribute__((pure)) int +ferror(FILE *stream); + +void +clearerr(FILE *stream); + +/* + * Remove the file at path (a directory is removed like rmdir). Returns 0, + * or -1 with errno set. + */ +int +remove(const char *path); + +/* Rename oldpath to newpath. Returns 0, or -1 with errno set. */ +int +rename(const char *oldpath, const char *newpath); + +/* + * Create an anonymous temporary file ("w+b"): the file is created in /tmp + * and unlinked immediately, so it disappears on close. Returns the stream, + * or NULL with errno set. + */ +FILE * +tmpfile(void); + +/* The descriptor underlying the stream. */ +__attribute__((pure)) int +fileno(FILE *stream); + +/* getc/putc/getchar/putchar as macros over fgetc/fputc (see above). */ +#define getc(stream) fgetc(stream) +#define putc(c, stream) fputc((c), (stream)) +#define getchar() fgetc(stdin) +#define putchar(c) fputc((c), stdout) + +#if VLIBC_LEVEL_GE(2) + +/* + * Generate a name for a temporary file ("/tmp/vlibcXXXXXX" form; the file + * is NOT created). With s == NULL a static buffer is used. Not thread-safe, + * obsolescent. + */ +char * +tmpnam(char *s); + +/* Controlling terminal path: copies "/dev/tty" into s (or a static + * buffer when s == NULL) and returns it. */ +char * +ctermid(char *s); + +/* BSD: like setvbuf with _IOFBF/_IONBF and the given size. */ +void +setbuffer(FILE *stream, char *buf, size_t size); + +/* BSD: select line buffering (setvbuf with _IOLBF and NULL buffer). */ +void +setlinebuf(FILE *stream); + +/* glibc LFS name alias: identical to fopen on LP64. */ +FILE * +fopen64(const char *restrict path, const char *restrict mode); + +#endif /* VLIBC_LEVEL_GE(2) */ + +#ifdef __cplusplus +} +#endif + +#endif /* VLIBC_STDIO_H */ diff --git a/src/stdio/stdio.c b/src/stdio/stdio.c new file mode 100644 index 0000000..07a0dc5 --- /dev/null +++ b/src/stdio/stdio.c @@ -0,0 +1,1168 @@ +#ifdef HAVE_CONFIG_H +#include +#endif + +#include +#include +#include + +#include "../internal/malloc.h" +#include "../internal/syscall.h" +#include "stdio_impl.h" + +/* + * vlibc — buffered stdio core (todo 15). + * + * Every operation goes through the raw __syscallN seam (the public open/ + * read/write wrappers of todo 19 do not exist yet). Buffer management and + * the position invariant are documented in stdio_impl.h. + * + * Text vs binary: on Linux the two are identical, so the 'b' mode flag is + * accepted and ignored. + * + * std streams: stdin/stdout/stderr are static FILE objects over fds 0/1/2 + * with lazy initialization on first use. Buffering is decided from the + * descriptor type (fstat): a character device (tty) selects line buffering + * for stdout and stdin, while stderr is always unbuffered. All other + * descriptors are fully buffered. This matters for interactive programs + * and is what makes the interactive test behave. + * + * fflush(NULL) and the exit hook flush every stream with pending output; + * the open FILEs are tracked in a small internal registry list (plus the + * three static std streams). + * + * __vlibc_stdio_exit_flush() is the hook that exit() must call before + * terminating; wiring it into src/start/exit.c is owned by todo 3 (or the + * integration pass that follows this commit) — do not call it from here. + */ + +/* Kernel UAPI constants, defined locally until include/fcntl.h lands. */ +#define STDIO_AT_FDCWD (-100) +#define STDIO_O_RDONLY 0x0 +#define STDIO_O_WRONLY 0x1 +#define STDIO_O_RDWR 0x2 +#define STDIO_O_CREAT 0x40 +#define STDIO_O_EXCL 0x80 +#define STDIO_O_TRUNC 0x200 +#define STDIO_O_APPEND 0x400 +#define STDIO_O_CLOEXEC 0x80000 +#define STDIO_AT_REMOVEDIR 0x200 +#define STDIO_F_GETFL 3 + +/* st_mode type bits (kernel UAPI). */ +#define STDIO_S_IFMT 0170000 +#define STDIO_S_IFCHR 0020000 + +/* + * Minimal x86_64 struct stat — internal only, no kernel headers. Only + * st_mode is consumed; the field offsets follow the kernel's layout + * (mode at offset 24). + */ +struct stdio_stat +{ + unsigned long long st_dev; /* 0 */ + unsigned long long st_ino; /* 8 */ + unsigned long long st_nlink; /* 16 */ + unsigned int st_mode; /* 24 */ + unsigned int st_uid; /* 28 */ + unsigned int st_gid; /* 32 */ + unsigned int st_pad0; /* 36 */ + unsigned long long st_rdev; /* 40 */ + long long st_size; /* 48 */ + long st_blksize; /* 56 */ + long long st_blocks; /* 64 */ + long long st_atime_sec; /* 72 */ + long long st_atime_nsec; /* 80 */ + long long st_mtime_sec; /* 88 */ + long long st_mtime_nsec; /* 96 */ + long long st_ctime_sec; /* 104 */ + long long st_ctime_nsec; /* 112 */ + long long st_unused[3]; /* 120: kernel writes 144 bytes total */ +}; + +/* The three pre-wired standard streams: fd 0/1/2, stderr unbuffered. + * Buffers and line-buffering decisions are filled in lazily on first use. */ +static struct vlibc_FILE stdin_storage = { + .fd = 0, + .flags = F_READ, +}; +static struct vlibc_FILE stdout_storage = { + .fd = 1, + .flags = F_WRITE, +}; +static struct vlibc_FILE stderr_storage = { + .fd = 2, + .flags = F_WRITE | F_UNBUF, +}; + +FILE *stdin = (FILE *)&stdin_storage; +FILE *stdout = (FILE *)&stdout_storage; +FILE *stderr = (FILE *)&stderr_storage; + +/* Registry of dynamically opened streams, for fflush(NULL) and exit. */ +static FILE *open_list; + +/* Internal LCG for tmpfile/tmpnam suffixes, seeded from the pid (the + * mkstemp pattern of todo 13, kept independent of rand() and time()). */ +static unsigned long long stdio_rand_state; +static int stdio_rand_seeded; + +/* Forward declaration: the temp-name suffix generator. */ +static void +stdio_rand_fill(char *x); + +static void +stdio_rand_advance(void) +{ + if (!stdio_rand_seeded) + { + long pid = __syscall0(SYS_getpid); + + stdio_rand_state = ((unsigned long long)pid * 6364136223846793005ULL) + 1ULL; + stdio_rand_seeded = 1; + } + else + { + stdio_rand_state = (stdio_rand_state * 6364136223846793005ULL) + 1ULL; + } +} + +static void +stdio_rand_fill(char *x) +{ + static const char alphabet[] = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"; + int i; + + for (i = 0; i < 6; i++) + { + stdio_rand_advance(); + x[i] = alphabet[(size_t)((stdio_rand_state >> 33) % 62)]; + } +} + +/* True when fd is a character device (a terminal). */ +static int +stdio_isatty(int fd) +{ + /* Zero-init keeps the analyzer from flagging st_mode as garbage: the + * kernel write is guaranteed on the success path that reads it. */ + struct stdio_stat st = {0}; + + if (__syscall2(SYS_fstat, fd, (long)&st) < 0) + { + return 0; + } + return (st.st_mode & STDIO_S_IFMT) == STDIO_S_IFCHR; +} + +void +stdio_init_if_needed(FILE *f) +{ + if (f->buf != NULL) + { + return; + } + if (f->buf_size == 0) + { + /* A std stream: size the buffer and pick the buffering mode. */ + f->buf_size = (f->flags & F_UNBUF) ? 1 : BUFSIZ; + if ((f->flags & (F_LINEBUF | F_UNBUF)) == 0 && stdio_isatty(f->fd)) + { + f->flags |= F_LINEBUF; + } + } + f->buf = (unsigned char *)__libc_malloc(f->buf_size); + if (f->buf == NULL) + { + /* errno already ENOMEM from the allocator; mark the stream bad. */ + f->flags |= F_ERR; + return; + } + f->flags |= F_OWNBUF; + f->rpos = f->rstop = f->buf; + f->wpos = f->buf; + f->wstop = f->buf + f->buf_size; +} + +// NOLINTBEGIN(bugprone-easily-swappable-parameters) +int +stdio_parse_mode(const char *mode, int *m, int *oflags) +{ + int fl = 0; + int of = 0; + int i; + + switch (mode[0]) + { + case 'r': + fl |= F_READ; + break; + case 'w': + fl |= F_WRITE; + of |= STDIO_O_CREAT | STDIO_O_TRUNC; + break; + case 'a': + fl |= F_WRITE | F_APPEND; + of |= STDIO_O_CREAT | STDIO_O_APPEND; + break; + default: + return 0; + } + for (i = 1; mode[i] != '\0'; i++) + { + switch (mode[i]) + { + case '+': + fl |= F_READ | F_WRITE; + break; + case 'b': + /* Text/binary are identical on Linux. */ + break; + case 'x': + of |= STDIO_O_EXCL; + break; + default: + return 0; + } + } + if ((fl & F_READ) && (fl & F_WRITE)) + { + of |= STDIO_O_RDWR; + } + else if (fl & F_WRITE) + { + of |= STDIO_O_WRONLY; + } + else + { + of |= STDIO_O_RDONLY; + } + *m = fl; + *oflags = of; + return 1; +} +// NOLINTEND(bugprone-easily-swappable-parameters) + +off_t +stdio_lseek(int fd, off_t off, int whence) +{ + long r = __syscall3(SYS_lseek, fd, (long)off, whence); + + if (r < 0) + { + errno = -(int)r; + return (off_t)-1; + } + return (off_t)r; +} + +// NOLINTBEGIN(bugprone-easily-swappable-parameters) +FILE * +stdio_alloc_file(int fd, int m) +{ + FILE *f = (FILE *)__libc_malloc(sizeof(struct vlibc_FILE)); + + if (f == NULL) + { + return NULL; + } + f->buf = (unsigned char *)__libc_malloc(BUFSIZ); + if (f->buf == NULL) + { + __libc_free(f); + return NULL; + } + f->buf_size = BUFSIZ; + f->rpos = f->rstop = f->buf; + f->wpos = f->buf; + f->wstop = f->buf + BUFSIZ; + f->fd = fd; + f->flags = m | F_OWNBUF | F_HEAP; + f->pos = 0; + f->ungot = 0; + f->next = open_list; + open_list = f; + return f; +} +// NOLINTEND(bugprone-easily-swappable-parameters) + +void +stdio_refill(FILE *f) +{ + long n; + + f->rpos = f->rstop = f->buf; + n = __syscall3(SYS_read, f->fd, (long)f->buf, (long)f->buf_size); + if (n > 0) + { + f->rstop = f->buf + n; + } + else if (n == 0) + { + f->flags |= F_EOF; + } + else + { + (void)syscall_ret(n); /* sets errno */ + f->flags |= F_ERR; + } +} + +int +stdio_discard_read(FILE *f) +{ + if (f->rpos < f->rstop) + { + /* Unread buffered data: move the kernel position back to the + * logical position (the byte at rpos) before the switch. */ + if (stdio_lseek(f->fd, f->pos + (off_t)(f->rpos - f->buf), SEEK_SET) == (off_t)-1) + { + f->flags |= F_ERR; + return -1; + } + } + f->rpos = f->rstop = f->buf; + return 0; +} + +// NOLINTBEGIN(clang-analyzer-security.insecureAPI.DeprecatedOrUnsafeBufferHandling) +int +stdio_flush(FILE *f) +{ + size_t n; + long r; + + if (!(f->flags & F_WRITE)) + { + return 0; + } + n = (size_t)(f->wpos - f->buf); + if (n == 0) + { + return 0; + } + if (f->flags & F_APPEND) + { + /* Writes land at end of file regardless of the tracked pos. */ + off_t end = stdio_lseek(f->fd, 0, SEEK_END); + + if (end == (off_t)-1) + { + f->flags |= F_ERR; + return -1; + } + f->pos = end; + } + r = __syscall3(SYS_write, f->fd, (long)f->buf, (long)n); + if (r < 0) + { + (void)syscall_ret(r); /* sets errno */ + f->flags |= F_ERR; + return -1; + } + f->pos += (off_t)r; + if ((size_t)r < n) + { + /* Partial write: keep the unwritten remainder, report the error. */ + size_t keep = n - (size_t)r; + + __builtin_memmove(f->buf, f->buf + r, keep); + f->wpos = f->buf + keep; + f->flags |= F_ERR; + return -1; + } + f->wpos = f->buf; + return 0; +} +// NOLINTEND(clang-analyzer-security.insecureAPI.DeprecatedOrUnsafeBufferHandling) + +// NOLINTBEGIN(bugprone-easily-swappable-parameters) +FILE * +fopen(const char *restrict path, const char *restrict mode) +{ + int m; + int oflags; + int fd; + FILE *f; + + if (!stdio_parse_mode(mode, &m, &oflags)) + { + errno = EINVAL; + return NULL; + } + fd = syscall_ret( + __syscall4(SYS_openat, STDIO_AT_FDCWD, (long)path, (long)(oflags | STDIO_O_CLOEXEC), 0666)); + if (fd < 0) + { + return NULL; + } + f = stdio_alloc_file(fd, m); + if (f == NULL) + { + (void)syscall_ret(__syscall1(SYS_close, fd)); + return NULL; + } + return f; +} +// NOLINTEND(bugprone-easily-swappable-parameters) + +FILE * +fdopen(int fd, const char *mode) +{ + int m; + int oflags; + long fl; + int acc; + + if (fd < 0) + { + errno = EBADF; + return NULL; + } + if (!stdio_parse_mode(mode, &m, &oflags)) + { + errno = EINVAL; + return NULL; + } + fl = __syscall3(SYS_fcntl, fd, STDIO_F_GETFL, 0); + if (fl < 0) + { + (void)syscall_ret(fl); /* sets errno */ + return NULL; + } + acc = (int)fl & STDIO_O_RDWR; + if ((acc == STDIO_O_RDONLY && !(m & F_READ)) || (acc == STDIO_O_WRONLY && !(m & F_WRITE))) + { + /* The mode requests access the descriptor does not allow. */ + errno = EINVAL; + return NULL; + } + return stdio_alloc_file(fd, m); +} + +// NOLINTBEGIN(bugprone-easily-swappable-parameters) +FILE * +freopen(const char *restrict path, const char *restrict mode, FILE *restrict stream) +{ + int m; + int oflags; + int newfd; + + if (stream == NULL) + { + errno = EINVAL; + return NULL; + } + stdio_init_if_needed(stream); + if (!stdio_parse_mode(mode, &m, &oflags)) + { + errno = EINVAL; + return NULL; + } + if (path == NULL) + { + /* Change the mode only; keep the descriptor. */ + stream->flags = (stream->flags & ~(F_READ | F_WRITE | F_APPEND)) | m; + stream->flags &= ~(F_EOF | F_ERR | F_PUSHED); + return stream; + } + if (stream->flags & F_WRITE) + { + (void)stdio_flush(stream); + } + if (stream->fd >= 0) + { + (void)syscall_ret(__syscall1(SYS_close, stream->fd)); + } + stream->fd = -1; + newfd = syscall_ret( + __syscall4(SYS_openat, STDIO_AT_FDCWD, (long)path, (long)(oflags | STDIO_O_CLOEXEC), 0666)); + if (newfd < 0) + { + stream->flags |= F_ERR; + return NULL; + } + stream->fd = newfd; + stream->flags = (stream->flags & ~(F_READ | F_WRITE | F_APPEND | F_EOF | F_ERR | F_PUSHED)) | m; + stream->rpos = stream->rstop = stream->buf; + stream->wpos = stream->buf; + stream->pos = 0; + return stream; +} +// NOLINTEND(bugprone-easily-swappable-parameters) + +int +fclose(FILE *stream) +{ + int rc = 0; + + if (stream == NULL) + { + return EOF; + } + stdio_init_if_needed(stream); + if ((stream->flags & F_WRITE) && stdio_flush(stream) < 0) + { + rc = EOF; + } + if (stream->fd >= 0 && syscall_ret(__syscall1(SYS_close, stream->fd)) < 0) + { + rc = EOF; + } + stream->fd = -1; + if (stream->flags & F_HEAP) + { + FILE **link = &open_list; + + while (*link != NULL && *link != stream) + { + link = &(*link)->next; + } + if (*link == stream) + { + *link = stream->next; + } + __libc_free(stream); + } + return rc; +} + +int +fflush(FILE *stream) +{ + FILE *p; + FILE *const std_streams[3] = {stdin, stdout, stderr}; + int rc = 0; + int i; + + if (stream == NULL) + { + for (i = 0; i < 3; i++) + { + p = std_streams[i]; + stdio_init_if_needed(p); + if ((p->flags & F_WRITE) && stdio_flush(p) < 0) + { + rc = EOF; + } + } + for (p = open_list; p != NULL; p = p->next) + { + if ((p->flags & F_WRITE) && stdio_flush(p) < 0) + { + rc = EOF; + } + } + return rc; + } + stdio_init_if_needed(stream); + if (stream->flags & F_WRITE) + { + return stdio_flush(stream) < 0 ? EOF : 0; + } + /* Read-side stream: discard unread data, never corrupt. */ + if (stdio_discard_read(stream) < 0) + { + return EOF; + } + return 0; +} + +void +setbuf(FILE *restrict stream, char *restrict buf) +{ + (void)setvbuf(stream, buf, buf != NULL ? _IOFBF : _IONBF, BUFSIZ); +} + +int +setvbuf(FILE *restrict stream, char *restrict buf, int mode, size_t size) +{ + if (stream == NULL) + { + errno = EINVAL; + return -1; + } + if (mode != _IOFBF && mode != _IOLBF && mode != _IONBF) + { + errno = EINVAL; + return -1; + } + if (buf != NULL && size == 0 && mode != _IONBF) + { + errno = EINVAL; + return -1; + } + stdio_init_if_needed(stream); + /* No buffer may be pending while its shape changes. */ + if (stream->flags & F_WRITE) + { + (void)stdio_flush(stream); + } + (void)stdio_discard_read(stream); + stream->flags &= ~(F_LINEBUF | F_UNBUF | F_PUSHED); + if (mode == _IOLBF) + { + stream->flags |= F_LINEBUF; + } + if (mode == _IONBF) + { + stream->flags |= F_UNBUF; + } + if (buf != NULL) + { + if (stream->flags & F_OWNBUF) + { + __libc_free(stream->buf); + } + stream->buf = (unsigned char *)buf; + stream->buf_size = size; + stream->flags &= ~F_OWNBUF; + } + else + { + if (stream->flags & F_OWNBUF) + { + __libc_free(stream->buf); + } + /* Lazily allocated on first use (0 or 1, see init). */ + stream->buf = NULL; + stream->buf_size = (mode == _IONBF) ? 1 : BUFSIZ; + stream->flags &= ~F_OWNBUF; + } + stream->rpos = stream->rstop = stream->buf; + stream->wpos = stream->buf; + stream->wstop = stream->buf != NULL ? stream->buf + stream->buf_size : NULL; + return 0; +} + +int +fgetc(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 +fputc(int c, FILE *stream) +{ + stdio_init_if_needed(stream); + if (!(stream->flags & F_WRITE)) + { + stream->flags |= F_ERR; + return EOF; + } + if (stream->rpos < stream->rstop) + { + /* Pending read data: discard it before writing. */ + 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; +} + +size_t +fread(void *restrict ptr, size_t size, size_t nmemb, FILE *restrict stream) +{ + unsigned char *dst = (unsigned char *)ptr; + size_t items = 0; + size_t in_item = 0; + + if (size == 0 || nmemb == 0) + { + return 0; + } + while (items < nmemb) + { + int c; + + if (in_item == size) + { + items++; + in_item = 0; + dst += size; + continue; + } + c = fgetc(stream); + if (c == EOF) + { + return items; /* short read: EOF or error (feof/ferror) */ + } + dst[in_item++] = (unsigned char)c; + } + return items; +} + +size_t +fwrite(const void *restrict ptr, size_t size, size_t nmemb, FILE *restrict stream) +{ + const unsigned char *src = (const unsigned char *)ptr; + size_t items = 0; + + if (size == 0 || nmemb == 0) + { + return 0; + } + while (items < nmemb) + { + size_t i; + + for (i = 0; i < size; i++) + { + if (fputc(src[i], stream) == EOF) + { + return items; /* partial write: completed items only */ + } + } + src += size; + items++; + } + return items; +} + +char * +fgets(char *restrict s, int n, FILE *restrict stream) +{ + int i; + + if (n < 1) + { + errno = EINVAL; + return NULL; + } + if (n == 1) + { + s[0] = '\0'; + return s; + } + for (i = 0; i < n - 1; i++) + { + int c = fgetc(stream); + + if (c == EOF) + { + if (i == 0) + { + return NULL; + } + break; + } + s[i] = (char)c; + if (c == '\n') + { + i++; + break; + } + } + s[i] = '\0'; + return s; +} + +int +fputs(const char *restrict s, FILE *restrict stream) +{ + while (*s != '\0') + { + if (fputc((unsigned char)*s, stream) == EOF) + { + return EOF; + } + s++; + } + return 0; +} + +int +ungetc(int c, FILE *stream) +{ + stdio_init_if_needed(stream); + if (c == EOF) + { + return EOF; + } + stream->flags &= ~F_EOF; + if (stream->flags & F_PUSHED) + { + return EOF; /* only one byte of pushback is guaranteed */ + } + if (stream->flags & F_WRITE) + { + if (!(stream->flags & F_READ)) + { + return EOF; /* cannot push back on a write-only stream */ + } + if (stdio_flush(stream) < 0) + { + return EOF; + } + stream->flags &= ~F_WRITE; + stream->flags |= F_READ; + } + stream->ungot = (unsigned char)c; + stream->flags |= F_PUSHED; + return (unsigned char)c; +} + +// NOLINTBEGIN(bugprone-easily-swappable-parameters) +int +fseeko(FILE *stream, off_t offset, int whence) +{ + off_t target; + + stdio_init_if_needed(stream); + if (whence != SEEK_SET && whence != SEEK_CUR && whence != SEEK_END) + { + errno = EINVAL; + return -1; + } + if (stream->flags & F_WRITE) + { + if (stdio_flush(stream) < 0) + { + return -1; + } + } + if (stream->flags & F_PUSHED) + { + stream->flags &= ~F_PUSHED; /* a seek discards the pushback */ + } + stream->flags &= ~F_EOF; + if (whence == SEEK_SET) + { + target = offset; + } + else if (whence == SEEK_CUR) + { + target = stream->pos + (off_t)(stream->rpos - stream->buf) + offset; + } + else + { + target = (off_t)-1; + } + if (target != (off_t)-1 && target == stream->pos + (off_t)(stream->rpos - stream->buf) && + stream->rpos == stream->rstop) + { + /* The kernel is already at the target: nothing to do. */ + return 0; + } + 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); + } + else + { + stream->pos = stdio_lseek(stream->fd, offset, SEEK_END); + } + if (stream->pos == (off_t)-1) + { + return -1; + } + stream->rpos = stream->rstop = stream->buf; + return 0; +} +// NOLINTEND(bugprone-easily-swappable-parameters) + +int +fseek(FILE *stream, long offset, int whence) +{ + return fseeko(stream, (off_t)offset, whence); +} + +off_t +ftello(FILE *stream) +{ + stdio_init_if_needed(stream); + if (stream->flags & F_WRITE) + { + return stream->pos + (off_t)(stream->wpos - stream->buf); + } + return stream->pos + (off_t)(stream->rpos - stream->buf); +} + +long +ftell(FILE *stream) +{ + return (long)ftello(stream); +} + +void +rewind(FILE *stream) +{ + (void)fseeko(stream, 0, SEEK_SET); + stream->flags &= ~(F_EOF | F_ERR); +} + +int +fgetpos(FILE *restrict stream, fpos_t *restrict pos) +{ + off_t p = ftello(stream); + + if (p == (off_t)-1) + { + return -1; + } + *pos = (fpos_t)p; + return 0; +} + +int +fsetpos(FILE *stream, const fpos_t *pos) +{ + return fseeko(stream, (off_t)*pos, SEEK_SET); +} + +int +feof(FILE *stream) +{ + return (stream->flags & F_EOF) != 0; +} + +int +ferror(FILE *stream) +{ + return (stream->flags & F_ERR) != 0; +} + +void +clearerr(FILE *stream) +{ + stream->flags &= ~(F_EOF | F_ERR); +} + +int +remove(const char *path) +{ + long r = __syscall3(SYS_unlinkat, STDIO_AT_FDCWD, (long)path, 0); + + if (r == 0) + { + return 0; + } + if (r == -21 /* EISDIR */ || r == -1 /* EPERM on some filesystems */) + { + r = __syscall3(SYS_unlinkat, STDIO_AT_FDCWD, (long)path, STDIO_AT_REMOVEDIR); + } + return syscall_ret(r); +} + +int +rename(const char *oldpath, const char *newpath) +{ + return syscall_ret( + __syscall4(SYS_renameat, STDIO_AT_FDCWD, (long)oldpath, STDIO_AT_FDCWD, (long)newpath)); +} + +FILE * +tmpfile(void) +{ + char name[] = "/tmp/vlibc-tf-XXXXXX"; + int tries; + int fd; + + for (tries = 0; tries < 100; tries++) + { + stdio_rand_fill(name + 13); + fd = syscall_ret(__syscall4(SYS_openat, STDIO_AT_FDCWD, (long)name, + (long)(STDIO_O_RDWR | STDIO_O_CREAT | STDIO_O_EXCL), 0600)); + if (fd >= 0) + { + break; + } + if (errno != EEXIST) + { + return NULL; + } + } + if (fd < 0) + { + errno = EEXIST; + return NULL; + } + /* Unlink immediately: the file lives only as long as the stream. */ + if (syscall_ret(__syscall3(SYS_unlinkat, STDIO_AT_FDCWD, (long)name, 0)) < 0) + { + (void)syscall_ret(__syscall1(SYS_close, fd)); + return NULL; + } + return fdopen(fd, "w+"); +} + +int +fileno(FILE *stream) +{ + return stream->fd; +} + +/* Implementation of the macro-shadowed names: undef first. */ +#undef getc +#undef putc +#undef getchar +#undef putchar + +int +getc(FILE *stream) +{ + return fgetc(stream); +} + +int +putc(int c, FILE *stream) +{ + return fputc(c, stream); +} + +int +getchar(void) +{ + return fgetc(stdin); +} + +int +putchar(int c) +{ + return fputc(c, stdout); +} + +/* + * Exit-time hook: flush every stream with pending output. exit() (todo 3) + * does not call this yet — the wiring lands in a follow-up integration + * commit; this symbol exists so that commit (and vfprintf's users) can + * depend on it from day one. + */ +// NOLINTBEGIN(bugprone-reserved-identifier, misc-use-internal-linkage) +hidden void +__vlibc_stdio_exit_flush(void) +{ + (void)fflush(NULL); +} +// NOLINTEND(bugprone-reserved-identifier, misc-use-internal-linkage) + +#if VLIBC_LEVEL_GE(2) + +char * +tmpnam(char *s) +{ + static char buf[L_tmpnam]; + + if (s == NULL) + { + s = buf; + } + /* "/tmp/vlibcXXXXXX": 4 + 1 + 6 chars + NUL = 16 <= L_tmpnam. */ + s[0] = '/'; + s[1] = 't'; + s[2] = 'm'; + s[3] = 'p'; + s[4] = '/'; + s[5] = 'v'; + s[6] = 'l'; + s[7] = 'i'; + s[8] = 'b'; + s[9] = 'c'; + stdio_rand_fill(s + 10); + s[16] = '\0'; + return s; +} + +char * +ctermid(char *s) +{ + static char buf[L_ctermid]; + const char tty[] = "/dev/tty"; + int i; + + if (s == NULL) + { + s = buf; + } + for (i = 0; tty[i] != '\0'; i++) + { + s[i] = tty[i]; + } + s[i] = '\0'; + return s; +} + +void +setbuffer(FILE *stream, char *buf, size_t size) +{ + (void)setvbuf(stream, buf, buf != NULL ? _IOFBF : _IONBF, size); +} + +void +setlinebuf(FILE *stream) +{ + (void)setvbuf(stream, NULL, _IOLBF, 0); +} + +FILE * +fopen64(const char *restrict path, const char *restrict mode) +{ + return fopen(path, mode); /* identical ABI on LP64 */ +} + +#endif /* VLIBC_LEVEL_GE(2) */ diff --git a/src/stdio/stdio_impl.h b/src/stdio/stdio_impl.h new file mode 100644 index 0000000..08cb8c4 --- /dev/null +++ b/src/stdio/stdio_impl.h @@ -0,0 +1,94 @@ +#ifndef VLIBC_STDIO_STDIO_IMPL_H +#define VLIBC_STDIO_STDIO_IMPL_H + +/* + * vlibc — internal FILE layout and stream helpers (todo 15). + * + * struct vlibc_FILE is the object behind the opaque FILE of . + * Position invariant (authoritative): + * + * - Read mode: `pos` is the file offset of buf[0]. The logical position + * (offset of the byte at rpos) is pos + (rpos - buf), and the kernel + * position is pos + (rstop - buf); with an empty buffer + * (rpos == rstop) the kernel sits exactly at the logical position. + * - Write mode: `pos` is the file offset of buf[0]; the next written byte + * lands at pos + (wpos - buf). With an empty buffer the kernel sits at + * pos. A flush advances pos by the number of bytes written. + * + * Mode switching: reading while write data is pending flushes it first; + * writing while unread buffered data is pending seeks the kernel back to + * the logical position (discarding the unread data). F_PUSHED marks the + * single guaranteed ungetc pushback byte in `ungot`; a successful seek + * discards it. + * + * The helper prototypes below are shared with the formatted-I/O todos + * (16: vfprintf, 17: vfscanf) so they operate on the same buffer state. + */ + +#include + +#include "../internal/libc.h" + +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 */ +}; + +/* Stream flags. */ +#define F_READ 0x001 /* reads are enabled by the open mode */ +#define F_WRITE 0x002 /* writes are enabled by the open mode */ +#define F_EOF 0x004 /* end of file has been seen */ +#define F_ERR 0x008 /* an I/O error occurred */ +#define F_LINEBUF 0x010 /* line buffered: flush on newline */ +#define F_UNBUF 0x020 /* unbuffered: 1-byte buffer */ +#define F_APPEND 0x040 /* append mode: writes land at end of file */ +#define F_OWNBUF 0x080 /* buf was allocated by the library */ +#define F_HEAP 0x100 /* the FILE struct itself is heap-allocated */ +#define F_PUSHED 0x200 /* ungot holds a pushed-back byte */ + +/* Flush pending write data; returns 0, or -1 with F_ERR set. */ +hidden int +stdio_flush(FILE *f); + +/* Refill the read buffer from the descriptor (read mode only). */ +hidden void +stdio_refill(FILE *f); + +/* Discard unread buffered data, seeking the kernel back to pos. */ +hidden int +stdio_discard_read(FILE *f); + +/* Allocate the buffer lazily (std streams, setvbuf with NULL). */ +hidden void +stdio_init_if_needed(FILE *f); + +/* Allocate and register a FILE over an open descriptor. */ +hidden FILE * +stdio_alloc_file(int fd, int m); + +/* + * Parse a fopen-style mode string. On success stores the F_READ/F_WRITE + * capability bits in *m, the openat flags in *oflags, and returns 1; + * returns 0 for an invalid mode. + */ +hidden int +stdio_parse_mode(const char *mode, int *m, int *oflags); + +/* + * Raw lseek with errno translation. Separate from syscall_ret() because + * syscall_ret narrows results to int and would truncate large offsets. + */ +hidden off_t +stdio_lseek(int fd, off_t off, int whence); + +#endif /* VLIBC_STDIO_STDIO_IMPL_H */ diff --git a/tests/test_stdio.c b/tests/test_stdio.c new file mode 100644 index 0000000..77fad55 --- /dev/null +++ b/tests/test_stdio.c @@ -0,0 +1,718 @@ +/* + * vlibc — buffered stdio test (todo 15). + * + * Exercises the stdio FILE core end to end: + * + * 1. Round trip: fopen "w+b", fwrite a 1 MiB deterministic binary + * pattern, fflush, ftello == size, fseek(0, SEEK_SET), fread the + * pattern back, byte-identical. Then read past the end: fgetc == + * EOF, feof set, ferror clear. + * 2. Text lines: fputs lines, fgets line boundaries (newline kept, + * NUL-terminated), the n-1 clamp, NULL + feof at end of file. + * 3. Seek: fseeko SEEK_SET/CUR/END positions, ftell/ftello after + * fgetc, fgetpos/fsetpos round trip, rewind. + * 4. ungetc: pushback of a read character yields the same character; + * ungetc(EOF) == EOF; a second pushback fails; fseek discards the + * pushed-back byte. + * 5. fflush mid-stream then continue; fflush(NULL) flushes an open + * file stream and a freopen'd stdout. + * 6. setvbuf modes: _IONBF (NULL buffer), _IOFBF (user buffer), + * _IOLBF flushes on newline without an explicit fflush; setbuf + * with NULL selects unbuffered. + * 7. fdopen over a raw descriptor; fileno returns it; fclose closes + * it (probed with raw fcntl). + * 8. freopen: re-open a different path on the same FILE; NULL path + * changes the mode only. + * 9. remove/rename lifecycle; tmpfile write+rewind+read. + * 10. Level 2: tmpnam/ctermid/setbuffer/setlinebuf/fopen64. + * + * Failure mode (-f): fseek past EOF then fread returns 0 with feof set + * and no crash; fopen of a nonexistent path returns NULL; fdopen(-1) + * returns NULL; fputc on a read-only stream returns EOF with ferror; + * remove of a nonexistent path returns -1. The -f mode makes the + * library write errno in several places and then leaves through a raw + * SYS_exit_group before any host-libc cleanup runs (the + * tests/syscall_test.c discipline). + * + * The default mode keeps the stream error paths free of library errno + * writes, except one setvbuf EINVAL case which is bracketed with a + * save/restore of the host TCB slot 1 (the tests/test_env.c pattern). + * + * errno is never READ here. All diagnostics go through raw SYS_write; + * the only header included is vlibc's own (host headers would + * pull GCC internals shadowed by -Iinclude). + * + * Not part of the library proper; compiled manually for this todo (the + * tests/ + make check wiring is owned by a later todo). + */ + +#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 buffers. noipa keeps -O2 from folding the check. + * From here through the scenarios the analyzer is waived: the stream + * checker cannot model vlibc's FILE semantics (deliberate reads at EOF, + * reads after a failed probe, buffers written by our own fread), and its + * raw-syscall modeling flags byte comparisons as reading garbage. */ +// NOLINTBEGIN(clang-analyzer-unix.Stream, clang-analyzer-core.UndefinedBinaryOperatorResult, +// clang-analyzer-unix.StdCLibraryFunctions) +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) +{ + 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 (used by the L2 scenario). */ +#if VLIBC_LEVEL_GE(2) +static __attribute__((noipa)) int +prefix_eq(const char *s, const char *prefix) +{ + while (*prefix != '\0') + { + if (*s != *prefix) + { + return 0; + } + s++; + prefix++; + } + return 1; +} +#endif + +/* 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; +} + +/* Deterministic pattern generator (64-bit LCG, like the library's). */ +// NOLINTBEGIN(bugprone-easily-swappable-parameters) +static void +fill_pattern(unsigned char *p, unsigned long n, unsigned long long seed) +{ + unsigned long i; + unsigned long long s = seed; + + for (i = 0; i < n; i++) + { + s = (s * 6364136223846793005ULL) + 1ULL; + p[i] = (unsigned char)(s >> 33); + } +} +// NOLINTEND(bugprone-easily-swappable-parameters) + +/* 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; +} + +static unsigned char pattern[1 << 20]; +static unsigned char back[1 << 20]; + +/* 1. Binary round trip. */ +static void +roundtrip_scenario(void) +{ + const char path[] = "/tmp/vlibc-test-stdio-roundtrip.bin"; + FILE *f; + unsigned long w; + unsigned long r; + + f = fopen(path, "w+b"); + check(f != NULL, "fopen w+b succeeds"); + if (f == NULL) + { + return; + } + fill_pattern(pattern, sizeof(pattern), 42); + w = (unsigned long)fwrite(pattern, 1, sizeof(pattern), f); + check(w == sizeof(pattern), "fwrite writes the full 1 MiB"); + check(fflush(f) == 0, "fflush after fwrite returns 0"); + check(ftello(f) == (off_t)sizeof(pattern), "ftello after the write == size"); + check(fseek(f, 0, SEEK_SET) == 0, "fseek(0, SEEK_SET) returns 0"); + r = (unsigned long)fread(back, 1, sizeof(back), f); + check(r == sizeof(back), "fread reads the full 1 MiB"); + check(mem_eq(pattern, back, sizeof(pattern)), "1 MiB round trip is byte-identical"); + check(feof(f) == 0, "feof is clear after reading exactly to the end"); + check(fgetc(f) == EOF, "fgetc past the end returns EOF"); + check(feof(f) != 0, "feof is set after reading past the end"); + check(ferror(f) == 0, "ferror stays clear at EOF"); + clearerr(f); + check(feof(f) == 0, "clearerr clears feof"); + check(fclose(f) == 0, "fclose returns 0"); + check(remove(path) == 0, "remove deletes the round-trip file"); +} + +/* 2. Text lines. */ +static void +text_scenario(void) +{ + const char path[] = "/tmp/vlibc-test-stdio-text.txt"; + FILE *f; + char line[128]; + char small[4]; + + f = fopen(path, "w"); + check(f != NULL, "text fopen w succeeds"); + if (f == NULL) + { + return; + } + check(fputs("alpha\n", f) >= 0, "fputs line 1 returns non-negative"); + check(fputs("beta gamma\n", f) >= 0, "fputs line 2 returns non-negative"); + check(fputs("delta-no-newline", f) >= 0, "fputs final line without newline"); + check(fclose(f) == 0, "fclose text writer returns 0"); + + f = fopen(path, "r"); + check(f != NULL, "text fopen r succeeds"); + if (f == NULL) + { + return; + } + check(fgets(line, (int)sizeof(line), f) == line, "fgets line 1 returns s"); + check(str_eq(line, "alpha\n"), "fgets line 1 == \"alpha\\n\""); + check(fgets(line, (int)sizeof(line), f) == line, "fgets line 2 returns s"); + check(str_eq(line, "beta gamma\n"), "fgets line 2 == \"beta gamma\\n\""); + check(fgets(line, (int)sizeof(line), f) == line, "fgets final line returns s"); + check(str_eq(line, "delta-no-newline"), "fgets final line has no newline"); + check(fgets(line, (int)sizeof(line), f) == NULL, "fgets at EOF returns NULL"); + check(feof(f) != 0, "feof is set after the NULL fgets"); + check(fclose(f) == 0, "fclose text reader returns 0"); + + f = fopen(path, "r"); + check(fgets(small, (int)sizeof(small), f) == small, "fgets with n=4 returns s"); + check(small[0] == 'a' && small[1] == 'l' && small[2] == 'p' && small[3] == '\0', + "fgets clamps to n-1 chars and NUL-terminates"); + check(fclose(f) == 0, "fclose after the clamp check returns 0"); + check(remove(path) == 0, "remove deletes the text file"); +} + +/* 3. Seek and position queries. */ +static void +seek_scenario(void) +{ + const char path[] = "/tmp/vlibc-test-stdio-seek.bin"; + FILE *f; + fpos_t saved; + int c; + + f = fopen(path, "w+"); + check(f != NULL, "seek fopen w+ succeeds"); + if (f == NULL) + { + return; + } + check(fputs("0123456789", f) >= 0, "seek setup writes digits"); + check(fseeko(f, 3, SEEK_SET) == 0, "fseeko(3, SEEK_SET) returns 0"); + c = fgetc(f); + check(c == '3', "fgetc after SEEK_SET 3 == '3'"); + check(ftello(f) == 4, "ftello after reading one char == 4"); + check(fseek(f, 2, SEEK_CUR) == 0, "fseek(2, SEEK_CUR) returns 0"); + c = fgetc(f); + check(c == '6', "fgetc after SEEK_CUR 2 == '6'"); + check(ftell(f) == 7, "ftell after the SEEK_CUR read == 7"); + check(fseek(f, -2, SEEK_END) == 0, "fseek(-2, SEEK_END) returns 0"); + c = fgetc(f); + check(c == '8', "fgetc after SEEK_END -2 == '8'"); + check(fgetpos(f, &saved) == 0, "fgetpos returns 0"); + check((off_t)saved == 9, "fgetpos saved position == 9"); + check(fseek(f, 0, SEEK_SET) == 0, "fseek(0, SEEK_SET) returns 0"); + c = fgetc(f); + check(c == '0', "fgetc at the start == '0'"); + check(fsetpos(f, &saved) == 0, "fsetpos returns 0"); + c = fgetc(f); + check(c == '9', "fgetc after fsetpos == '9'"); + rewind(f); // NOLINT: rewind has no error return; its effect is asserted below + check(ftello(f) == 0, "rewind positions at 0"); + c = fgetc(f); + check(c == '0', "fgetc after rewind == '0'"); + check(fclose(f) == 0, "fclose seek stream returns 0"); + check(remove(path) == 0, "remove deletes the seek file"); +} + +/* 4. ungetc pushback. */ +static void +ungetc_scenario(void) +{ + const char path[] = "/tmp/vlibc-test-stdio-ungetc.txt"; + FILE *f; + int c; + + f = fopen(path, "w+"); + check(f != NULL, "ungetc fopen w+ succeeds"); + if (f == NULL) + { + return; + } + check(fputs("hello", f) >= 0, "ungetc setup writes \"hello\""); + check(fseek(f, 0, SEEK_SET) == 0, "ungetc setup seeks back"); + c = fgetc(f); + check(c == 'h', "fgetc reads 'h'"); + check(ungetc(c, f) == 'h', "ungetc('h') returns 'h'"); + c = fgetc(f); + check(c == 'h', "fgetc after ungetc reads 'h' again"); + check(ungetc(EOF, f) == EOF, "ungetc(EOF) returns EOF"); + check(ungetc('x', f) == 'x', "ungetc('x') returns 'x'"); + check(ungetc('y', f) == EOF, "second pushback without a read fails"); + c = fgetc(f); + check(c == 'x', "the first pushed-back byte is still readable"); + check(ungetc('z', f) == 'z', "ungetc('z') returns 'z'"); + check(fseek(f, 0, SEEK_SET) == 0, "fseek after ungetc returns 0"); + c = fgetc(f); + check(c == 'h', "fseek discards the pushed-back byte"); + check(fclose(f) == 0, "fclose ungetc stream returns 0"); + check(remove(path) == 0, "remove deletes the ungetc file"); +} + +/* 5. fflush mid-stream and fflush(NULL). */ +static void +flush_scenario(void) +{ + const char path[] = "/tmp/vlibc-test-stdio-flush.txt"; + const char outpath[] = "/tmp/vlibc-test-stdio-stdout.txt"; + char buf[64]; + FILE *f; + long got; + long saved_fd; + int r1; + int r2; + int r3; + int r4; + + f = fopen(path, "w"); + check(f != NULL, "flush fopen w succeeds"); + if (f == NULL) + { + return; + } + check(fwrite("abc", 1, 3, f) == 3, "fwrite the first half"); + check(fflush(f) == 0, "fflush mid-stream returns 0"); + check(fwrite("def", 1, 3, f) == 3, "fwrite the second half"); + + saved_fd = __syscall1(SYS_dup, 1); + check(saved_fd >= 0, "dup(1) saves the stdout descriptor"); + /* No check() output while fd 1 is the outpath file: the PASS lines + * would pollute the very file content the checks assert below. */ + r1 = (freopen(outpath, "w", stdout) == stdout); + r2 = (fputs("STDOUT-MARKER", stdout) >= 0); + r3 = (fflush(NULL) == 0); + r4 = (fclose(f) == 0); + __syscall2(SYS_dup2, saved_fd, 1); + __syscall1(SYS_close, saved_fd); + check(r1, "freopen stdout to a file"); + check(r2, "fputs to the freopen'd stdout"); + check(r3, "fflush(NULL) returns 0"); + check(r4, "fclose after fflush(NULL) returns 0"); + + got = raw_read_all(path, buf, sizeof(buf)); + check(got == 6 && mem_eq((const unsigned char *)buf, (const unsigned char *)"abcdef", 6), + "fflush(NULL) left the whole file content"); + got = raw_read_all(outpath, buf, sizeof(buf)); + check(got == 13 && + mem_eq((const unsigned char *)buf, (const unsigned char *)"STDOUT-MARKER", 13), + "fflush(NULL) flushed the freopen'd stdout"); + + check(remove(path) == 0, "remove deletes the flush file"); + check(remove(outpath) == 0, "remove deletes the stdout file"); +} + +/* 6. Buffering modes. */ +static void +setvbuf_scenario(void) +{ + const char path1[] = "/tmp/vlibc-test-stdio-setvbuf1.txt"; + const char path2[] = "/tmp/vlibc-test-stdio-setvbuf2.txt"; + const char path3[] = "/tmp/vlibc-test-stdio-setvbuf3.txt"; + char buf[64]; + static char userbuf[512]; + FILE *f; + unsigned long saved; + long got; + + f = fopen(path1, "w"); + check(f != NULL, "setvbuf fopen w succeeds"); + if (f == NULL) + { + return; + } + check(setvbuf(f, NULL, _IONBF, 0) == 0, "setvbuf _IONBF NULL returns 0"); + check(fputs("AB", f) >= 0, "fputs through the unbuffered stream"); + got = raw_read_all(path1, buf, sizeof(buf)); + check(got == 2 && buf[0] == 'A' && buf[1] == 'B', + "_IONBF data reaches the file without fflush"); + check(setvbuf(f, NULL, _IOFBF, BUFSIZ) == 0, "setvbuf _IOFBF NULL returns 0"); + check(fputs("CD", f) >= 0, "fputs through the fully buffered stream"); + check(fflush(f) == 0, "fflush the fully buffered stream"); + check(setvbuf(f, userbuf, _IOFBF, sizeof(userbuf)) == 0, + "setvbuf with a user buffer returns 0"); + check(fputs("EF", f) >= 0, "fputs through the user buffer"); + check(fflush(f) == 0, "fflush the user buffer"); + got = raw_read_all(path1, buf, sizeof(buf)); + check(got == 6 && mem_eq((const unsigned char *)buf, (const unsigned char *)"ABCDEF", 6), + "all three modes wrote sequential content"); + check(fclose(f) == 0, "fclose setvbuf stream returns 0"); + check(remove(path1) == 0, "remove deletes the setvbuf file"); + + f = fopen(path2, "w"); + check(setvbuf(f, NULL, _IOLBF, 0) == 0, "setvbuf _IOLBF NULL returns 0"); + check(fputs("line-one\n", f) >= 0, "fputs a newline-terminated line"); + got = raw_read_all(path2, buf, sizeof(buf)); + check(got == 9 && mem_eq((const unsigned char *)buf, (const unsigned char *)"line-one\n", 9), + "_IOLBF flushes on the newline without fflush"); + check(fputs("line-two\n", f) >= 0, "fputs a second line"); + check(fclose(f) == 0, "fclose line-buffered stream returns 0"); + got = raw_read_all(path2, buf, sizeof(buf)); + check(got == 18, "fclose flushed the second line"); + check(remove(path2) == 0, "remove deletes the line-buffered file"); + + f = fopen(path3, "w"); + setbuf(f, NULL); // NOLINT: setbuf has no error return; its effect is asserted below + check(fputs("SB", f) >= 0, "fputs through setbuf(NULL) stream"); + got = raw_read_all(path3, buf, sizeof(buf)); + check(got == 2 && buf[0] == 'S' && buf[1] == 'B', + "setbuf(NULL) data reaches the file without fflush"); + check(fclose(f) == 0, "fclose setbuf stream returns 0"); + check(remove(path3) == 0, "remove deletes the setbuf file"); + + saved = tcb_slot1_save(); + check(setvbuf(f, NULL, 99, 0) == -1, "setvbuf with an invalid mode returns -1"); + tcb_slot1_restore(saved); +} + +/* 7. fdopen and fileno. */ +static void +fdopen_scenario(void) +{ + const char path[] = "/tmp/vlibc-test-stdio-fdopen.txt"; + char line[64]; + FILE *g; + long fd; + long rc; + + fd = __syscall4(SYS_openat, -100, (long)path, (long)(0x2 | 0x40 | 0x200 /* RDWR|CREAT|TRUNC */), + 0666); + check(fd >= 0, "raw openat for fdopen succeeds"); + if (fd < 0) + { + return; + } + g = fdopen((int)fd, "w+"); + check(g != NULL, "fdopen wraps the descriptor"); + if (g == NULL) + { + __syscall1(SYS_close, fd); + return; + } + check(fileno(g) == (int)fd, "fileno returns the wrapped descriptor"); + check(fputs("via-fdopen", g) >= 0, "fputs through the fdopen'd stream"); + rewind(g); // NOLINT: rewind has no error return; its effect is asserted below + check(fgets(line, (int)sizeof(line), g) == line, "fgets after rewind returns s"); + check(str_eq(line, "via-fdopen"), "fgets reads back the fdopen'd content"); + check(fclose(g) == 0, "fclose fdopen stream returns 0"); + rc = __syscall3(SYS_fcntl, fd, 3 /* F_GETFL */, 0); + check(rc < 0, "fclose closed the underlying descriptor"); + check(remove(path) == 0, "remove deletes the fdopen file"); +} + +/* 8. freopen. */ +static void +freopen_scenario(void) +{ + const char path1[] = "/tmp/vlibc-test-stdio-freopen1.txt"; + const char path2[] = "/tmp/vlibc-test-stdio-freopen2.txt"; + char line[64]; + FILE *f; + + f = fopen(path1, "w"); + check(f != NULL, "freopen setup fopen w succeeds"); + if (f == NULL) + { + return; + } + check(fputs("one", f) >= 0, "freopen setup writes the first file"); + check(fclose(f) == 0, "fclose the first file"); + f = fopen(path1, "r"); + check(f != NULL, "freopen setup fopen r succeeds"); + if (f == NULL) + { + return; + } + check(freopen(NULL, "r", f) == f, "freopen with NULL path changes the mode only"); + check(fgets(line, (int)sizeof(line), f) == line, "fgets on the mode-changed stream"); + check(str_eq(line, "one"), "the mode change kept the descriptor"); + check(freopen(path2, "w", f) == f, "freopen rebinds to the second path"); + check(fputs("two", f) >= 0, "fputs through the rebound stream"); + check(fclose(f) == 0, "fclose the rebound stream"); + check(fopen(path2, "r") != NULL, "the second path exists"); + check(remove(path1) == 0, "remove deletes the first freopen file"); + check(remove(path2) == 0, "remove deletes the second freopen file"); +} + +/* 9. remove/rename and tmpfile. */ +static void +name_scenario(void) +{ + const char path1[] = "/tmp/vlibc-test-stdio-name1.txt"; + const char path2[] = "/tmp/vlibc-test-stdio-name2.txt"; + char line[64]; + FILE *f; + FILE *t; + + f = fopen(path1, "w"); + check(f != NULL, "name fopen w succeeds"); + if (f == NULL) + { + return; + } + check(fclose(f) == 0, "fclose the name file"); + check(rename(path1, path2) == 0, "rename returns 0"); + check(fopen(path1, "r") == NULL, "the old name is gone after rename"); + check(fopen(path2, "r") != NULL, "the new name exists after rename"); + check(remove(path2) == 0, "remove returns 0"); + check(fopen(path2, "r") == NULL, "the removed file is gone"); + + t = tmpfile(); + check(t != NULL, "tmpfile returns a stream"); + if (t != NULL) + { + check(fputs("temp-data", t) >= 0, "fputs through the tmpfile stream"); + rewind(t); // NOLINT: rewind has no error return; its effect is asserted below + check(fgets(line, (int)sizeof(line), t) == line, "fgets after rewind returns s"); + check(str_eq(line, "temp-data"), "tmpfile content round-trips"); + check(fclose(t) == 0, "fclose tmpfile returns 0"); + } +} + +#if VLIBC_LEVEL_GE(2) + +/* 10. Level 2 additions. */ +static void +level2_scenario(void) +{ + const char path[] = "/tmp/vlibc-test-stdio-l2.txt"; + char tname[L_tmpnam]; + char buf[64]; + char *p; + FILE *f; + static char userbuf[128]; + long got; + + p = tmpnam(tname); + check(p == tname, "tmpnam returns its argument"); + check(prefix_eq(tname, "/tmp/"), "tmpnam produces a /tmp name"); + check(tmpnam(NULL) != NULL, "tmpnam with NULL uses a static buffer"); + p = ctermid(NULL); + check(p != NULL && str_eq(p, "/dev/tty"), "ctermid returns \"/dev/tty\""); + + f = fopen64(path, "w"); + check(f != NULL, "fopen64 opens a stream"); + if (f != NULL) + { + check(fputs("l2", f) >= 0, "fputs through the fopen64 stream"); + check(fclose(f) == 0, "fclose the fopen64 stream"); + got = raw_read_all(path, buf, sizeof(buf)); + check(got == 2 && buf[0] == 'l' && buf[1] == '2', "fopen64 wrote the content"); + check(remove(path) == 0, "remove deletes the fopen64 file"); + } + + f = fopen(path, "w"); + setbuffer(f, userbuf, sizeof(userbuf)); + check(fputs("setbuffer", f) >= 0, "fputs through the setbuffer stream"); + check(fflush(f) == 0, "fflush the setbuffer stream"); + setlinebuf(f); + check(fputs("line\n", f) >= 0, "fputs a line through the setlinebuf stream"); + got = raw_read_all(path, buf, sizeof(buf)); + check(got == 14 && + mem_eq((const unsigned char *)buf, (const unsigned char *)"setbufferline\n", 14), + "setlinebuf flushed on the newline"); + check(fclose(f) == 0, "fclose the setlinebuf stream"); + check(remove(path) == 0, "remove deletes the setlinebuf file"); +} + +#endif /* VLIBC_LEVEL_GE(2) */ + +/* Failure scenarios (-f). */ +static int +failure_scenarios(void) +{ + const char path[] = "/tmp/vlibc-test-stdio-fail.bin"; + unsigned char small[8]; + FILE *f; + int rc; + + f = fopen(path, "w+b"); + if (f == NULL) + { + say(2, "FAIL: -f setup fopen failed\n"); + failures++; + } + else + { + check(fwrite("abc", 1, 3, f) == 3, "-f setup fwrite"); + check(fseek(f, 100, SEEK_SET) == 0, "fseek past EOF succeeds"); + check(ftello(f) == 100, "ftello after fseek past EOF == 100"); + rc = (int)fread(small, 1, sizeof(small), f); + check(rc == 0, "fread past EOF returns 0 items"); + check(feof(f) != 0, "feof is set after the past-EOF read"); + check(ferror(f) == 0, "ferror stays clear: it was EOF, not an error"); + check(fclose(f) == 0, "fclose the -f stream"); + check(remove(path) == 0, "remove the -f file"); + } + check(fopen("/tmp/vlibc-no-such-file-000", "r") == NULL, + "fopen of a nonexistent path returns NULL"); + check(fdopen(-1, "r") == NULL, "fdopen(-1) returns NULL"); + f = fopen(path, "r"); + if (f != NULL) + { + check(fputc('x', f) == EOF, "fputc on a read-only stream returns EOF"); + check(ferror(f) != 0, "ferror is set after the invalid fputc"); + check(fclose(f) == 0, "fclose the read-only stream"); + } + check(remove("/tmp/vlibc-no-such-file-000") == -1, "remove of a missing path returns -1"); + return failures > 0 ? 1 : 0; +} +// NOLINTEND(clang-analyzer-unix.Stream, clang-analyzer-core.UndefinedBinaryOperatorResult, +// clang-analyzer-unix.StdCLibraryFunctions) + +int +main(int argc, char **argv) +{ + if (argc == 2 && argv[1][0] == '-' && argv[1][1] == 'f') + { + /* + * The failure scenarios make the library write errno several + * times; leave via the raw syscall so the host cleanup never + * runs after those writes (see the banner). + */ + int rc = failure_scenarios(); + + __syscall1(SYS_exit_group, rc); + return rc; /* not reached */ + } + + roundtrip_scenario(); + text_scenario(); + seek_scenario(); + ungetc_scenario(); + flush_scenario(); + setvbuf_scenario(); + fdopen_scenario(); + freopen_scenario(); + name_scenario(); +#if VLIBC_LEVEL_GE(2) + level2_scenario(); +#endif + + if (failures > 0) + { + say(2, "FAILED ("); + say_dec(2, (unsigned long)failures); + say(2, " check(s))\n"); + return 1; + } + say(1, "all stdio tests passed\n"); + return 0; +}