read line endings sequentially
static inline bool fillBuffer(FILE *, char [], size_t *, size_t *);


It'd be nice to provide names for these function parameters, especially since two of them have the same type (size_t *).

It's fairly unusual to see a pointer parameter declared with [] instead of * — I infer your rationale, something like "This pointer is expected to point to an array of contiguous objects, not just one object," but the inconsistency (and risk of some maintainer forgetting and writing sizeof buffer just once) is too costly IMHO. The fact that char *buffer is immediately followed by size_t bufferlen should be a dead giveaway that buffer points to an array.

...Oh, what do you mean char *buffer isn't followed by size_t bufferlen?!

static inline bool fillBuffer(FILE *file, char buffer[], size_t *numBytesRead, size_t *numBytesUsed) {
    *numBytesRead = fread(buffer, 1, BUFFER_SIZE, file);


Yep, that's a buffer overflow waiting to happen. Your fillBuffer function takes a pointer (buffer) and unconditionally writes up to BUFFER_SIZE (4096) bytes into it. If the caller passes in a smaller buffer... boom.
So what we want to see here is more like the API to fread itself:

static inline bool fillBuffer(FILE *file, char *buffer, size_t bufferlen, size_t *numBytesRead, size_t *numBytesUsed) {
    *numBytesRead = fread(buffer, 1, bufferlen, file);
    *numBytesUsed = 0;

    return (0 &lt; *numBytesRead);
}


I know that fread returns numBytesRead as its return value, so I'm suspicious of the way fillBuffer returns numBytesRead via an out-parameter. Out-parameters are a code smell. So I look at what we're using our return value for.

    return (0 &lt; *numBytesRead);


Comparing a size_t against 0 should generally be done with !=, not &lt;, since the only value of type size_t that is not &gt; 0 is 0 itself.

    return (0 != *numBytesRead);


But this function returns bool, and size_t converts to bool in the obvious way, so:

    return *numBytesRead;


Oh look! We just eliminated that out-parameter!

static inline size_t fillBuffer(FILE *fp, char *buffer, size_t bufferlen, size_t *numBytesUsed) {
    size_t numBytesRead = fread(buffer, 1, bufferlen, fp);
    *numBytesUsed = 0;
    return numBytesRead;
}


Now simplify:

static inline size_t fillBuffer(FILE *fp, char *buffer, size_t bufferlen, size_t *numBytesUsed) {
    *numBytesUsed = 0;
    return fread(buffer, 1, bufferlen, fp);
}


Now pause. Notice that numBytesUsed is not actually used by this function at all, except to unconditionally set it to 0. The Single Responsibility Principle suggests that perhaps setting this arbitrary size_t to 0 could be done by the caller, so as not to bother us with an essentially unrelated task. But then our whole function becomes a tail-call to fread(...), which seems ridiculous. Let's eliminate this function. Find its caller...

static inline bool nextCharEquals(FILE *file, char buffer[], size_t *numBytesRead, size_t *numBytesUsed, char value) {
    return (((*numBytesUsed &lt; *numBytesRead) || fillBuffer(file, buffer, numBytesRead, numBytesUsed)) &amp;&amp; (buffer[*numBytesUsed] == value));
}


Oh dear me. Let's rewrite this mess using if and else:

static inline bool nextCharEquals(FILE *file, char buffer[], size_t *numBytesRead, size_t *numBytesUsed, char value) {
    if (*numBytesUsed &gt;= *numBytesRead) {
        if (fillBuffer(file, buffer, numBytesRead, numBytesUsed) == 0) {
            return false;
        }
    }
    return buffer[*numBytesUsed] == value;
}


Okay, now for the inlining step!

static inline bool nextCharEquals(FILE *fp, char *buffer, size_t bufferlen, size_t *numBytesUsed, char value) {
    if (*numBytesUsed &gt;= *numBytesRead) {
        *numBytesRead = fread(buffer, 1, bufferlen, fp);
        *numBytesUsed = 0;
        if (*numBytesRead == 0) {
            return false;
        }
    }
    return buffer[*numBytesUsed] == value;
}


Now we can start to make out where the real problems lie. For example, it is tempting to observe that if (*numBytesRead == 0) is equivalent to if (*numBytesUsed &gt;= *numBytesRead) (since we just set *numBytesUsed to 0 on the previous line). So we are tempted to rewrite this function as

static inline bool nextCharEquals(FILE *fp, char *buffer, size_t bufferlen, size_t *numBytesUsed, char value) {
    while (*numBytesUsed &gt;= *numBytesRead) {
        *numBytesRead = fread(buffer, 1, bufferlen, fp);
        *numBytesUsed = 0;
        // TODO: check errno? check feof(fp)?
    }
    return buffer[*numBytesUsed] == value;
}


Of course as written that would be an infinite loop on EOF; but it makes us ponder in what situations we expect to see fread(...) == 0... which starts us thinking about error handling.



Meanwhile, we can apply the Single Responsibility Principle again to figure out that perhaps char value doesn't belong in this function. Why would our caller want to write

nextCharEquals(dataFile, buffer, &amp;numBytesRead, &amp;numBytesUsed, '\n')


when they could write

nextChar(dataFile, buffer, &amp;numBytesRead, &amp;numBytesUsed) == '\n'


instead?

Any time you're spelling out the name of a built-in operator (==) in your function name, it's a good sign that your function is doing more than it should be. Let the built-in operator pull its own weight!



One more random tip: Instead of hard-coding BUFFER_SIZE all over your program, try to use sizeof buffer.

char buffer[BUFFER_SIZE];
foo(buffer, BUFFER_SIZE);  // bad, repetitive, easy to typo

char buffer[4096];
foo(buffer, sizeof buffer);  // good, clean, hard to typo


It'll also train you to pass your buffer sizes around properly, since you won't have all your buffers' sizes so promiscuously available in "global variables." And that will help you prepare for the world of multi-source-file programs.