Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 22 additions & 0 deletions C/impl/sonLibFile.c
Original file line number Diff line number Diff line change
Expand Up @@ -220,3 +220,25 @@ FILE *st_fopen(const char *fileName, const char *mode) {
}
return ret;
}

void st_fcheck(FILE *fileHandle, const char *fileName) {
// the flush is what pushes the buffer at the operating system, so it has
// to happen before the error indicator is worth reading
if (fflush(fileHandle) != 0) {
st_errnoAbort("Failed to write the file %s, so its contents are incomplete", fileName);
}
// the indicator is sticky, so this catches a failure at any earlier point
// in the write, including one whose errno has since been overwritten
if (ferror(fileHandle)) {
st_errAbort("Failed to write the file %s, so its contents are incomplete. "
"Check the free space, the quota and the permissions on the "
"file system holding it", fileName);
}
}

void st_fclose(FILE *fileHandle, const char *fileName) {
st_fcheck(fileHandle, fileName);
if (fclose(fileHandle) != 0) {
st_errnoAbort("Failed to close the file %s, so its contents may be incomplete", fileName);
}
}
23 changes: 23 additions & 0 deletions C/inc/sonLibFile.h
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,29 @@ void stFile_rmtree(const char *fileName);
*/
FILE *st_fopen(const char *fileName, const char *mode);

/*
* Die if an output file has failed to write. stdio reports a failed write --
* a full disk, a quota, a read-only mount -- by setting the stream's error
* indicator, and nothing is obliged to look at it, so without a check a
* truncated output file is indistinguishable from a complete one and the
* program still exits successfully. A short fasta is still a valid fasta.
*
* The flush is part of the check rather than an optimisation: the buffer is
* not necessarily handed to the operating system until it happens, so a
* stream that has already lost data can still look clean beforehand.
*
* Use on stdout, or mid-stream, wherever the file is not being closed here.
*/
void st_fcheck(FILE *fileHandle, const char *fileName);

/*
* st_fcheck followed by a close whose return value is also checked. Closing
* is the last point at which buffered data reaches the operating system, so a
* write that fails there is reported nowhere else. Prefer this to a bare
* fclose on any file the program has written.
*/
void st_fclose(FILE *fileHandle, const char *fileName);

#ifdef __cplusplus
}
#endif
Expand Down
85 changes: 85 additions & 0 deletions C/tests/sonLibFileTest.c
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,28 @@
* Released under the MIT license, see LICENSE.txt
*/

// fork and waitpid are POSIX.1-2001. On Linux/glibc they are suppressed when
// compiling with -std=c99 (which sets __STRICT_ANSI__); _GNU_SOURCE restores
// them and must be defined before any system headers. _GNU_SOURCE is used
// rather than _POSIX_C_SOURCE because it expands the Darwin C level instead of
// restricting it, matching what sonLibFile.c does.
#if defined(__linux__) || (defined(__unix__) && !defined(__APPLE__))
#ifndef _GNU_SOURCE
#define _GNU_SOURCE
#endif
#endif

#include "sonLibGlobalsTest.h"

// fork and waitpid exist on every Unix-like platform; the test that needs them
// is compiled out only where they do not (e.g. Windows).
#if defined(__unix__) || defined(__unix) || (defined(__APPLE__) && defined(__MACH__))
#define ST_TEST_HAVE_FORK 1
#include <unistd.h>
#include <errno.h>
#include <sys/wait.h>
#endif

static char *tempFileDir = "sonLibFileTestTempDir";
static char *tempFileName1 =
"sonLibFileTestTempDir/sonLibFileTestTempFile1.txt";
Expand Down Expand Up @@ -150,13 +170,78 @@ static void test_stFile_pathJoin(CuTest *testCase) {
free(s);
}

static void test_st_fclose(CuTest *testCase) {
setup();
// a write that succeeds must be left completely alone by the checks
FILE *fileHandle = st_fopen(tempFileName1, "w");
fprintf(fileHandle, "hello world\n");
st_fcheck(fileHandle, tempFileName1);
fprintf(fileHandle, "bye bye\n");
st_fclose(fileHandle, tempFileName1);

fileHandle = st_fopen(tempFileName1, "r");
char *s = stFile_getLineFromFile(fileHandle);
CuAssertStrEquals(testCase, "hello world", s);
free(s);
s = stFile_getLineFromFile(fileHandle);
CuAssertStrEquals(testCase, "bye bye", s);
free(s);
CuAssertPtrEquals(testCase, NULL, stFile_getLineFromFile(fileHandle));
fclose(fileHandle);
teardown();
}

#ifdef ST_TEST_HAVE_FORK
/*
* The failure the checks exist for: a write that cannot land. Every write to
* /dev/full fails with ENOSPC, which is the disk-full case without needing a
* full disk. st_fcheck must not return, so the check runs in a child process.
* /dev/full is a Linux thing; where it is absent the child reports a skip.
*/
static void test_st_fcheck_detectsFailedWrite(CuTest *testCase) {
fflush(stdout);
fflush(stderr);
pid_t pid = fork();
CuAssertTrue(testCase, pid >= 0);
if (pid == 0) {
// no CuAssert in here -- the parent judges this child by its exit status
FILE *fileHandle = fopen("/dev/full", "w");
if (fileHandle == NULL) {
_exit(66); // no /dev/full, reported as a skip below
}
// Prove the path really is the always-full device before asserting
// anything about it. A rootfs unpacked without device nodes, or a
// sandbox that stubs /dev, can leave a plain writable file here, and
// then the write below would succeed and the test would report a
// failure that says nothing about st_fcheck.
if (write(fileno(fileHandle), "x", 1) != -1 || errno != ENOSPC) {
_exit(66);
}
fprintf(fileHandle, "this cannot possibly be written\n");
st_fcheck(fileHandle, "/dev/full");
_exit(0); // st_fcheck returned, which is the bug this test is for
}
int status = 0;
CuAssertTrue(testCase, waitpid(pid, &status, 0) == pid);
CuAssertTrue(testCase, WIFEXITED(status));
if (WEXITSTATUS(status) == 66) {
return; // /dev/full not present, nothing to assert
}
CuAssertIntEquals(testCase, 1, WEXITSTATUS(status));
}
#endif

CuSuite* sonLibFileTestSuite(void) {
CuSuite* suite = CuSuiteNew();
SUITE_ADD_TEST(suite, test_stFile_getLineFromFile);
SUITE_ADD_TEST(suite, test_stFile_pathJoin);
SUITE_ADD_TEST(suite, test_stFile_exists);
SUITE_ADD_TEST(suite, test_stFile_isDir);
SUITE_ADD_TEST(suite, test_stFile_getFileNamesInDirectory);
SUITE_ADD_TEST(suite, test_st_fclose);
#ifdef ST_TEST_HAVE_FORK
SUITE_ADD_TEST(suite, test_st_fcheck_detectsFailedWrite);
#endif

return suite;
}