Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fatal error handling, part 9 #111

Merged
merged 24 commits into from
Dec 29, 2022
Merged
Show file tree
Hide file tree
Changes from 9 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
263 changes: 185 additions & 78 deletions src/mmfatl.c
Original file line number Diff line number Diff line change
Expand Up @@ -34,59 +34,9 @@ enum {
NUL = '\x00',
};

//----------

/*
* During development you may not want to expose preliminary results to the
* normal compile, as this would trigger 'unused' warnings, for example. In
* the regression test environment your code may be referenced by testing code,
* though.
*
* This section should be empty, or even removed, once your development is
* finished.
*/
#ifdef TEST_ENABLE
# define UNDER_DEVELOPMENT
#endif

#ifdef UNDER_DEVELOPMENT

/*!
* converts an unsigned to a sequence of decimal digits representing its value.
* The value range is known to be at least 2**32 on contemporary hardware, but
* C99 guarantees just 2**16. We support unsigned in formatted error output
* to allow for macros like __LINE__ denoting error positions in text files.
*
* There exist no utoa in the C99 standard library, that could be used instead,
* and sprintf must not be used in a memory-tight situation (AS Unsafe heap,
* https://www.gnu.org/software/libc/manual/html_node/Formatted-Output-Functions.html).
* \param value an unsigned value to convert.
* \returns a pointer to a string converted from \p value. Except for zero,
* the result has a leading non-zero digit.
* \attention The result is stable only until the next call to this function.
*/
static char const* unsignedToString(unsigned value) {
/*
* sizeof(value) * CHAR_BIT are the bits encodable in value, the factor 146/485,
* derived from a chained fraction, is about 0.3010309, slightly greater than
* log 2. So the number within the brackets is the count of decimal digits
* encodable in value. Two extra bytes compensate for the truncation error of
* the division and allow for a terminating NUL.
*/
static char digits[(sizeof(value) * CHAR_BIT * 146) / 485 + 2];
//*** utility code used in the implementation of the interface ***
wlammen marked this conversation as resolved.
Show resolved Hide resolved

unsigned ofs = sizeof(digits) - 1;
digits[ofs] = NUL;
if (value == 0)
digits[--ofs] = '0';
else {
while (value) {
digits[--ofs] = (value % 10) + '0';
value /= 10;
}
}
return digits + ofs;
}

/*!
* \brief declares a buffer used to generate a text message through a
Expand Down Expand Up @@ -245,38 +195,54 @@ static void initState(struct ParserState* state, struct Buffer* buffer) {
state->format = empty;
}

/*!
* converts an unsigned to a sequence of decimal digits representing its value.
* The value range is known to be at least 2**32 on contemporary hardware, but
* C99 guarantees just 2**16. We support unsigned in formatted error output
* to allow for macros like __LINE__ denoting error positions in text files.
*
* There exist no utoa in the C99 standard library, that could be used instead,
* and sprintf must not be used in a memory-tight situation (AS Unsafe heap,
* https://www.gnu.org/software/libc/manual/html_node/Formatted-Output-Functions.html).
* \param value an unsigned value to convert.
* \returns a pointer to a string converted from \p value. Except for zero,
* the result has a leading non-zero digit.
* \attention The result is stable only until the next call to this function.
digama0 marked this conversation as resolved.
Show resolved Hide resolved
*/
static char const* unsignedToString(unsigned value) {
/*
* sizeof(value) * CHAR_BIT are the bits encodable in value, the factor 146/485,
* derived from a chained fraction, is about 0.3010309, slightly greater than
* log 2. So the number within the brackets is the count of decimal digits
* encodable in value. Two extra bytes compensate for the truncation error of
* the division and allow for a terminating NUL.
*/
static char digits[(sizeof(value) * CHAR_BIT * 146) / 485 + 2];

unsigned ofs = sizeof(digits) - 1;
digits[ofs] = NUL;
if (value == 0)
digits[--ofs] = '0';
else {
while (value) {
digits[--ofs] = (value % 10) + '0';
value /= 10;
}
}
return digits + ofs;
}

/*! reflect a possible buffer overflow in the parser state
* \param state [not null] ParserState object being updated in case of
* overflow
* \return false in case of overflow
* \post in case of overflow the current format position is moved to the end
*/
static bool checkOverflow(struct ParserState* state) {
bool result = !isBufferOverflow(state->out);
if (!result)
bool isOverflow = isBufferOverflow(state->out);
if (isOverflow)
state->format += strlen(state->format);
return result;
}

/*!
* \brief Preparing internal data structures for an error message.
*
* Empties the message buffer used to construct error messages.
*
* Prior to generating an error message some internal data structures need to
* be initialized. Usually such initialization is automatically executed on
* program startup. Since we are in a fatal error situation, we do not rely
* on this. Instead we assume memory corruption has affected this module's
* data and renders its state useless. So we initialize it immediately before
* the error message is generated. Note that we still rely on part of the
* system be running. We cannot overcome a fully clobbered system, we only
* can increase our chances of bypassing some degree of memory corruption.
*
* \post internal data structures are initialized and ready for constructing
* a message from a format string and parameter values.
*/
static void fatalErrorInit(void) {
initBuffer(&buffer);
initState(&state, &buffer);
return !isOverflow;
}

