feat(stdio): getline/getdelim/stream locking

This commit is contained in:
2026-09-05 17:18:18 -04:00
parent 14c6fd6cf6
commit a2f7c33601
7 changed files with 1625 additions and 13 deletions
+82
View File
@@ -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 */
+503
View File
@@ -0,0 +1,503 @@
#ifdef HAVE_CONFIG_H
#include <config.h>
#endif
#include <errno.h>
#include <stddef.h>
#include <stdio.h>
#include <stdlib.h>
#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;
}
+142
View File
@@ -0,0 +1,142 @@
#ifdef HAVE_CONFIG_H
#include <config.h>
#endif
#include <errno.h>
#include <stddef.h>
#include <stdio.h>
#include <stdlib.h>
#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);
}
+178
View File
@@ -0,0 +1,178 @@
#ifdef HAVE_CONFIG_H
#include <config.h>
#endif
#include <errno.h>
#include <stddef.h>
#include <stdio.h>
#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);
}
+45 -2
View File
@@ -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
{
+64 -11
View File
@@ -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 <stdio.h>
#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 */
+611
View File
@@ -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 <stdio.h>
#include <stdlib.h>
#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;
}