Day 9, part 3 - A longer trip

Advent of Code 2015

Adrien Foucart

2026-04-14

[Back to index]

Puzzle: https://adventofcode.com/2015/day/9

My solution: https://codeberg.org/adfoucart/aocode15/src/branch/main/src/day9.c

In the previous episode, we got our star for finding the shortest path through all the nodes, but we also ended on a worrying bug: the parse_strings method, when there’s an empty line at the end of the input file, sometimes fills this empty line with data that it’s clearly pulling from elsewhere. Let’s start by trying to isolate the error in a test. I had a test to check that a last empty line was just equals to \0, but evidently it’s not sufficient here:

bool test_parse_strings_empty_line_is_null(){
    char test[] = "last empty line\n";
    str_list_t* str_list = parse_strings(test, '\n');
    return (str_list->str_list[1][0] == '\0');
}

This test runs fine. Writing a new test specifically for the day 9 inputs immediately shows a big difference:

bool test_parse_strings_empty_line_is_null_on_day9(){
    char* test = freadall("./data/day9.txt");
    str_list_t* str_list = parse_strings(test, '\n');
    return (str_list->str_list[28][0] == '\0');
}

This fails. But in the first case, I was defining a string with the char test[] = "..." syntax, which automatically adds a \0 at the end for us. In my day 9 version here, I’m getting a char* from freadall, which is a function I’ve written, and therefore more likely to contain some mistake. Let’s take a look at freadall:

char* freadall(char *path){
    FILE *fp;
    char *buffer;
    int fsize;

    fp = fopen(path, "r");
    if( fp == NULL )
        exit(1);
    // find end of file to allocate size
    fseek(fp, 0L, SEEK_END);
    fsize = ftell(fp);
    // return to beginning of file
    rewind(fp);

    printf("Size of file: %d\n", fsize);

    // allocate file content
    buffer = malloc(sizeof(char)*fsize+1);

    // read file
    fread(buffer, sizeof(char), fsize, fp);
    buffer[fsize] = '\0';

    fclose(fp);
    return buffer;
}

So I’m using fseek and ftell to compute the size of the file, allocating that to a buffer, copying the file content, and then setting the last character to \0. My hypothesis now is that the size I compute is not correct, and I therefore allocate too much space to my buffer. Since I set the last char in the memory region to \0, there could be some stuff between the end of the copied file content and the added \0. I guess.

The goal with fseek and ftell is to first set the pointer to the end of the file with fseek(fp, 0L, SEEK_END), then to get the position with ftell, which should therefore be equal to the size. That’s what I got from this StackOverflow comment, at least ! One little problem looking now at the documentation for ftell:

If the stream is open in text mode, the value returned by this function is unspecified and is only meaningful as the input to fseek(). 

That doesn’t sound good. From this other StackOverflow comment chain, this is mostly a Windows problem, but, per one Andrew Henle:

So, there is no portable way that's strictly compliant with the C standard to get the size of a file other than to read it char-by-char and count them. There are some downright strange storage systems out there...

So let’s make a less efficient but safer freadall, shall we? Here’s the plan:

This should do it:

char* freadall(char *path){
    FILE *fp;
    char *buffer;
    int fsize = 0;
    int capacity = 512;
    int MAX_SIZE = 65532;

    fp = fopen(path, "r");
    if( fp == NULL )
        exit(1);
    
    // preallocate file content
    buffer = malloc(sizeof(char)*capacity);

    // read file
    do {
        int c = fgetc(fp);
        if (c == EOF) break;
        if (fsize == capacity){
            capacity *= 2;
            buffer = realloc(buffer, capacity);
        }
        buffer[fsize++] = c;
    } while (fsize < MAX_SIZE);
    
    fclose(fp);

    buffer[fsize] = '\0';
    buffer = realloc(buffer, fsize);

    return buffer;
}

I’ve added a MAX_SIZE because I’m always uncomfortable to keep a while(true) somewhere. Running my tests again, everything seems fine. Running the day9.c code also works, even with the new line in the file. Great.

Part 2, same as part 1

Part 2 of the puzzle is the same as part 1, except we need to longest path instead of the shortest. Since we did an exhaustive search in part 1, we just have to change some signs and variable names:

int find_longest_distance_from_node(path_t* path, node_t* node){
    int max_dist = -1;
    for (int i=0; i < node->n_connections; i++){
        if (path_contains(path, node->connections[i]->dest)){
            continue;
        }
        // clone path
        path_t* newpath = path_clone(path);
        path_add(newpath, node->connections[i]->dest, node->connections[i]->dist);
        int further = find_longest_distance_from_node(newpath, node->connections[i]->dest);
        if (further > max_dist){
            max_dist = further;
        }
        path_destroy(newpath);
    }
    if (max_dist < 0)
        return path->total_dist;

    return max_dist;
}

int find_longest_distance(list_node_t* nodes){
    int max_dist = 0;
    for (int i = 0; i < nodes->n_nodes; i++){
        node_t* node = nodes->nodes[i];
        path_t* path = path_create(node);
        int max_from_node = find_longest_distance_from_node(path, node);
        if (max_from_node > max_dist){
            max_dist = max_from_node;
        }
    }

    return max_dist;
}

And now that our freadall does what it’s supposed to do, we directly get the right answer. One month for part 1, five minutes for part 2. That’s unusual. I’ll take it. 18 stars down, 32 to go. If I go to the end. We’ll see.