C fgetln and strcpy

This is a continuation of the exploring C articles I started here. This will focus on a quirk that caused me some debugging pain.

In fairness, this is documented on the man-page of fgetln:

This line is not a C string as it does not end with a terminating NULL character.

A simple implementation of fgetln to iterate through every line in a file:

int len = 0;

FILE *f = fopen(file_path, "r"); //open the file

// line will be filled with NULL on eof or error, use 
// feof or ferror to determine which
while( ( line = fgetln( f, &len ) ) != NULL ) {
    
}

fclose(f);

If you want to store this line somewhere for later display, you might be tempted to use strcpy:

char buffer[256] = { 0 };
int len = 0;

FILE *f = fopen(file_path, "r");

while( ( line = fgetln( f, &len ) ) != NULL ) {
    strcpy(buffer, line);
}

fclose(f);

This will appear to work, only occasionally (in testing, I've had it work as expected seven times in a row) throwing garbage onto the end of one line.

The correct implementation requires use of strncpy:

while( ( line = fgetln( f, &len ) ) != NULL ) {
    strncpy(buffer, line, len ); //this only works because we initialized buffer to all 0's
}

In C, there is no concept of a string as an object. Instead, they are represented as arrays of individual characters with a null-terminator (or 0) denoting the end.

strcpy uses this fact to work. Starting at the pointer passed for the src, it steps character-by-character until it reaches a null-terminator, copying memory over at each step. A common implementation that most libraries use:

char *strcpy( char *dest, char *src ) {
    // strcpy returns a pointer to the written string, so save a version
    char *orig_dest = dest;
    // while the character at src isn't 0, copy the character over to dest and increase
    // the write location
    while( *dest++ = *src++ );
    return orig_dest;
}

However, like the quote at the beginning of this article, fgetln doesn't null-terminate strings, instead returning the literal byte-stream from the file (\n, \t, and all). Even worse, normally the area grabbed for line is zero'd out, making it seem like it is working as intended. As noted above, the strncpy example only works because buffer was initialized to all 0's in its declaration. In fact, there is a bug with the strncpy example. Take the following file:

I'm a long line
short line

If the strncpy example is modified to print the line like so:

while( ( line = fgetln( f, &len ) ) != NULL ) {
    strncpy(buffer, line, len );
    printf("%s\n", buffer);
}

You get the following output:

I'm a long line
short line line

To solve this, strings need to be null-terminated after copied to the buffer:

strncpy(buffer, line, len );
buffer[len - 1] = 0;
printf("%s\n", buffer);

However, this quirk can also be an advantage. If you have a long string that you want to truncate, this can easily be done by simply throwing a null-terminator in the position you want to truncate:

char some_long_string[1024] = //some string
some_long_string[80] = 0;

Another interesting quirk: characters in C are integers, with their value representing an ASCII value. This mean, if you wanted to write a cipher that increased every letter by one (e.g., "abc" becomes "bcd"), it can be done easily:

for( int i = 0; i < strlen(s); i++ ) {
    s[i]++; //increase the value of the character at index i by 1
}