Files
vlibc/tests/test_getline.c

612 lines
19 KiB
C

/*
* 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;
}