/*!
Expand Down Expand Up @@ -359,7 +325,85 @@ static void parse(struct ParserState* state) {
} while (*state->format != NUL);
}

#endif // UNDER_DEVELOPMENT
/**** Implementation of the interface in the header file ****/

/* See the header file for descriptions
* Global instances buffer and state are known to the following functions.
* They must not be passed as parameters, as they are implementation details
* not to be exposed through the interface.
*/

char const* getFatalErrorPlaceholderToken(
enum fatalErrorPlaceholderType type) {

static char result[3];

switch (type)
{
case MMFATL_PH_PREFIX:
case MMFATL_PH_STRING:
case MMFATL_PH_UNSIGNED:
result[0] = MMFATL_PH_PREFIX;
result[1] = type;
result[2] = NUL;
break;
default:
return NULL;
}
return result;
}

void fatalErrorInit(void) {
initBuffer(&buffer);
initState(&state, &buffer);
}

bool fatalErrorPush(char const* format, ...) {
bool overflow = isBufferOverflow(&buffer);
if (!overflow && format != NULL) {
// initialize the parser state
state.format = format;
va_start(state.args, format);
parse(&state);
overflow = isBufferOverflow(&buffer);
va_end(state.args);
}

return !overflow;
}

void fatalErrorPrintAndExit(void) {
#ifndef TEST_ENABLE // we don't want a test program terminate here
fputs(buffer.text, stderr);
exit(EXIT_FAILURE);
#endif
}

void fatalErrorExitAt(char const* file, unsigned line,
char const* msgWithPlaceholders, ...) {
fatalErrorInit();

// a format for the error location, only showing relevant data
char const* format = NULL;
if (file && *file)
{
if (line > 0)
format = "At %s:%u\n";
else
format = "In file %s:\n";
}
else if (line > 0)
format = "%sIn line %u:\n";

if (fatalErrorPush(format, file, line) && msgWithPlaceholders) {
state.format = msgWithPlaceholders;
va_start(state.args, msgWithPlaceholders);
parse(&state);
va_end(state.args);
}

fatalErrorPrintAndExit();
}


//================= Regression tests =====================
Expand Down Expand Up @@ -668,14 +712,77 @@ static bool test_parse(void) {
return true;
}

static bool test_getFatalErrorPlaceholderToken(void) {
char const* result = getFatalErrorPlaceholderToken(MMFATL_PH_PREFIX);
ASSERT(result && strcmp(result, "%%") == 0);
result = getFatalErrorPlaceholderToken(MMFATL_PH_UNSIGNED);
ASSERT(result && strcmp(result, "%u") == 0);
ASSERT(getFatalErrorPlaceholderToken(
(enum fatalErrorPlaceholderType)NUL) == NULL);
return true;
}

static bool test_fatalErrorPush() {
fatalErrorInit(); // pre-condition

// case format NULL or empty (do nothing)
ASSERT(fatalErrorPush(NULL));
ASSERT(strcmp(buffer.text, "") == 0);
ASSERT(fatalErrorPush(""));
ASSERT(strcmp(buffer.text, "") == 0);

// simple message, no placeholder
ASSERT(fatalErrorPush("abc"));
ASSERT(strcmp(buffer.text, "abc") == 0);

// message with placeholders, appended
ASSERT(fatalErrorPush("x%sy%uz", "--", 123));
ASSERT(strcmp(buffer.text, "abcx--y123z") == 0);

// overflow
limitFreeBuffer(2);
ASSERT(!fatalErrorPush("abc"));
ASSERT(strcmp(buffer.text, "$ab$") == 0);

ASSERT(!fatalErrorPush(NULL));
ASSERT(!fatalErrorPush(""));
ASSERT(strcmp(buffer.text, "$ab$") == 0);

return true;
}

static bool test_fatalErrorExitAt() {
// note that in test mode the fatalErrorExitAt neither prints to stderr
// nor exits. The message is still in the buffer.

fatalErrorExitAt("test.c", 1000, "%s failed!", "program");
ASSERT(strcmp(buffer.text, "At test.c:1000\nprogram failed!") == 0);
// ignoring line
fatalErrorExitAt("x.c", 0, "test %u failed!", 5);
ASSERT(strcmp(buffer.text, "In file x.c:\ntest 5 failed!") == 0);
// ignoring file
fatalErrorExitAt(NULL, 123, "%s", "need help!");
ASSERT(strcmp(buffer.text, "In line 123:\nneed help!") == 0);

// ignoring error location
fatalErrorExitAt(NULL, 0, "take lessons, you fool!");
ASSERT(strcmp(buffer.text, "take lessons, you fool!") == 0);

return true;
}


void test_mmfatl(void) {
RUN_TEST(test_unsignedToString);
RUN_TEST(test_fatalErrorInit);
RUN_TEST(test_isBufferOverflow);
RUN_TEST(test_appendText);
RUN_TEST(test_handleText);
RUN_TEST(test_parse);
RUN_TEST(test_unsignedToString);
RUN_TEST(test_handleSubstitution);
RUN_TEST(test_getFatalErrorPlaceholderToken);
RUN_TEST(test_fatalErrorPush);
RUN_TEST(test_fatalErrorExitAt);
}

#endif // TEST_ENABLE
Loading