/ tests / fflush.c
fflush.c
  1  #include <errno.h>
  2  #include <stdio.h>
  3  #include <stdlib.h>
  4  #include <unistd.h>
  5  #include <darwintest.h>
  6  #include <darwintest_utils.h>
  7  
  8  static char tmpfile_template[] = "/tmp/libc_test_fflushXXXXX";
  9  #define BUFSZ 128
 10  static char wrbuf[BUFSZ] = "";
 11  static const size_t filesz = BUFSZ * 120;
 12  
 13  static void
 14  cleanup_tmp_file(void)
 15  {
 16  	(void)unlink(tmpfile_template);
 17  }
 18  
 19  static const char *
 20  assert_empty_tmp_file(void)
 21  {
 22  	T_SETUPBEGIN;
 23  
 24  	int tmpfd = mkstemp(tmpfile_template);
 25  	T_ASSERT_POSIX_SUCCESS(tmpfd, "created tmp file at %s", tmpfile_template);
 26  	T_ATEND(cleanup_tmp_file);
 27  	close(tmpfd);
 28  
 29  	T_SETUPEND;
 30  
 31  	return tmpfile_template;
 32  }
 33  
 34  static const char *
 35  assert_full_tmp_file(void)
 36  {
 37  	T_SETUPBEGIN;
 38  
 39  	int tmpfd = mkstemp(tmpfile_template);
 40  	T_ASSERT_POSIX_SUCCESS(tmpfd, "created tmp file at %s", tmpfile_template);
 41  	T_ATEND(cleanup_tmp_file);
 42  
 43  	/*
 44  	 * Write a pattern of bytes into the file -- the lowercase alphabet,
 45  	 * separated by newlines.
 46  	 */
 47  	for (size_t i = 0; i < BUFSZ; i++) {
 48  		wrbuf[i] = 'a' + (i % 27);
 49  		if (i % 27 == 26) {
 50  			wrbuf[i] = '\n';
 51  		}
 52  	}
 53  	for (size_t i = 0; i < filesz; i++) {
 54  		ssize_t byteswr = 0;
 55  		do {
 56  			byteswr = write(tmpfd, wrbuf, BUFSZ);
 57  		} while (byteswr == -1 && errno == EAGAIN);
 58  
 59  		T_QUIET; T_ASSERT_POSIX_SUCCESS(byteswr, "wrote %d bytes to tmp file",
 60  				BUFSZ);
 61  		T_QUIET; T_ASSERT_EQ(byteswr, (ssize_t)BUFSZ,
 62  				"wrote correct amount of bytes to tmp file");
 63  	}
 64  
 65  	close(tmpfd);
 66  
 67  	T_SETUPEND;
 68  
 69  	return tmpfile_template;
 70  }
 71  
 72  /*
 73   * Ensure that fflush on an input stream conforms to the SUSv3 definition, which
 74   * requires synchronizing the FILE position with the underlying file descriptor.
 75   */
 76  T_DECL(fflush_input, "fflush on a read-only FILE resets fd offset")
 77  {
 78  	const char *tmpfile = assert_full_tmp_file();
 79  
 80  	T_SETUPBEGIN;
 81  
 82  	FILE *tmpf = fopen(tmpfile, "r");
 83  	T_QUIET; T_WITH_ERRNO;
 84  	T_ASSERT_NOTNULL(tmpf, "opened tmp file for reading");
 85  
 86  	/*
 87  	 * Move some way into the file.
 88  	 */
 89  	char buf[100] = "";
 90  	size_t nread = fread(buf, sizeof(buf), 1, tmpf);
 91  	T_ASSERT_EQ(nread, (size_t)1, "read correct number of items from FILE");
 92  	char last_read_char = buf[sizeof(buf) - 1];
 93  
 94  	off_t curoff = lseek(fileno(tmpf), 0, SEEK_CUR);
 95  	T_ASSERT_GT(curoff, (off_t)0, "file offset should be non-zero");
 96  
 97  	T_SETUPEND;
 98  
 99  	/*
100  	 * fflush(3) to reset the fd back to the FILE offset.
101  	 */
102  	int ret = fflush(tmpf);
103  	T_ASSERT_POSIX_SUCCESS(ret, "fflush on read-only FILE");
104  
105  	off_t flushoff = lseek(fileno(tmpf), 0, SEEK_CUR);
106  	T_ASSERT_EQ(flushoff, (off_t)sizeof(buf),
107  			"offset of file should be bytes read on FILE after fflush");
108  
109  	/*
110  	 * Make sure the FILE is reading the right thing -- the next character
111  	 * should be one letter after the last byte read, from the last call to
112  	 * fread(3).
113  	 */
114  	char c = '\0';
115  	nread = fread(&c, sizeof(c), 1, tmpf);
116  	T_QUIET;
117  	T_ASSERT_EQ(nread, (size_t)1, "read correct number of items from FILE");
118  
119  	/*
120  	 * The pattern in the file is the alphabet -- and this doesn't land on
121  	 * a newline.
122  	 */
123  	T_QUIET;
124  	T_ASSERT_NE((flushoff) % 27, (off_t)0,
125  			"previous offset shouldn't land on newline");
126  	T_QUIET;
127  	T_ASSERT_NE((flushoff + 1) % 27, (off_t)0,
128  			"current offset shouldn't land on newline");
129  
130  	T_ASSERT_EQ(c, last_read_char + 1, "read correct byte after fflush");
131  
132  	ret = fflush(tmpf);
133  	T_QUIET; T_ASSERT_POSIX_SUCCESS(ret, "fflush on read-only FILE");
134  
135  	flushoff = lseek(fileno(tmpf), 0, SEEK_CUR);
136  	T_ASSERT_EQ(flushoff, (off_t)(sizeof(buf) + sizeof(c)),
137  			"offset of file should be incremented after subsequent read");
138  
139  	/*
140  	 * Use ungetc(3) to induce the optimized ungetc behavior in the FILE.
141  	 */
142  	int ugret = ungetc(c, tmpf);
143  	T_QUIET; T_ASSERT_NE(ugret, EOF, "ungetc after fflush");
144  	T_QUIET; T_ASSERT_EQ((char)ugret, c, "ungetc un-got the correct char");
145  
146  	ret = fflush(tmpf);
147  	T_ASSERT_POSIX_SUCCESS(ret, "fflush after ungetc");
148  	flushoff = lseek(fileno(tmpf), 0, SEEK_CUR);
149  	T_ASSERT_EQ(flushoff, (off_t)sizeof(buf),
150  			"offset of file should be correct after ungetc and fflush");
151  
152  	nread = fread(&c, sizeof(c), 1, tmpf);
153  	T_QUIET;
154  	T_ASSERT_EQ(nread, (size_t)1, "read correct number of items from FILE");
155  	T_ASSERT_EQ(c, last_read_char + 1,
156  			"read correct byte after ungetc and fflush");
157  }
158  
159  /*
160   * Try to trick fclose into not reporting an ENOSPC error from the underlying
161   * descriptor in update mode.  Previous versions of Libc only flushed the FILE
162   * if it was write-only.
163   */
164  
165  #if TARGET_OS_OSX
166  /*
167   * Only macOS contains a version of hdiutil that can create disk images.
168   */
169  
170  #define DMGFILE "/tmp/test_fclose_enospc.dmg"
171  #define VOLNAME "test_fclose_enospc"
172  static const char *small_file = "/Volumes/" VOLNAME "/test.txt";
173  
174  static void
175  cleanup_dmg(void)
176  {
177  	char *hdiutil_detach_argv[] = {
178  		"/usr/bin/hdiutil", "detach", "/Volumes/" VOLNAME, NULL,
179  	};
180  	pid_t hdiutil_detach = -1;
181  	int ret = dt_launch_tool(&hdiutil_detach, hdiutil_detach_argv, false, NULL,
182  			NULL);
183  	if (ret != -1) {
184  		int status = 0;
185  		(void)waitpid(hdiutil_detach, &status, 0);
186  	}
187  	(void)unlink(DMGFILE);
188  }
189  
190  T_DECL(fclose_enospc, "ensure ENOSPC is preserved on fclose")
191  {
192  	T_SETUPBEGIN;
193  
194  	/*
195  	 * Ensure a disk is available that will fill up and start returning ENOSPC.
196  	 *
197  	 * system(3) would be easier...
198  	 */
199  	char *hdiutil_argv[] = {
200  		"/usr/bin/hdiutil", "create", "-size", "10m", "-type", "UDIF",
201  		"-volname", VOLNAME, "-nospotlight", "-fs", "HFS+", DMGFILE, "-attach",
202  		NULL,
203  	};
204  	pid_t hdiutil_create = -1;
205  	int ret = dt_launch_tool(&hdiutil_create, hdiutil_argv, false, NULL, NULL);
206  	T_ASSERT_POSIX_SUCCESS(ret, "created and attached 10MB DMG");
207  	T_ATEND(cleanup_dmg);
208  	int status = 0;
209  	pid_t waited = waitpid(hdiutil_create, &status, 0);
210  	T_QUIET; T_ASSERT_EQ(waited, hdiutil_create,
211  			"should have waited for the process that was launched");
212  	T_QUIET;
213  	T_ASSERT_TRUE(WIFEXITED(status), "hdiutil should have exited");
214  	T_QUIET;
215  	T_ASSERT_EQ(WEXITSTATUS(status), 0,
216  			"hdiutil should have exited successfully");
217  
218  	/*
219  	 * Open for updating, as previously only write-only files would be flushed
220  	 * on fclose.
221  	 */
222  	FILE *fp = fopen(small_file, "a+");
223  	T_WITH_ERRNO;
224  	T_ASSERT_NOTNULL(fp, "opened file at %s for append-updating", small_file);
225  
226  	char *buf = malloc(BUFSIZ);
227  	T_QUIET; T_WITH_ERRNO;
228  	T_ASSERT_NOTNULL(buf, "should allocate BUFSIZ bytes");
229  
230  	for (int i = 0; i < BUFSIZ; i++) {
231  		buf[i] = (char)(i % 256);
232  	}
233  
234  	/*
235  	 * Fill up the disk -- induce ENOSPC.
236  	 */
237  	size_t wrsize = BUFSIZ;
238  	for (int i = 0; i < 2; i++) {
239  		for (;;) {
240  			errno = 0;
241  			if (write(fileno(fp), buf, wrsize) < 0) {
242  				if (errno == ENOSPC) {
243  					break;
244  				}
245  				T_WITH_ERRNO; T_ASSERT_FAIL("write(2) failed");
246  			}
247  		}
248  		wrsize = 1;
249  	}
250  	T_PASS("filled up the file until ENOSPC");
251  	free(buf);
252  
253  	/*
254  	 * Make sure the FILE is at the end, so any writes it does hit ENOSPC.
255  	 */
256  	ret = fseek(fp, 0, SEEK_END);
257  	T_ASSERT_POSIX_SUCCESS(ret, "fseek to the end of a complete file");
258  
259  	/*
260  	 * Try to push a character into the file; since this is buffered, it should
261  	 * succeed.
262  	 */
263  	ret = fputc('a', fp);
264  	T_ASSERT_POSIX_SUCCESS(ret,
265  			"fputc to put an additional character in the FILE");
266  
267  	T_SETUPEND;
268  
269  	/*
270  	 * fclose should catch the ENOSPC error when it flushes the file, before it
271  	 * closes the underlying descriptor.
272  	 */
273  	errno = 0;
274  	ret = fclose(fp);
275  	if (ret != EOF) {
276  		T_ASSERT_FAIL("fclose should fail when the FILE is full");
277  	}
278  	if (errno != ENOSPC) {
279  		T_WITH_ERRNO; T_ASSERT_FAIL("fclose should fail with ENOSPC");
280  	}
281  
282  	T_PASS("fclose returned ENOSPC");
283  }
284  
285  #endif // TARGET_OS_OSX
286  
287  /*
288   * Ensure no errors are returned when flushing a read-only, unseekable input
289   * stream.
290   */
291  T_DECL(fflush_unseekable_input,
292  		"ensure sanity when an unseekable input stream is flushed")
293  {
294  	T_SETUPBEGIN;
295  
296  	/*
297  	 * Use a pipe for the unseekable streams.
298  	 */
299  	int pipes[2];
300  	int ret = pipe(pipes);
301  	T_ASSERT_POSIX_SUCCESS(ret, "create a pipe");
302  	FILE *in = fdopen(pipes[0], "r");
303  	T_QUIET; T_WITH_ERRNO; T_ASSERT_NOTNULL(in,
304  			"open input stream to read end of pipe");
305  	FILE *out = fdopen(pipes[1], "w");
306  	T_QUIET; T_WITH_ERRNO; T_ASSERT_NOTNULL(out,
307  			"open output stream to write end of pipe");
308  
309  	/*
310  	 * Fill the pipe with some text (but not too much that the write would
311  	 * block!).
312  	 */
313  	fprintf(out, "this is a test and has some more text");
314  	ret = fflush(out);
315  	T_ASSERT_POSIX_SUCCESS(ret, "flushed the output stream");
316  
317  	/*
318  	 * Protect stdio from delving too deep into the pipe.
319  	 */
320  	char inbuf[8] = {};
321  	setbuffer(in, inbuf, sizeof(inbuf));
322  
323  	/*
324  	 * Just read a teensy bit to get the FILE offset different from the
325  	 * descriptor "offset."
326  	 */
327  	char rdbuf[2] = {};
328  	size_t nitems = fread(rdbuf, sizeof(rdbuf), 1, in);
329  	T_QUIET; T_ASSERT_GT(nitems, (size_t)0,
330  			"read from the read end of the pipe");
331  
332  	T_SETUPEND;
333  
334  	ret = fflush(in);
335  	T_ASSERT_POSIX_SUCCESS(ret,
336  			"should successfully flush unseekable input stream after reading");
337  }
338  
339  /*
340   * Ensure that reading to the end of a file and then calling ftell() still
341   * causes EOF.
342   */
343  T_DECL(ftell_feof,
344  		"ensure ftell does not reset feof when actually at end of file") {
345  	T_SETUPBEGIN;
346  	FILE *fp = fopen("/System/Library/CoreServices/SystemVersion.plist", "rb");
347  	T_WITH_ERRNO;
348  	T_ASSERT_NOTNULL(fp, "opened SystemVersion.plist");
349  	struct stat sb;
350  	T_ASSERT_POSIX_SUCCESS(fstat(fileno(fp), &sb), "fstat SystemVersion.plist");
351  	void *buf = malloc((size_t)(sb.st_size * 2));
352  	T_ASSERT_NOTNULL(buf, "allocating buffer for size of SystemVersion.plist");
353  	T_SETUPEND;
354  
355  	T_ASSERT_POSIX_SUCCESS(fseek(fp, 0, SEEK_SET), "seek to beginning");
356  	// fread can return short *or* zero, according to manpage
357  	fread(buf, (size_t)(sb.st_size * 2), 1, fp);
358  	T_ASSERT_EQ(ftell(fp), (long)sb.st_size, "ftell() == file size");
359  	T_ASSERT_TRUE(feof(fp), "feof() reports end-of-file");
360  	free(buf);
361  }
362  
363  T_DECL(putc_flush, "ensure putc flushes to file on close") {
364  	const char *fname = assert_empty_tmp_file();
365  	FILE *fp = fopen(fname, "w");
366  	T_WITH_ERRNO;
367  	T_ASSERT_NOTNULL(fp, "opened temporary file read/write");
368  	T_WITH_ERRNO;
369  	T_ASSERT_EQ(fwrite("testing", 1, 7, fp), 7UL, "write temp contents");
370  	(void)fclose(fp);
371  
372  	fp = fopen(fname, "r+");
373  	T_WITH_ERRNO;
374  	T_ASSERT_NOTNULL(fp, "opened temporary file read/write");
375  
376  	T_ASSERT_POSIX_SUCCESS(fseek(fp, -1, SEEK_END), "seek to end - 1");
377  	T_ASSERT_EQ(fgetc(fp), 'g', "fgetc should read 'g'");
378  	T_ASSERT_EQ(fgetc(fp), EOF, "fgetc should read EOF");
379  	T_ASSERT_EQ(ftell(fp), 7L, "ftell should report position 7");
380  
381  	int ret = fputc('!', fp);
382  	T_ASSERT_POSIX_SUCCESS(ret,
383  			"fputc to put an additional character in the FILE");
384  	T_ASSERT_EQ(ftell(fp), 8L, "ftell should report position 8");
385  
386  	T_QUIET;
387  	T_ASSERT_POSIX_SUCCESS(fclose(fp), "close temp file");
388  
389  	fp = fopen(fname, "r");
390  	T_WITH_ERRNO;
391  	T_ASSERT_NOTNULL(fp, "opened temporary file read/write");
392  
393  	char buf[9];
394  	T_WITH_ERRNO;
395  	T_ASSERT_NOTNULL(fgets(buf, sizeof(buf), fp), "read file data");
396  	T_ASSERT_EQ_STR(buf, "testing!", "read all the new data");
397  
398  	(void)fclose(fp);
399  }
400  
401  T_DECL(putc_writedrop, "ensure writes are flushed with a pending read buffer") {
402  	const char *fname = assert_empty_tmp_file();
403  	FILE *fp = fopen(fname, "w");
404  	T_WITH_ERRNO;
405  	T_ASSERT_NOTNULL(fp, "opened temporary file read/write");
406  	T_WITH_ERRNO;
407  	T_ASSERT_EQ(fwrite("testing", 1, 7, fp), 7UL, "write temp contents");
408  	(void)fclose(fp);
409  
410  	fp = fopen(fname, "r+");
411  	T_WITH_ERRNO;
412  	T_ASSERT_NOTNULL(fp, "opened temporary file read/write");
413  
414  	T_ASSERT_POSIX_SUCCESS(fseek(fp, -1, SEEK_END), "seek to end - 1");
415  
416  	int ret = fputc('!', fp);
417  	T_ASSERT_POSIX_SUCCESS(ret,
418  			"fputc to put an additional character in the FILE");
419  	// flush the write buffer by reading a byte from the stream to put the
420  	// FILE* into read mode
421  	T_ASSERT_EQ(fgetc(fp), EOF, "fgetc should read EOF");
422  
423  	T_QUIET;
424  	T_ASSERT_POSIX_SUCCESS(fclose(fp), "close temp file");
425  
426  	fp = fopen(fname, "r");
427  	T_WITH_ERRNO;
428  	T_ASSERT_NOTNULL(fp, "opened temporary file read/write");
429  
430  	char buf[9];
431  	T_WITH_ERRNO;
432  	T_ASSERT_NOTNULL(fgets(buf, sizeof(buf), fp), "read file data");
433  	T_ASSERT_EQ_STR(buf, "testin!", "read all the new data");
434  
435  	(void)fclose(fp);
436  }