//! Filesystem free-space query. The Zig standard library has no `statvfs`, so
//! this file declares the libc entry point directly. Every nxdns build links
//! libc for sqlite3, so no target loses this.

const std = @import("std");

/// The 64-bit glibc and musl layouts of `struct statvfs` agree field for field.
/// `__reserved` covers glibc's `f_type` plus `__f_spare[5]` and musl's `f_type`
/// plus `__reserved[5]`; musl's anonymous bitfield before `f_fsid` is
/// zero-width on a 64-bit target. Both nxdns targets are 64-bit.
pub const StatVfs = extern struct {
    f_bsize: c_ulong,
    f_frsize: c_ulong,
    f_blocks: u64,
    f_bfree: u64,
    f_bavail: u64,
    f_files: u64,
    f_ffree: u64,
    f_favail: u64,
    f_fsid: c_ulong,
    f_flag: c_ulong,
    f_namemax: c_ulong,
    __reserved: [6]c_int,
};

extern fn statvfs(path: [*:0]const u8, buf: *StatVfs) c_int;

pub const Error = error{StatFailed};

/// Bytes free for an unprivileged writer on the filesystem holding `path`.
/// `f_bavail` excludes the reserved blocks that `f_bfree` counts, so this is
/// the number the disk monitor's thresholds must compare against.
pub fn freeBytes(path: [:0]const u8) Error!u64 {
    var buf: StatVfs = undefined;
    if (statvfs(path.ptr, &buf) != 0) return error.StatFailed;
    return buf.f_bavail * buf.f_frsize;
}

const testing = std.testing;

test "freeBytes reports space on the working directory" {
    const free = try freeBytes(".");
    try testing.expect(free > 0);
}

test "freeBytes fails on a path that does not exist" {
    try testing.expectError(error.StatFailed, freeBytes("./nxdns-no-such-path-9d3f"));
}

test "the struct matches the C ABI size and offsets" {
    // A layout mismatch would silently read the wrong field rather than fail,
    // so the offsets are pinned here.
    try testing.expectEqual(@as(usize, 0), @offsetOf(StatVfs, "f_bsize"));
    try testing.expectEqual(@as(usize, 8), @offsetOf(StatVfs, "f_frsize"));
    try testing.expectEqual(@as(usize, 16), @offsetOf(StatVfs, "f_blocks"));
    try testing.expectEqual(@as(usize, 32), @offsetOf(StatVfs, "f_bavail"));
    try testing.expectEqual(@as(usize, 64), @offsetOf(StatVfs, "f_fsid"));
    try testing.expectEqual(@as(usize, 80), @offsetOf(StatVfs, "f_namemax"));
    try testing.expectEqual(@as(usize, 112), @sizeOf(StatVfs));
}
