std.fs.Dir.openFile with output from std.fmt.bufPrint is throwing me a curveball

I’m working on Advent of Code 2025 with Zig, and my first order of business was to come up with a repeatable way for me to store my input files and fetch their contents. So I zig init’d my project directory, created an input folder in the root of it, and now store my input files there where each file is just a .txt file with its name as the day, i.e. 1.txt.

The problem I’m having is related to how I create the file path. I’m using std.fmt.bufPrint to take the day number as input, convert it from u8 to a u8, and append “.txt” to it so I can feed it to openFile. bufPrint fills the input buffer with the formatted text and also returns what was formatted as a slice. What I would intuitively prefer is to just use the actual buffer as my file path:

const cwd: std.fs.Dir = std.fs.cwd();
var input_dir = cwd.openDir("input", .{}) catch |err| {...};
defer input_dir.close();

var file_path: [7:0]u8 = std.mem.zeroes([7:0]u8); // length 7 to account for max u8 digits + ".txt"
_ = std.fmt.bufPrint(&file_path, "{d}.txt", .{day}) catch |err| {...};

const file = input_dir.openFile(&file_path, .{ .mode = .read_only }) catch |err| {...};
defer file.close();

But this fails due to a BadPathName error from openFile. I can make this work by coercing file_path to a [5]u8 by changing &file_pathto file_path[0..5] where I call openFile. That’s obviously not wise because the moment I have a 2-digit day in the path it’ll break. The more straightforward version of the whole thing looks like this:

const cwd: std.fs.Dir = std.fs.cwd();
var input_dir = cwd.openDir(“input”, .{}) catch |err| {…};
defer input_dir.close();

var path_buf: []u8 = undefined;
const file_path = std.fmt.bufPrint(&path_buf, “{d}.txt”, .{day}) catch |err| {…};

const file = input_dir.openFile(file_path, .{ .mode = .read_only }) catch |err| {…};
defer file.close();

This whole exercise leaves me with two questions:

  1. Is that simplified example the more Zig-idiomatic version of what I’m trying to do? It just seems like a waste to use a slice when I allocated a buffer specifically, but maybe I’m overthinking it and there’s not really any waste since it’s still just a slice of that buffer at the end of the day.
  2. Is openFile not using a sentinel-value slice/array appropriately? Or am I just making a bad assumption of how it ought to work?

/edit: Forgot to mention I’m on Windows, which impacts paths quite a bit

I think you forgot to make path_buf an array.

I think you are misunderstanding what a slice is, it is a pointer and a runtime length. This is absolutely the usecase for a slice. It sounds like you think it’s being allocated on the heap, which is not true. If there isn’t an allocator involved, it’s not on the heap.

In this case its pointing to the buffer you made, with the runtime length of the printed data.

again, misunderstanding what a sentinel means in regards to slice/array, it means after the length specified by the array/slice, there is the sentinel. It is incorrect to have the sentinel within the length of the array/slice. The only reason you are allowed to insert the sentinel is so that you can create a subslice that does have it after the length.
ie:

// if this had a sentinel `0` then buf would have 10 `5`s then a `0`
const buf: [10]u8 = @splat(5); //prefered way to fill an array with a single value
// insert sentinel
buf[3] = 0;
// create a sentinel slice
const slice: [:0]const u8 = buf[0..3 :0]; // 3 `5`s then a `0`
// [start..end :sentinel] will *assert* that the sentinel is *already* there
// buf doesnt need to itself be 0 terminated

to put it simply the length of an array/slice is trusted more then the sentinel.
ps: sentinels are most useful when you need a pointer without length information, usually when calling C code. you can get that with [*:0]T which is a sentinel terminated pointer to an unknown number of items.
pps: you can get the above pointer from a slice, slice.ptr.

3 Likes

Updated path_buf in my example–that was a sloppy copy-paste-edit on my part to show what I was doing.

As for everything else: thank you! Great explanation, and TIL about @splat.

also openFile takes a non terminated slice, terminated slices/arrays coerce to non terminated versions.

so it is unaware you were ever trying to use one. Aside from the check that returned the BadPathName error, but thats less a sentinel check and more of a this shouldnt be here check, because under the hood it terns it into a sentinel terminated slice before passing to to the OS, because the OS doesnt take length information so it uses a sentinel.

If it let you put a 0 in the middle, the Os would think its a different path then what you intended.

2 Likes