feat(stdio): getline/getdelim/stream locking
This commit is contained in:
@@ -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);
|
||||
}
|
||||
+45
-2
@@ -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
@@ -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 */
|
||||
|
||||
Reference in New Issue
Block a user