// A zig test case that exercises some glibc symbols that have uncovered
// problems in the past.  This test must be compiled against a glibc.
//
// The build.zig tests the binary built from this source to see that
// symbols are statically or dynamically linked, as expected.

const std = @import("std");
const builtin = @import("builtin");
const assert = std.debug.assert;

const c_malloc = @import("malloc.h"); // for reallocarray
const c_stdlib = @import("stdlib.h"); // for atexit
const c_string = @import("string.h"); // for strlcpy

// Version of glibc this test is being built to run against
const glibc_ver = builtin.os.versionRange().gnuLibCVersion().?;

extern "c" fn fstatat(dirfd: i32, path: [*:0]const u8, buf: [*]const u8, flag: u32) c_int;
extern "c" fn stat(noalias path: [*:0]const u8, noalias buf: [*]const u8) c_int;

// PR #17034 - fstat moved between libc_nonshared and libc
fn checkStat() !void {
    const cwd_fd = std.Io.Dir.cwd().handle;

    var buf: [256]u8 = @splat(0);
    var result = fstatat(cwd_fd, "a_file_that_definitely_does_not_exist", &buf, 0);
    assert(result == -1);
    assert(std.posix.errno(result) == .NOENT);

    result = stat("a_file_that_definitely_does_not_exist", &buf);
    assert(result == -1);
    assert(std.posix.errno(result) == .NOENT);
}

// PR #17607 - reallocarray not visible in headers
fn checkReallocarray() !void {
    // reallocarray was introduced in v2.26
    if (comptime glibc_ver.order(.{ .major = 2, .minor = 26, .patch = 0 }) == .lt) {
        if (@hasDecl(c_malloc, "reallocarray")) {
            @compileError("Before v2.26 'malloc.h' does not define 'reallocarray'");
        }
    } else {
        return try checkReallocarray_v2_26();
    }
}

fn checkReallocarray_v2_26() !void {
    const size = 16;
    const tenX = c_malloc.reallocarray(c_malloc.NULL, 10, size);
    const elevenX = c_malloc.reallocarray(tenX, 11, size);

    assert(tenX != c_malloc.NULL);
    assert(elevenX != c_malloc.NULL);
}

// getauxval introduced in v2.16
fn checkGetAuxVal() !void {
    if (comptime glibc_ver.order(.{ .major = 2, .minor = 16, .patch = 0 }) == .lt) {
        if (@hasDecl(std.c, "getauxval")) {
            @compileError("Before v2.16 glibc does not define 'getauxval'");
        }
    } else {
        try checkGetAuxVal_v2_16();
    }
}

fn checkGetAuxVal_v2_16() !void {
    const base = std.c.getauxval(std.elf.AT_BASE);
    const pgsz = std.c.getauxval(std.elf.AT_PAGESZ);

    assert(base != 0);
    assert(pgsz != 0);
}

// strlcpy introduced in v2.38, which is newer than many installed glibcs
fn checkStrlcpy() !void {
    if (comptime glibc_ver.order(.{ .major = 2, .minor = 38, .patch = 0 }) == .lt) {
        if (@hasDecl(c_string, "strlcpy")) {
            @compileError("Before v2.38 glibc does not define 'strlcpy'");
        }
    } else {
        try checkStrlcpy_v2_38();
    }
}

fn checkStrlcpy_v2_38() !void {
    var buf: [99]u8 = undefined;
    const used = c_string.strlcpy(&buf, "strlcpy works!", buf.len);
    assert(used == 14);
}

// atexit is part of libc_nonshared, so ensure its linked in correctly
fn forceExit0Callback() callconv(.c) void {
    std.c.exit(0); // Override the main() exit code
}

fn checkAtExit() !void {
    const result = c_stdlib.atexit(forceExit0Callback);
    assert(result == 0);
}

pub fn main() !u8 {
    try checkStat();
    try checkReallocarray();
    try checkStrlcpy();

    try checkGetAuxVal();
    try checkAtExit();

    std.c.exit(1); // overridden by atexit() callback
}
