feat(stdio): getline/getdelim/stream locking
This commit is contained in:
@@ -347,4 +347,86 @@ int
|
|||||||
vasprintf(char **restrict strp, const char *restrict format, va_list ap);
|
vasprintf(char **restrict strp, const char *restrict format, va_list ap);
|
||||||
#endif
|
#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 */
|
#endif /* VLIBC_STDIO_H */
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
+44
-1
@@ -284,6 +284,9 @@ stdio_alloc_file(int fd, int m)
|
|||||||
}
|
}
|
||||||
f->pos = 0;
|
f->pos = 0;
|
||||||
f->ungot = 0;
|
f->ungot = 0;
|
||||||
|
f->lock = 0;
|
||||||
|
f->lock_owner = NULL;
|
||||||
|
f->mem = NULL;
|
||||||
f->next = open_list;
|
f->next = open_list;
|
||||||
open_list = f;
|
open_list = f;
|
||||||
return f;
|
return f;
|
||||||
@@ -295,6 +298,13 @@ stdio_refill(FILE *f)
|
|||||||
{
|
{
|
||||||
long n;
|
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;
|
f->rpos = f->rstop = f->buf;
|
||||||
n = __syscall3(SYS_read, f->fd, (long)f->buf, (long)f->buf_size);
|
n = __syscall3(SYS_read, f->fd, (long)f->buf, (long)f->buf_size);
|
||||||
if (n > 0)
|
if (n > 0)
|
||||||
@@ -315,6 +325,13 @@ stdio_refill(FILE *f)
|
|||||||
int
|
int
|
||||||
stdio_discard_read(FILE *f)
|
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)
|
if (f->rpos < f->rstop)
|
||||||
{
|
{
|
||||||
/* Unread buffered data: move the kernel position back to the
|
/* Unread buffered data: move the kernel position back to the
|
||||||
@@ -340,6 +357,13 @@ stdio_flush(FILE *f)
|
|||||||
size_t n;
|
size_t n;
|
||||||
long r;
|
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))
|
if (!(f->flags & F_WRITE))
|
||||||
{
|
{
|
||||||
return 0;
|
return 0;
|
||||||
@@ -522,6 +546,13 @@ fclose(FILE *stream)
|
|||||||
{
|
{
|
||||||
rc = EOF;
|
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)
|
if (stream->fd >= 0 && syscall_ret(__syscall1(SYS_close, stream->fd)) < 0)
|
||||||
{
|
{
|
||||||
rc = EOF;
|
rc = EOF;
|
||||||
@@ -946,9 +977,21 @@ fseeko(FILE *stream, off_t offset, int whence)
|
|||||||
}
|
}
|
||||||
if (target != (off_t)-1)
|
if (target != (off_t)-1)
|
||||||
{
|
{
|
||||||
/* SEEK_SET/SEEK_CUR: seek the kernel to the absolute target. */
|
/* 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);
|
stream->pos = stdio_lseek(stream->fd, target, SEEK_SET);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
else if (stream->mem != NULL)
|
||||||
|
{
|
||||||
|
stream->pos = stdio_mem_lseek(stream, offset, SEEK_END);
|
||||||
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
stream->pos = stdio_lseek(stream->fd, offset, SEEK_END);
|
stream->pos = stdio_lseek(stream->fd, offset, SEEK_END);
|
||||||
|
|||||||
@@ -25,12 +25,32 @@
|
|||||||
*
|
*
|
||||||
* The helper prototypes below are shared with the formatted-I/O todos
|
* The helper prototypes below are shared with the formatted-I/O todos
|
||||||
* (16: vfprintf, 17: vfscanf) so they operate on the same buffer state.
|
* (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 <stdio.h>
|
||||||
|
|
||||||
#include "../internal/libc.h"
|
#include "../internal/libc.h"
|
||||||
|
|
||||||
|
/* Opaque memory-stream backend (fmemopen/open_memstream, todo 18). */
|
||||||
|
struct vlibc_memstream;
|
||||||
|
|
||||||
struct vlibc_FILE
|
struct vlibc_FILE
|
||||||
{
|
{
|
||||||
unsigned char *buf; /* buffer base: library- or user-supplied */
|
unsigned char *buf; /* buffer base: library- or user-supplied */
|
||||||
@@ -44,6 +64,9 @@ struct vlibc_FILE
|
|||||||
off_t pos; /* position per the invariant above */
|
off_t pos; /* position per the invariant above */
|
||||||
struct vlibc_FILE *next; /* registry link for fflush(NULL) */
|
struct vlibc_FILE *next; /* registry link for fflush(NULL) */
|
||||||
unsigned char ungot; /* pushed-back byte when F_PUSHED */
|
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. */
|
/* Stream flags. */
|
||||||
@@ -94,4 +117,34 @@ stdio_parse_mode(const char *mode, int *m, int *oflags);
|
|||||||
hidden off_t
|
hidden off_t
|
||||||
stdio_lseek(int fd, off_t off, int whence);
|
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 */
|
#endif /* VLIBC_STDIO_STDIO_IMPL_H */
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user