+#include <errno.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <unistd.h>
+#include <darwintest.h>
+#include <darwintest_utils.h>
+
+static char tmpfile_template[] = "/tmp/libc_test_fflushXXXXX";
+#define BUFSZ 128
+static char wrbuf[BUFSZ] = "";
+static const size_t filesz = BUFSZ * 120;
+
+static void
+cleanup_tmp_file(void)
+{
+ (void)unlink(tmpfile_template);
+}
+
+static const char *
+assert_empty_tmp_file(void)
+{
+ T_SETUPBEGIN;
+
+ int tmpfd = mkstemp(tmpfile_template);
+ T_ASSERT_POSIX_SUCCESS(tmpfd, "created tmp file at %s", tmpfile_template);
+ T_ATEND(cleanup_tmp_file);
+ close(tmpfd);
+
+ T_SETUPEND;
+
+ return tmpfile_template;
+}
+
+static const char *
+assert_full_tmp_file(void)
+{
+ T_SETUPBEGIN;
+
+ int tmpfd = mkstemp(tmpfile_template);
+ T_ASSERT_POSIX_SUCCESS(tmpfd, "created tmp file at %s", tmpfile_template);
+ T_ATEND(cleanup_tmp_file);
+
+ /*
+ * Write a pattern of bytes into the file -- the lowercase alphabet,
+ * separated by newlines.
+ */
+ for (size_t i = 0; i < BUFSZ; i++) {
+ wrbuf[i] = 'a' + (i % 27);
+ if (i % 27 == 26) {
+ wrbuf[i] = '\n';
+ }
+ }
+ for (size_t i = 0; i < filesz; i++) {
+ ssize_t byteswr = 0;
+ do {
+ byteswr = write(tmpfd, wrbuf, BUFSZ);
+ } while (byteswr == -1 && errno == EAGAIN);
+
+ T_QUIET; T_ASSERT_POSIX_SUCCESS(byteswr, "wrote %d bytes to tmp file",
+ BUFSZ);
+ T_QUIET; T_ASSERT_EQ(byteswr, (ssize_t)BUFSZ,
+ "wrote correct amount of bytes to tmp file");
+ }
+
+ close(tmpfd);
+
+ T_SETUPEND;
+
+ return tmpfile_template;
+}
+
+/*
+ * Ensure that fflush on an input stream conforms to the SUSv3 definition, which
+ * requires synchronizing the FILE position with the underlying file descriptor.
+ */
+T_DECL(fflush_input, "fflush on a read-only FILE resets fd offset")
+{
+ const char *tmpfile = assert_full_tmp_file();
+
+ T_SETUPBEGIN;
+
+ FILE *tmpf = fopen(tmpfile, "r");
+ T_QUIET; T_WITH_ERRNO;
+ T_ASSERT_NOTNULL(tmpf, "opened tmp file for reading");
+
+ /*
+ * Move some way into the file.
+ */
+ char buf[100] = "";
+ size_t nread = fread(buf, sizeof(buf), 1, tmpf);
+ T_ASSERT_EQ(nread, (size_t)1, "read correct number of items from FILE");
+ char last_read_char = buf[sizeof(buf) - 1];
+
+ off_t curoff = lseek(fileno(tmpf), 0, SEEK_CUR);
+ T_ASSERT_GT(curoff, (off_t)0, "file offset should be non-zero");
+
+ T_SETUPEND;
+
+ /*
+ * fflush(3) to reset the fd back to the FILE offset.
+ */
+ int ret = fflush(tmpf);
+ T_ASSERT_POSIX_SUCCESS(ret, "fflush on read-only FILE");
+
+ off_t flushoff = lseek(fileno(tmpf), 0, SEEK_CUR);
+ T_ASSERT_EQ(flushoff, (off_t)sizeof(buf),
+ "offset of file should be bytes read on FILE after fflush");
+
+ /*
+ * Make sure the FILE is reading the right thing -- the next character
+ * should be one letter after the last byte read, from the last call to
+ * fread(3).
+ */
+ char c = '\0';
+ nread = fread(&c, sizeof(c), 1, tmpf);
+ T_QUIET;
+ T_ASSERT_EQ(nread, (size_t)1, "read correct number of items from FILE");
+
+ /*
+ * The pattern in the file is the alphabet -- and this doesn't land on
+ * a newline.
+ */
+ T_QUIET;
+ T_ASSERT_NE((flushoff) % 27, (off_t)0,
+ "previous offset shouldn't land on newline");
+ T_QUIET;
+ T_ASSERT_NE((flushoff + 1) % 27, (off_t)0,
+ "current offset shouldn't land on newline");
+
+ T_ASSERT_EQ(c, last_read_char + 1, "read correct byte after fflush");
+
+ ret = fflush(tmpf);
+ T_QUIET; T_ASSERT_POSIX_SUCCESS(ret, "fflush on read-only FILE");
+
+ flushoff = lseek(fileno(tmpf), 0, SEEK_CUR);
+ T_ASSERT_EQ(flushoff, (off_t)(sizeof(buf) + sizeof(c)),
+ "offset of file should be incremented after subsequent read");
+
+ /*
+ * Use ungetc(3) to induce the optimized ungetc behavior in the FILE.
+ */
+ int ugret = ungetc(c, tmpf);
+ T_QUIET; T_ASSERT_NE(ugret, EOF, "ungetc after fflush");
+ T_QUIET; T_ASSERT_EQ((char)ugret, c, "ungetc un-got the correct char");
+
+ ret = fflush(tmpf);
+ T_ASSERT_POSIX_SUCCESS(ret, "fflush after ungetc");
+ flushoff = lseek(fileno(tmpf), 0, SEEK_CUR);
+ T_ASSERT_EQ(flushoff, (off_t)sizeof(buf),
+ "offset of file should be correct after ungetc and fflush");
+
+ nread = fread(&c, sizeof(c), 1, tmpf);
+ T_QUIET;
+ T_ASSERT_EQ(nread, (size_t)1, "read correct number of items from FILE");
+ T_ASSERT_EQ(c, last_read_char + 1,
+ "read correct byte after ungetc and fflush");
+}
+
+/*
+ * Try to trick fclose into not reporting an ENOSPC error from the underlying
+ * descriptor in update mode. Previous versions of Libc only flushed the FILE
+ * if it was write-only.
+ */
+
+#if TARGET_OS_OSX
+/*
+ * Only macOS contains a version of hdiutil that can create disk images.
+ */
+
+#define DMGFILE "/tmp/test_fclose_enospc.dmg"
+#define VOLNAME "test_fclose_enospc"
+static const char *small_file = "/Volumes/" VOLNAME "/test.txt";
+
+static void
+cleanup_dmg(void)
+{
+ char *hdiutil_detach_argv[] = {
+ "/usr/bin/hdiutil", "detach", "/Volumes/" VOLNAME, NULL,
+ };
+ pid_t hdiutil_detach = -1;
+ int ret = dt_launch_tool(&hdiutil_detach, hdiutil_detach_argv, false, NULL,
+ NULL);
+ if (ret != -1) {
+ int status = 0;
+ (void)waitpid(hdiutil_detach, &status, 0);
+ }
+ (void)unlink(DMGFILE);
+}
+
+T_DECL(fclose_enospc, "ensure ENOSPC is preserved on fclose")
+{
+ T_SETUPBEGIN;
+
+ /*
+ * Ensure a disk is available that will fill up and start returning ENOSPC.
+ *
+ * system(3) would be easier...
+ */
+ char *hdiutil_argv[] = {
+ "/usr/bin/hdiutil", "create", "-size", "5m", "-type", "UDIF",
+ "-volname", VOLNAME, "-nospotlight", "-fs", "HFS+", DMGFILE, "-attach",
+ NULL,
+ };
+ pid_t hdiutil_create = -1;
+ int ret = dt_launch_tool(&hdiutil_create, hdiutil_argv, false, NULL, NULL);
+ T_ASSERT_POSIX_SUCCESS(ret, "created and attached 5MB DMG");
+ int status = 0;
+ pid_t waited = waitpid(hdiutil_create, &status, 0);
+ T_QUIET; T_ASSERT_EQ(waited, hdiutil_create,
+ "should have waited for the process that was launched");
+ T_QUIET;
+ T_ASSERT_TRUE(WIFEXITED(status), "hdiutil should have exited");
+ T_QUIET;
+ T_ASSERT_EQ(WEXITSTATUS(status), 0,
+ "hdiutil should have exited successfully");
+
+ T_ATEND(cleanup_dmg);
+
+ /*
+ * Open for updating, as previously only write-only files would be flushed
+ * on fclose.
+ */
+ FILE *fp = fopen(small_file, "a+");
+ T_WITH_ERRNO;
+ T_ASSERT_NOTNULL(fp, "opened file at %s for append-updating", small_file);
+
+ char *buf = malloc(BUFSIZ);
+ T_QUIET; T_WITH_ERRNO;
+ T_ASSERT_NOTNULL(buf, "should allocate BUFSIZ bytes");
+
+ for (int i = 0; i < BUFSIZ; i++) {
+ buf[i] = (char)(i % 256);
+ }
+
+ /*
+ * Fill up the disk -- induce ENOSPC.
+ */
+ size_t wrsize = BUFSIZ;
+ for (int i = 0; i < 2; i++) {
+ for (;;) {
+ errno = 0;
+ if (write(fileno(fp), buf, wrsize) < 0) {
+ if (errno == ENOSPC) {
+ break;
+ }
+ T_WITH_ERRNO; T_ASSERT_FAIL("write(2) failed");
+ }
+ }
+ wrsize = 1;
+ }
+ T_PASS("filled up the file until ENOSPC");
+ free(buf);
+
+ /*
+ * Make sure the FILE is at the end, so any writes it does hit ENOSPC.
+ */
+ ret = fseek(fp, 0, SEEK_END);
+ T_ASSERT_POSIX_SUCCESS(ret, "fseek to the end of a complete file");
+
+ /*
+ * Try to push a character into the file; since this is buffered, it should
+ * succeed.
+ */
+ ret = fputc('a', fp);
+ T_ASSERT_POSIX_SUCCESS(ret,
+ "fputc to put an additional character in the FILE");
+
+ T_SETUPEND;
+
+ /*
+ * fclose should catch the ENOSPC error when it flushes the file, before it
+ * closes the underlying descriptor.
+ */
+ errno = 0;
+ ret = fclose(fp);
+ if (ret != EOF) {
+ T_ASSERT_FAIL("fclose should fail when the FILE is full");
+ }
+ if (errno != ENOSPC) {
+ T_WITH_ERRNO; T_ASSERT_FAIL("fclose should fail with ENOSPC");
+ }
+
+ T_PASS("fclose returned ENOSPC");
+}
+
+#endif // TARGET_OS_OSX
+
+/*
+ * Ensure no errors are returned when flushing a read-only, unseekable input
+ * stream.
+ */
+T_DECL(fflush_unseekable_input,
+ "ensure sanity when an unseekable input stream is flushed")
+{
+ T_SETUPBEGIN;
+
+ /*
+ * Use a pipe for the unseekable streams.
+ */
+ int pipes[2];
+ int ret = pipe(pipes);
+ T_ASSERT_POSIX_SUCCESS(ret, "create a pipe");
+ FILE *in = fdopen(pipes[0], "r");
+ T_QUIET; T_WITH_ERRNO; T_ASSERT_NOTNULL(in,
+ "open input stream to read end of pipe");
+ FILE *out = fdopen(pipes[1], "w");
+ T_QUIET; T_WITH_ERRNO; T_ASSERT_NOTNULL(out,
+ "open output stream to write end of pipe");
+
+ /*
+ * Fill the pipe with some text (but not too much that the write would
+ * block!).
+ */
+ fprintf(out, "this is a test and has some more text");
+ ret = fflush(out);
+ T_ASSERT_POSIX_SUCCESS(ret, "flushed the output stream");
+
+ /*
+ * Protect stdio from delving too deep into the pipe.
+ */
+ char inbuf[8] = {};
+ setbuffer(in, inbuf, sizeof(inbuf));
+
+ /*
+ * Just read a teensy bit to get the FILE offset different from the
+ * descriptor "offset."
+ */
+ char rdbuf[2] = {};
+ size_t nitems = fread(rdbuf, sizeof(rdbuf), 1, in);
+ T_QUIET; T_ASSERT_GT(nitems, (size_t)0,
+ "read from the read end of the pipe");
+
+ T_SETUPEND;
+
+ ret = fflush(in);
+ T_ASSERT_POSIX_SUCCESS(ret,
+ "should successfully flush unseekable input stream after reading");
+}
+
+/*
+ * Ensure that reading to the end of a file and then calling ftell() still
+ * causes EOF.
+ */
+T_DECL(ftell_feof,
+ "ensure ftell does not reset feof when actually at end of file") {
+ T_SETUPBEGIN;
+ FILE *fp = fopen("/System/Library/CoreServices/SystemVersion.plist", "rb");
+ T_WITH_ERRNO;
+ T_ASSERT_NOTNULL(fp, "opened SystemVersion.plist");
+ struct stat sb;
+ T_ASSERT_POSIX_SUCCESS(fstat(fileno(fp), &sb), "fstat SystemVersion.plist");
+ void *buf = malloc(sb.st_size * 2);
+ T_ASSERT_NOTNULL(buf, "allocating buffer for size of SystemVersion.plist");
+ T_SETUPEND;
+
+ T_ASSERT_POSIX_SUCCESS(fseek(fp, 0, SEEK_SET), "seek to beginning");
+ // fread can return short *or* zero, according to manpage
+ fread(buf, sb.st_size * 2, 1, fp);
+ T_ASSERT_EQ(ftell(fp), sb.st_size, "tfell() == file size");
+ T_ASSERT_TRUE(feof(fp), "feof() reports end-of-file");
+ free(buf);
+}
+
+T_DECL(putc_flush, "ensure putc flushes to file on close") {
+ const char *fname = assert_empty_tmp_file();
+ FILE *fp = fopen(fname, "w");
+ T_WITH_ERRNO;
+ T_ASSERT_NOTNULL(fp, "opened temporary file read/write");
+ T_WITH_ERRNO;
+ T_ASSERT_EQ(fwrite("testing", 1, 7, fp), 7, "write temp contents");
+ (void)fclose(fp);
+
+ fp = fopen(fname, "r+");
+ T_WITH_ERRNO;
+ T_ASSERT_NOTNULL(fp, "opened temporary file read/write");
+
+ T_ASSERT_POSIX_SUCCESS(fseek(fp, -1, SEEK_END), "seek to end - 1");
+ T_ASSERT_EQ(fgetc(fp), 'g', "fgetc should read 'g'");
+ T_ASSERT_EQ(fgetc(fp), EOF, "fgetc should read EOF");
+ T_ASSERT_EQ(ftell(fp), 7, "tfell should report position 7");
+
+ int ret = fputc('!', fp);
+ T_ASSERT_POSIX_SUCCESS(ret,
+ "fputc to put an additional character in the FILE");
+ T_ASSERT_EQ(ftell(fp), 8, "tfell should report position 8");
+
+ T_QUIET;
+ T_ASSERT_POSIX_SUCCESS(fclose(fp), "close temp file");
+
+ fp = fopen(fname, "r");
+ T_WITH_ERRNO;
+ T_ASSERT_NOTNULL(fp, "opened temporary file read/write");
+
+ char buf[9];
+ T_WITH_ERRNO;
+ T_ASSERT_NOTNULL(fgets(buf, sizeof(buf), fp), "read file data");
+ T_ASSERT_EQ_STR(buf, "testing!", "read all the new data");
+
+ (void)fclose(fp);
+}
+
+T_DECL(putc_writedrop, "ensure writes are flushed with a pending read buffer") {
+ const char *fname = assert_empty_tmp_file();
+ FILE *fp = fopen(fname, "w");
+ T_WITH_ERRNO;
+ T_ASSERT_NOTNULL(fp, "opened temporary file read/write");
+ T_WITH_ERRNO;
+ T_ASSERT_EQ(fwrite("testing", 1, 7, fp), 7, "write temp contents");
+ (void)fclose(fp);
+
+ fp = fopen(fname, "r+");
+ T_WITH_ERRNO;
+ T_ASSERT_NOTNULL(fp, "opened temporary file read/write");
+
+ T_ASSERT_POSIX_SUCCESS(fseek(fp, -1, SEEK_END), "seek to end - 1");
+
+ int ret = fputc('!', fp);
+ T_ASSERT_POSIX_SUCCESS(ret,
+ "fputc to put an additional character in the FILE");
+ // flush the write buffer by reading a byte from the stream to put the
+ // FILE* into read mode
+ T_ASSERT_EQ(fgetc(fp), EOF, "fgetc should read EOF");
+
+ T_QUIET;
+ T_ASSERT_POSIX_SUCCESS(fclose(fp), "close temp file");
+
+ fp = fopen(fname, "r");
+ T_WITH_ERRNO;
+ T_ASSERT_NOTNULL(fp, "opened temporary file read/write");
+
+ char buf[9];
+ T_WITH_ERRNO;
+ T_ASSERT_NOTNULL(fgets(buf, sizeof(buf), fp), "read file data");
+ T_ASSERT_EQ_STR(buf, "testin!", "read all the new data");
+
+ (void)fclose(fp);
+}