/* Test if fwrite returns consistent values on partial writes. 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 <errno.h> /* stdio.h provides BUFSIZ, which is the size of fwrite's internal buffer. */ #include <stdio.h> #include <stdlib.h> #include <string.h> #include <support/check.h> #include <support/fuse.h> #include <support/support.h> #include <support/xstdio.h> #include <support/temp_file.h> /* Length of the buffer in bytes. */ #define INBUF_SIZE (BUFSIZ) /* Amount of bytes written to fwrite's internal cache that trigger a flush. */ #define CACHE_THRESHOLD (BUFSIZ / 2) #define ITERATIONS 1000 /* Maximum number of bytes written during a partial write. */ #define PARTIAL_BYTES 4 #define EXPECT_EVENT(opcode, state, expected_state) \ { \ if (state != expected_state) \ { \ char *s = support_fuse_opcode (opcode); \ FAIL ("unexpected event %s at state %d", s, state); \ free (s); \ } \ } /* The goal of this test is to check that file position of a file stream is correctly updated when write () returns a partial write. The file system simulates pseudorandom partial writes while the test is running. Meanwhile the main thread calls fwrite () with a large object first and small objects later. The usage of a large enough object ensures that fwrite's internal cache is full enough, without triggering a write to file. Subsequent calls to fwrite are guaranteed to trigger a write to file. */ static void fuse_thread (struct support_fuse *f, void *closure) { struct fuse_in_header *inh; int state = 0; while ((inh = support_fuse_next (f)) != NULL) { { char *opcode = support_fuse_opcode (inh->opcode); printf ("info: (T) event %s(%llu) len=%u nodeid=%llu\n", opcode, (unsigned long long int) inh->unique, inh->len, (unsigned long long int) inh->nodeid); free (opcode); } /* Handle mountpoint and basic directory operation for the root (1). */ if (support_fuse_handle_mountpoint (f) || (inh->nodeid == 1 && support_fuse_handle_directory (f))) continue; switch (inh->opcode) { case FUSE_LOOKUP: EXPECT_EVENT (inh->nodeid, state, 0); state++; support_fuse_reply_error (f, ENOENT); break; case FUSE_CREATE: EXPECT_EVENT (inh->nodeid, state, 1); state++; struct fuse_entry_out *entry; struct fuse_open_out *open; support_fuse_prepare_create (f, 2, &entry, &open); entry->attr.mode = S_IFREG | 0600; support_fuse_reply_prepared (f); break; case FUSE_GETXATTR: /* We don't need to support extended attributes in this test. */ support_fuse_reply_error (f, ENOSYS); break; case FUSE_GETATTR: /* Happens after open. */ if (inh->nodeid == 2) { struct fuse_attr_out *out = support_fuse_prepare_attr (f); out->attr.mode = S_IFREG | 0600; out->attr.size = 0; support_fuse_reply_prepared (f); } else support_fuse_reply_error (f, ENOENT); break; case FUSE_WRITE: if (inh->nodeid == 2) { struct fuse_write_out out; if (state > 1 && state < ITERATIONS + 2) { /* The 2nd and subsequent calls to fwrite () trigger a flush of fwrite's internal cache. Simulate a partial write of up to PARTIAL_BYTES bytes. */ out.padding = 0; out.size = 1 + rand () % PARTIAL_BYTES, state++; support_fuse_reply (f, &out, sizeof (out)); } else if (state >= ITERATIONS + 2) { /* This request is expected to come from fflush (). Copy all the data successfully. This may be executed more than once. */ struct fuse_write_in *p = support_fuse_cast (WRITE, inh); out.padding = 0; out.size = p->size, state++; support_fuse_reply (f, &out, sizeof (out)); } else support_fuse_reply_error (f, EIO); } else support_fuse_reply_error (f, EIO); break; case FUSE_FLUSH: case FUSE_RELEASE: TEST_COMPARE (inh->nodeid, 2); support_fuse_reply_empty (f); break; default: FAIL ("unexpected event %s", support_fuse_opcode (inh->opcode)); support_fuse_reply_error (f, EIO); } } } static int do_test (void) { char *in; int i; size_t written; _Static_assert (CACHE_THRESHOLD <= INBUF_SIZE, "the input buffer must be larger than the cache threshold"); /* Avoid filling up fwrite's cache. */ _Static_assert (CACHE_THRESHOLD - 1 + PARTIAL_BYTES * ITERATIONS <= BUFSIZ, "fwrite's cache must fit all data written"); support_fuse_init (); struct support_fuse *fs = support_fuse_mount (fuse_thread, NULL); /* Create and open a temporary file in the fuse mount point. */ char *fname = xasprintf ("%s/%sXXXXXX", support_fuse_mountpoint (fs), "tst-fwrite-fuse"); int fd = mkstemp (fname); TEST_VERIFY_EXIT (fd != -1); FILE *f = fdopen (fd, "w"); TEST_VERIFY_EXIT (f != NULL); /* Allocate an input array that will be written to the temporary file. */ in = xmalloc (INBUF_SIZE); for (i = 0; i < INBUF_SIZE; i++) in[i] = i % 0xff; /* Ensure the file position indicator is at the beginning of the stream. */ TEST_COMPARE (ftell (f), 0); /* Try to fill as most data to the cache of the file stream as possible with a single large object. All data is expected to be written to the cache. No errors are expected from this. */ TEST_COMPARE (fwrite (in, CACHE_THRESHOLD - 1, 1, f), 1); TEST_COMPARE (ferror (f), 0); written = CACHE_THRESHOLD - 1; /* Ensure the file position indicator advanced correctly. */ TEST_COMPARE (ftell (f), written); for (i = 0; i < ITERATIONS; i++) { /* Write an extra object of size PARTIAL_BYTES that triggers a write to disk. Our FS will write at most PARTIAL_BYTES bytes to the file instead of all the data. By writting PARTIAL_BYTES, we guarantee the amount of data in the cache will never decrease below CACHE_THRESHOLD. No errors are expected. */ TEST_COMPARE (fwrite (in, PARTIAL_BYTES, 1, f), 1); TEST_COMPARE (ferror (f), 0); written += PARTIAL_BYTES; /* Ensure the file position indicator advanced correctly. */ TEST_COMPARE (ftell (f), written); } /* Flush the rest of the data. */ TEST_COMPARE (fflush (f), 0); TEST_COMPARE (ferror (f), 0); /* Ensure the file position indicator was not modified. */ TEST_COMPARE (ftell (f), written); /* In case an unexpected error happened, clear it before exiting. */ if (ferror (f)) clearerr (f); xfclose (f); free (fname); free (in); support_fuse_unmount (fs); return 0; } #include <support/test-driver.c>