596 lines
14 KiB
Diff
596 lines
14 KiB
Diff
commit 203452a460143c2b0bf80e0e92027e0fd6e19fa4
|
|
Author: Joseph Myers <josmyers@redhat.com>
|
|
Date: Tue Jan 28 23:39:12 2025 +0000
|
|
|
|
Add test of input file flushing / offset issues
|
|
|
|
Having fixed several bugs relating to flushing of FILE* streams (with
|
|
fflush and other operations) and their offsets (both the file position
|
|
indicator in the FILE*, and the offset in the underlying open file
|
|
description), especially after ungetc but not limited to that case,
|
|
add a test that more systematically covers different combinations of
|
|
cases for such issues, with 57220 separate scenarios tested (which
|
|
include examples of all the five separate fixed bugs), all of which
|
|
pass given the five previous bug fixes.
|
|
|
|
Tested for x86_64.
|
|
|
|
diff --git a/stdio-common/Makefile b/stdio-common/Makefile
|
|
index a49ab3701e88d4e6..fe4a7c4acc3932b2 100644
|
|
--- a/stdio-common/Makefile
|
|
+++ b/stdio-common/Makefile
|
|
@@ -268,6 +268,7 @@ tests := \
|
|
tst-printf-round \
|
|
tst-printfsz \
|
|
tst-put-error \
|
|
+ tst-read-offset \
|
|
tst-renameat2 \
|
|
tst-rndseek \
|
|
tst-scanf-bz27650 \
|
|
diff --git a/stdio-common/tst-read-offset.c b/stdio-common/tst-read-offset.c
|
|
new file mode 100644
|
|
index 0000000000000000..b8706607fc86da99
|
|
--- /dev/null
|
|
+++ b/stdio-common/tst-read-offset.c
|
|
@@ -0,0 +1,560 @@
|
|
+/* Test offsets in files being read, in particular with ungetc.
|
|
+ Copyright (C) 2025 Free Software Foundation, Inc.
|
|
+ This file is part of the GNU C Library.
|
|
+
|
|
+ The GNU C Library is free software; you can redistribute it and/or
|
|
+ modify it under the terms of the GNU Lesser General Public
|
|
+ License as published by the Free Software Foundation; either
|
|
+ version 2.1 of the License, or (at your option) any later version.
|
|
+
|
|
+ The GNU C Library is distributed in the hope that it will be useful,
|
|
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
|
+ Lesser General Public License for more details.
|
|
+
|
|
+ You should have received a copy of the GNU Lesser General Public
|
|
+ License along with the GNU C Library; if not, see
|
|
+ <https://www.gnu.org/licenses/>. */
|
|
+
|
|
+#include <dlfcn.h>
|
|
+#include <stdbool.h>
|
|
+#include <stdio.h>
|
|
+#include <stdlib.h>
|
|
+#include <unistd.h>
|
|
+
|
|
+#include <support/check.h>
|
|
+#include <support/temp_file.h>
|
|
+#include <support/xstdio.h>
|
|
+#include <support/xunistd.h>
|
|
+
|
|
+static volatile bool fail = false;
|
|
+
|
|
+/* Induce a malloc failure whenever FAIL is set. */
|
|
+void *
|
|
+malloc (size_t sz)
|
|
+{
|
|
+ if (fail)
|
|
+ return NULL;
|
|
+
|
|
+ static void *(*real_malloc) (size_t);
|
|
+
|
|
+ if (real_malloc == NULL)
|
|
+ real_malloc = dlsym (RTLD_NEXT, "malloc");
|
|
+
|
|
+ return real_malloc (sz);
|
|
+}
|
|
+
|
|
+/* The name of the temporary file used by all the tests. */
|
|
+static char *filename;
|
|
+
|
|
+/* st_blksize value for that file, or BUFSIZ if out of range. */
|
|
+static int blksize = BUFSIZ;
|
|
+
|
|
+/* Test data, both written to that file and used as an in-memory
|
|
+ stream. */
|
|
+char test_data[2 * BUFSIZ];
|
|
+
|
|
+/* Ways to open a test stream for reading (that may use different code
|
|
+ paths in libio). */
|
|
+enum test_open_case
|
|
+ {
|
|
+ test_open_fopen,
|
|
+ test_open_fopen_m,
|
|
+ test_open_fopen64,
|
|
+ test_open_fopen64_m,
|
|
+ test_open_fmemopen,
|
|
+ test_open_max
|
|
+ };
|
|
+
|
|
+static const char *const test_open_case_name[test_open_max] =
|
|
+ {
|
|
+ "fopen", "fopen(mmap)", "fopen64", "fopen64(mmap)", "fmemopen"
|
|
+ };
|
|
+
|
|
+static FILE *
|
|
+open_test_stream (enum test_open_case c)
|
|
+{
|
|
+ FILE *fp;
|
|
+ switch (c)
|
|
+ {
|
|
+ case test_open_fopen:
|
|
+ fp = fopen (filename, "r");
|
|
+ break;
|
|
+
|
|
+ case test_open_fopen_m:
|
|
+ fp = fopen (filename, "rm");
|
|
+ break;
|
|
+
|
|
+ case test_open_fopen64:
|
|
+ fp = fopen64 (filename, "r");
|
|
+ break;
|
|
+
|
|
+ case test_open_fopen64_m:
|
|
+ fp = fopen64 (filename, "rm");
|
|
+ break;
|
|
+
|
|
+ case test_open_fmemopen:
|
|
+ fp = fmemopen (test_data, 2 * BUFSIZ, "r");
|
|
+ break;
|
|
+
|
|
+ default:
|
|
+ abort ();
|
|
+ }
|
|
+ TEST_VERIFY_EXIT (fp != NULL);
|
|
+ return fp;
|
|
+}
|
|
+
|
|
+/* Base locations at which the main test (ungetc calls then doing
|
|
+ something that clears ungetc characters, then checking offset)
|
|
+ starts. */
|
|
+enum test_base_loc
|
|
+ {
|
|
+ base_loc_start,
|
|
+ base_loc_blksize,
|
|
+ base_loc_half,
|
|
+ base_loc_bufsiz,
|
|
+ base_loc_eof,
|
|
+ base_loc_max
|
|
+ };
|
|
+
|
|
+static int
|
|
+base_loc_to_bytes (enum test_base_loc loc, int offset)
|
|
+{
|
|
+ switch (loc)
|
|
+ {
|
|
+ case base_loc_start:
|
|
+ return offset;
|
|
+
|
|
+ case base_loc_blksize:
|
|
+ return blksize + offset;
|
|
+
|
|
+ case base_loc_half:
|
|
+ return BUFSIZ / 2 + offset;
|
|
+
|
|
+ case base_loc_bufsiz:
|
|
+ return BUFSIZ + offset;
|
|
+
|
|
+ case base_loc_eof:
|
|
+ return 2 * BUFSIZ + offset;
|
|
+
|
|
+ default:
|
|
+ abort ();
|
|
+ }
|
|
+}
|
|
+
|
|
+/* Ways to clear data from ungetc. */
|
|
+enum clear_ungetc_case
|
|
+ {
|
|
+ clear_fseek,
|
|
+ clear_fseekm1,
|
|
+ clear_fseekp1,
|
|
+ clear_fseeko,
|
|
+ clear_fseekom1,
|
|
+ clear_fseekop1,
|
|
+ clear_fseeko64,
|
|
+ clear_fseeko64m1,
|
|
+ clear_fseeko64p1,
|
|
+ clear_fsetpos,
|
|
+ clear_fsetposu,
|
|
+ clear_fsetpos64,
|
|
+ clear_fsetpos64u,
|
|
+ clear_fflush,
|
|
+ clear_fflush_null,
|
|
+ clear_fclose,
|
|
+ clear_max
|
|
+ };
|
|
+
|
|
+static const char *const clear_ungetc_case_name[clear_max] =
|
|
+ {
|
|
+ "fseek", "fseek(-1)", "fseek(1)", "fseeko", "fseeko(-1)", "fseeko(1)",
|
|
+ "fseeko64", "fseeko64(-1)", "fseeko64(1)", "fsetpos", "fsetpos(before)",
|
|
+ "fsetpos64", "fsetpos64(before)", "fflush", "fflush(NULL)", "fclose"
|
|
+ };
|
|
+
|
|
+static int
|
|
+clear_offset (enum clear_ungetc_case c, int num_ungetc)
|
|
+{
|
|
+ switch (c)
|
|
+ {
|
|
+ case clear_fseekm1:
|
|
+ case clear_fseekom1:
|
|
+ case clear_fseeko64m1:
|
|
+ return -1;
|
|
+
|
|
+ case clear_fseekp1:
|
|
+ case clear_fseekop1:
|
|
+ case clear_fseeko64p1:
|
|
+ return 1;
|
|
+
|
|
+ case clear_fsetposu:
|
|
+ case clear_fsetpos64u:
|
|
+ return num_ungetc;
|
|
+
|
|
+ default:
|
|
+ return 0;
|
|
+ }
|
|
+}
|
|
+
|
|
+/* The offsets used with fsetpos / fsetpos64. */
|
|
+static fpos_t pos;
|
|
+static fpos64_t pos64;
|
|
+
|
|
+static int
|
|
+do_clear_ungetc (FILE *fp, enum clear_ungetc_case c, int num_ungetc)
|
|
+{
|
|
+ int ret;
|
|
+ int offset = clear_offset (c, num_ungetc);
|
|
+ switch (c)
|
|
+ {
|
|
+ case clear_fseek:
|
|
+ case clear_fseekm1:
|
|
+ case clear_fseekp1:
|
|
+ ret = fseek (fp, offset, SEEK_CUR);
|
|
+ break;
|
|
+
|
|
+ case clear_fseeko:
|
|
+ case clear_fseekom1:
|
|
+ case clear_fseekop1:
|
|
+ ret = fseeko (fp, offset, SEEK_CUR);
|
|
+ break;
|
|
+
|
|
+ case clear_fseeko64:
|
|
+ case clear_fseeko64m1:
|
|
+ case clear_fseeko64p1:
|
|
+ ret = fseeko64 (fp, offset, SEEK_CUR);
|
|
+ break;
|
|
+
|
|
+ case clear_fsetpos:
|
|
+ case clear_fsetposu:
|
|
+ ret = fsetpos (fp, &pos);
|
|
+ break;
|
|
+
|
|
+ case clear_fsetpos64:
|
|
+ case clear_fsetpos64u:
|
|
+ ret = fsetpos64 (fp, &pos64);
|
|
+ break;
|
|
+
|
|
+ case clear_fflush:
|
|
+ ret = fflush (fp);
|
|
+ break;
|
|
+
|
|
+ case clear_fflush_null:
|
|
+ ret = fflush (NULL);
|
|
+ break;
|
|
+
|
|
+ case clear_fclose:
|
|
+ ret = fclose (fp);
|
|
+ break;
|
|
+
|
|
+ default:
|
|
+ abort();
|
|
+ }
|
|
+ TEST_COMPARE (ret, 0);
|
|
+ return offset;
|
|
+}
|
|
+
|
|
+static bool
|
|
+clear_valid (enum test_open_case c, enum clear_ungetc_case cl)
|
|
+{
|
|
+ switch (c)
|
|
+ {
|
|
+ case test_open_fmemopen:
|
|
+ /* fflush is not valid for input memory streams, and fclose is
|
|
+ useless for this test for such streams because there is no
|
|
+ underlying open file description for which an offset could be
|
|
+ checked after fclose. */
|
|
+ switch (cl)
|
|
+ {
|
|
+ case clear_fflush:
|
|
+ case clear_fflush_null:
|
|
+ case clear_fclose:
|
|
+ return false;
|
|
+
|
|
+ default:
|
|
+ return true;
|
|
+ }
|
|
+
|
|
+ default:
|
|
+ /* All ways of clearing ungetc state are valid for streams with
|
|
+ an underlying file. */
|
|
+ return true;
|
|
+ }
|
|
+}
|
|
+
|
|
+static bool
|
|
+clear_closes_file (enum clear_ungetc_case cl)
|
|
+{
|
|
+ switch (cl)
|
|
+ {
|
|
+ case clear_fclose:
|
|
+ return true;
|
|
+
|
|
+ default:
|
|
+ return false;
|
|
+ }
|
|
+}
|
|
+
|
|
+static void
|
|
+clear_getpos_before (FILE *fp, enum clear_ungetc_case c)
|
|
+{
|
|
+ switch (c)
|
|
+ {
|
|
+ case clear_fsetposu:
|
|
+ TEST_COMPARE (fgetpos (fp, &pos), 0);
|
|
+ break;
|
|
+
|
|
+ case clear_fsetpos64u:
|
|
+ TEST_COMPARE (fgetpos64 (fp, &pos64), 0);
|
|
+ break;
|
|
+
|
|
+ default:
|
|
+ break;
|
|
+ }
|
|
+}
|
|
+
|
|
+static void
|
|
+clear_getpos_after (FILE *fp, enum clear_ungetc_case c)
|
|
+{
|
|
+ switch (c)
|
|
+ {
|
|
+ case clear_fsetpos:
|
|
+ TEST_COMPARE (fgetpos (fp, &pos), 0);
|
|
+ break;
|
|
+
|
|
+ case clear_fsetpos64:
|
|
+ TEST_COMPARE (fgetpos64 (fp, &pos64), 0);
|
|
+ break;
|
|
+
|
|
+ default:
|
|
+ break;
|
|
+ }
|
|
+}
|
|
+
|
|
+/* Ways to verify results of clearing ungetc data. */
|
|
+enum verify_case
|
|
+ {
|
|
+ verify_read,
|
|
+ verify_ftell,
|
|
+ verify_ftello,
|
|
+ verify_ftello64,
|
|
+ verify_fd,
|
|
+ verify_max
|
|
+ };
|
|
+
|
|
+static const char *const verify_case_name[verify_max] =
|
|
+ {
|
|
+ "read", "ftell", "ftello", "ftello64", "fd"
|
|
+ };
|
|
+
|
|
+static bool
|
|
+valid_fd_offset (enum test_open_case c, enum clear_ungetc_case cl)
|
|
+{
|
|
+ switch (c)
|
|
+ {
|
|
+ case test_open_fmemopen:
|
|
+ /* No open file description. */
|
|
+ return false;
|
|
+
|
|
+ default:
|
|
+ /* fseek does not necessarily set the offset for the underlying
|
|
+ open file description ("If the most recent operation, other
|
|
+ than ftell(), on a given stream is fflush(), the file offset
|
|
+ in the underlying open file description shall be adjusted to
|
|
+ reflect the location specified by fseek()." in POSIX does not
|
|
+ include the case here where getc was the last operation).
|
|
+ Similarly, fsetpos does not necessarily set that offset
|
|
+ either. */
|
|
+ switch (cl)
|
|
+ {
|
|
+ case clear_fflush:
|
|
+ case clear_fflush_null:
|
|
+ case clear_fclose:
|
|
+ return true;
|
|
+
|
|
+ default:
|
|
+ return false;
|
|
+ }
|
|
+ }
|
|
+}
|
|
+
|
|
+static bool
|
|
+verify_valid (enum test_open_case c, enum clear_ungetc_case cl,
|
|
+ enum verify_case v)
|
|
+{
|
|
+ switch (v)
|
|
+ {
|
|
+ case verify_fd:
|
|
+ return valid_fd_offset (c, cl);
|
|
+
|
|
+ default:
|
|
+ switch (cl)
|
|
+ {
|
|
+ case clear_fclose:
|
|
+ return false;
|
|
+
|
|
+ default:
|
|
+ return true;
|
|
+ }
|
|
+ }
|
|
+}
|
|
+
|
|
+static bool
|
|
+verify_uses_fd (enum verify_case v)
|
|
+{
|
|
+ switch (v)
|
|
+ {
|
|
+ case verify_fd:
|
|
+ return true;
|
|
+
|
|
+ default:
|
|
+ return false;
|
|
+ }
|
|
+}
|
|
+
|
|
+static int
|
|
+read_to_test_loc (FILE *fp, enum test_base_loc loc, int offset)
|
|
+{
|
|
+ int to_read = base_loc_to_bytes (loc, offset);
|
|
+ for (int i = 0; i < to_read; i++)
|
|
+ TEST_COMPARE (getc (fp), (unsigned char) i);
|
|
+ return to_read;
|
|
+}
|
|
+
|
|
+static void
|
|
+setup (void)
|
|
+{
|
|
+ int fd = create_temp_file ("tst-read-offset", &filename);
|
|
+ TEST_VERIFY_EXIT (fd != -1);
|
|
+ struct stat64 st;
|
|
+ xfstat64 (fd, &st);
|
|
+ if (st.st_blksize > 0 && st.st_blksize < BUFSIZ)
|
|
+ blksize = st.st_blksize;
|
|
+ printf ("BUFSIZ = %d, blksize = %d\n", BUFSIZ, blksize);
|
|
+ xclose (fd);
|
|
+ FILE *fp = xfopen (filename, "w");
|
|
+ for (size_t i = 0; i < 2 * BUFSIZ; i++)
|
|
+ {
|
|
+ unsigned char c = i;
|
|
+ TEST_VERIFY_EXIT (fputc (c, fp) == c);
|
|
+ test_data[i] = c;
|
|
+ }
|
|
+ xfclose (fp);
|
|
+}
|
|
+
|
|
+static void
|
|
+test_one_case (enum test_open_case c, enum test_base_loc loc, int offset,
|
|
+ int num_ungetc, int num_ungetc_diff, bool ungetc_fallback,
|
|
+ enum clear_ungetc_case cl, enum verify_case v)
|
|
+{
|
|
+ int full_offset = base_loc_to_bytes (loc, offset);
|
|
+ printf ("Testing %s offset %d ungetc %d different %d %s%s %s\n",
|
|
+ test_open_case_name[c], full_offset, num_ungetc, num_ungetc_diff,
|
|
+ ungetc_fallback ? "fallback " : "", clear_ungetc_case_name[cl],
|
|
+ verify_case_name[v]);
|
|
+ FILE *fp = open_test_stream (c);
|
|
+ int cur_offset = read_to_test_loc (fp, loc, offset);
|
|
+ clear_getpos_before (fp, cl);
|
|
+ for (int i = 0; i < num_ungetc; i++)
|
|
+ {
|
|
+ unsigned char c = (i >= num_ungetc - num_ungetc_diff
|
|
+ ? cur_offset
|
|
+ : cur_offset - 1);
|
|
+ if (ungetc_fallback)
|
|
+ fail = true;
|
|
+ TEST_COMPARE (ungetc (c, fp), c);
|
|
+ fail = false;
|
|
+ cur_offset--;
|
|
+ }
|
|
+ clear_getpos_after (fp, cl);
|
|
+ int fd = -1;
|
|
+ bool done_dup = false;
|
|
+ if (verify_uses_fd (v))
|
|
+ {
|
|
+ fd = fileno (fp);
|
|
+ TEST_VERIFY (fd != -1);
|
|
+ if (clear_closes_file (cl))
|
|
+ {
|
|
+ fd = xdup (fd);
|
|
+ done_dup = true;
|
|
+ }
|
|
+ }
|
|
+ cur_offset += do_clear_ungetc (fp, cl, num_ungetc);
|
|
+ switch (v)
|
|
+ {
|
|
+ case verify_read:
|
|
+ for (;
|
|
+ cur_offset <= full_offset + 1 && cur_offset < 2 * BUFSIZ;
|
|
+ cur_offset++)
|
|
+ TEST_COMPARE (getc (fp), (unsigned char) cur_offset);
|
|
+ break;
|
|
+
|
|
+ case verify_ftell:
|
|
+ TEST_COMPARE (ftell (fp), cur_offset);
|
|
+ break;
|
|
+
|
|
+ case verify_ftello:
|
|
+ TEST_COMPARE (ftello (fp), cur_offset);
|
|
+ break;
|
|
+
|
|
+ case verify_ftello64:
|
|
+ TEST_COMPARE (ftello64 (fp), cur_offset);
|
|
+ break;
|
|
+
|
|
+ case verify_fd:
|
|
+ TEST_COMPARE (lseek (fd, 0, SEEK_CUR), cur_offset);
|
|
+ break;
|
|
+
|
|
+ default:
|
|
+ abort ();
|
|
+ }
|
|
+ if (! clear_closes_file (cl))
|
|
+ {
|
|
+ int ret = fclose (fp);
|
|
+ TEST_COMPARE (ret, 0);
|
|
+ }
|
|
+ if (done_dup)
|
|
+ xclose (fd);
|
|
+}
|
|
+
|
|
+int
|
|
+do_test (void)
|
|
+{
|
|
+ setup ();
|
|
+ for (enum test_open_case c = 0; c < test_open_max; c++)
|
|
+ for (enum test_base_loc loc = 0; loc < base_loc_max; loc++)
|
|
+ for (int offset = -2; offset <= 3; offset++)
|
|
+ for (int num_ungetc = 0;
|
|
+ num_ungetc <= 2 && num_ungetc <= base_loc_to_bytes (loc, offset);
|
|
+ num_ungetc++)
|
|
+ for (int num_ungetc_diff = 0;
|
|
+ num_ungetc_diff <= num_ungetc;
|
|
+ num_ungetc_diff++)
|
|
+ for (int ungetc_fallback = 0;
|
|
+ ungetc_fallback <= (num_ungetc == 1 ? 1 : 0);
|
|
+ ungetc_fallback++)
|
|
+ for (enum clear_ungetc_case cl = 0; cl < clear_max; cl++)
|
|
+ {
|
|
+ if (!clear_valid (c, cl))
|
|
+ continue;
|
|
+ if (base_loc_to_bytes (loc, offset) > 2 * BUFSIZ)
|
|
+ continue;
|
|
+ if ((base_loc_to_bytes (loc, offset)
|
|
+ - num_ungetc
|
|
+ + clear_offset (cl, num_ungetc)) < 0)
|
|
+ continue;
|
|
+ if ((base_loc_to_bytes (loc, offset)
|
|
+ - num_ungetc
|
|
+ + clear_offset (cl, num_ungetc)) > 2 * BUFSIZ)
|
|
+ continue;
|
|
+ for (enum verify_case v = 0; v < verify_max; v++)
|
|
+ {
|
|
+ if (!verify_valid (c, cl, v))
|
|
+ continue;
|
|
+ test_one_case (c, loc, offset, num_ungetc,
|
|
+ num_ungetc_diff, ungetc_fallback, cl, v);
|
|
+ }
|
|
+ }
|
|
+ return 0;
|
|
+}
|
|
+
|
|
+#include <support/test-driver.c>
|