#!/overlayer/syshub/bin/python3
# glibc next to musl on an installed Zainium OS: checks every rule of
# https://zainiumdynamics.tech/docs/musl-and-glibc against the running system.
# Run as the normal user: python3 -I ~/zainium-test/glibc-tests.py
import os, re, shutil, subprocess, time

W = os.path.expanduser("~/zainium-test/glibc")
LOG = os.path.expanduser("~/zainium-test/glibc-tests.txt")
G = "/overlayer/zexlib/union/x86_64-zainium-linux-gnu"
GLD = f"{G}/lib/ld-linux-x86-64.so.2"
MLD = "/overlayer/syshub/x86_64-zainium-linux-musl/lib/ld-musl-x86_64.so.1"
GCC = "x86_64-zainium-linux-gnu-gcc"
GXX = "x86_64-zainium-linux-gnu-g++"
results = []
out = open(LOG, "w")


def log(s=""):
    print(s)
    out.write(s + "\n")
    out.flush()


def check(name, ok, detail=""):
    results.append((name, ok))
    log(f"[{'PASS' if ok else 'FAIL'}] {name}" + (f" -- {detail}" if detail else ""))


def run(*a, **k):
    return subprocess.run(a, capture_output=True, text=True, **k)


def interp(path):
    r = run("readelf", "-l", path).stdout
    m = re.search(r"Requesting program interpreter: (\S+)\]", r)
    return m.group(1) if m else ""


shutil.rmtree(W, ignore_errors=True)
os.makedirs(W)
os.chdir(W)
log(f"Zainium OS glibc tests -- {time.strftime('%Y-%m-%d %H:%M:%S %z')}")
log(f"kernel: {os.uname().release}")

log("\n== Installed ==")
check("glibc loader exists", os.path.exists(GLD), GLD)
v = run(GLD, "--version").stdout.splitlines()
check("glibc loader runs", bool(v), v[0] if v else "")
check("libc.so.6 is in glibc's directory", os.path.exists(f"{G}/lib/libc.so.6"))
check("glibc compiler on PATH", shutil.which(GCC) is not None, shutil.which(GCC) or "")

C_TEST = r'''
#define _GNU_SOURCE
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <pwd.h>
#include <grp.h>
#include <netdb.h>
#include <time.h>
#include <pthread.h>
#include <dlfcn.h>
#include <locale.h>
#include <iconv.h>
#include <gnu/libc-version.h>
#include <unistd.h>

static void *thr(void *p) { *(int *)p = 42; return 0; }

int main(void) {
    printf("libc=%s\n", gnu_get_libc_version());
    struct passwd *pw = getpwnam("root");
    printf("passwd_root=%s\n", pw ? pw->pw_dir : "-");
    struct passwd *me = getpwuid(getuid());
    printf("passwd_me=%s\n", me ? me->pw_name : "-");
    struct group *gr = getgrnam("wheel");
    printf("group_wheel=%s\n", gr ? "yes" : "-");
    struct addrinfo *ai = 0;
    printf("host_localhost=%s\n", getaddrinfo("localhost", 0, 0, &ai) == 0 ? "yes" : "-");
    if (ai) freeaddrinfo(ai), ai = 0;
    printf("dns=%s\n", getaddrinfo("archive.zainiumdynamics.tech", "443", 0, &ai) == 0 ? "yes" : "-");
    if (ai) freeaddrinfo(ai);
    time_t t = time(0); struct tm lt; localtime_r(&t, &lt); tzset();
    printf("tz=%s\n", tzname[0]);
    int x = 0; pthread_t th; pthread_create(&th, 0, thr, &x); pthread_join(th, 0);
    printf("thread=%d\n", x);
    void *m = dlopen("libm.so.6", RTLD_NOW);
    double (*c)(double) = m ? (double (*)(double))dlsym(m, "cos") : 0;
    printf("dlopen_libm=%s\n", c && c(0.0) == 1.0 ? "yes" : "-");
    printf("locale=%s\n", setlocale(LC_ALL, "C.UTF-8") ? "yes" : "-");
    iconv_t cd = iconv_open("ISO-8859-1", "UTF-8");
    char in[] = "\xc3\xa9", outb[8], *ip = in, *op = outb; size_t il = 2, ol = sizeof outb;
    int ok = cd != (iconv_t)-1 && iconv(cd, &ip, &il, &op, &ol) != (size_t)-1 && (unsigned char)outb[0] == 0xe9;
    printf("iconv=%s\n", ok ? "yes" : "-");
    printf("system_sh=%d\n", system("exit 7") >> 8);
    FILE *f = fopen("/proc/self/maps", "r"); char line[512]; int musl_lib = 0;
    while (f && fgets(line, sizeof line, f))
        if ((strstr(line, "/overlayer/syshub/lib/") || strstr(line, "ld-musl"))
            && !strstr(line, "libsys_resolver.so")) musl_lib = 1;
    printf("maps_musl=%s\n", musl_lib ? "yes" : "no");
    return 0;
}
'''
open("t.c", "w").write(C_TEST)
CXX_TEST = r'''
#include <iostream>
#include <string>
#include <vector>
#include <fstream>
int main() {
    std::vector<std::string> v{"glibc", "c++"};
    std::string s; for (auto &x : v) s += x;
    std::ifstream m("/proc/self/maps"); std::string l, where = "-";
    while (std::getline(m, l)) if (l.find("libstdc++") != std::string::npos) { where = l.substr(l.find('/')); break; }
    std::cout << s << " " << where << std::endl;
}
'''
open("t.cc", "w").write(CXX_TEST)
open("h.c", "w").write('#include <stdio.h>\nint main(void){puts("hello");return 0;}\n')

log("\n== Building ==")
r = run(GCC, "-O2", "t.c", "-o", "t-glibc", "-lpthread", "-ldl")
check("glibc gcc builds a C program", r.returncode == 0, r.stderr.strip()[-200:])
r = run("gcc", "-O2", "h.c", "-o", "h-musl")
check("plain gcc builds for musl", r.returncode == 0, r.stderr.strip()[-200:])
r = run(GCC, "-O2", "h.c", "-o", "h-glibc")
check("the same program builds for glibc", r.returncode == 0, r.stderr.strip()[-200:])
r = run(GXX, "-O2", "t.cc", "-o", "t-cxx")
check("glibc g++ builds a C++ program", r.returncode == 0, r.stderr.strip()[-200:])

log("\n== Each program names its own loader ==")
check("musl program -> musl loader", interp("h-musl") == MLD, interp("h-musl"))
check("glibc program -> glibc loader", interp("h-glibc") == GLD, interp("h-glibc"))
a, b = run("./h-musl").stdout.strip(), run("./h-glibc").stdout.strip()
check("both run", a == b == "hello", f"musl={a!r} glibc={b!r}")

log("\n== glibc program: libraries and system configuration ==")
r = run("./t-glibc")
kv = dict(l.split("=", 1) for l in r.stdout.splitlines() if "=" in l)
check("runs", r.returncode == 0, r.stderr.strip()[-200:])
check("glibc version", kv.get("libc", "") != "", kv.get("libc", ""))
check("reads users (/overlayer/syshub/etc/passwd)", kv.get("passwd_root") == "/root", kv.get("passwd_root", ""))
check("knows the current user", kv.get("passwd_me") == run("id", "-un").stdout.strip(), "same name as id -un")
check("reads groups", kv.get("group_wheel") == "yes")
check("resolves localhost (hosts)", kv.get("host_localhost") == "yes")
check("resolves a name over DNS", kv.get("dns") == "yes")
musl_tz = run("date", "+%Z").stdout.strip()
check("same time zone as musl", kv.get("tz") == musl_tz or (kv.get("tz") in ("UTC", "GMT") and musl_tz in ("UTC", "GMT")), f"glibc={kv.get('tz')} musl={musl_tz}")
check("threads", kv.get("thread") == "42")
check("dlopen of a glibc library", kv.get("dlopen_libm") == "yes")
check("C.UTF-8 locale", kv.get("locale") == "yes")
check("iconv (gconv modules)", kv.get("iconv") == "yes")
check("system() runs /bin/sh", kv.get("system_sh") == "7", kv.get("system_sh", ""))
check("no musl library in the process (libsys_resolver.so, which needs no libc, is preloaded by design)", kv.get("maps_musl") == "no")
r = run("./t-cxx")
check("C++ program runs", r.returncode == 0 and r.stdout.startswith("glibcc++"), r.stdout.strip() or r.stderr.strip()[-200:])
check("libstdc++ comes from glibc's directory", G in r.stdout, r.stdout.strip().split(" ", 1)[-1])

log("\n== Prebuilt programs from other distributions ==")
r = run(GCC, "-O2", "h.c", "-o", "h-foreign", "-Wl,--dynamic-linker=/lib64/ld-linux-x86-64.so.2")
check("built one asking for /lib64/ld-linux-x86-64.so.2", r.returncode == 0 and interp("h-foreign") == "/lib64/ld-linux-x86-64.so.2", interp("h-foreign"))
r = run("./h-foreign")
check("it runs without /lib64 on disk", r.stdout.strip() == "hello" and not os.path.exists("/lib64"), r.stdout.strip() or r.stderr.strip()[-200:])

log("\n== Kept apart ==")
ldd = shutil.which("ldd") or ""
check("ldd on PATH is musl's", "glibc" not in ldd and G not in os.path.realpath(ldd or "/"), os.path.realpath(ldd) if ldd else "none")
check("glibc's ldd is in its own bin", os.path.exists(f"{G}/bin/ldd"))
h = run(GLD, "--help").stdout
check("glibc's loader cache is in its own etc", f"{G}/etc/ld.so.cache" in h, f"{G}/etc/ld.so.cache")
sp = [l.strip().split(" (")[0] for l in h.splitlines() if "(system search path)" in l]
check("glibc's loader searches only glibc's lib", sp == [f"{G}/lib"], ", ".join(sp))
check("no glibc libc.so.6 in the shared lib", not os.path.exists("/overlayer/syshub/lib/libc.so.6"))
bad = []
for top in ("bin", "sbin", "lib", "libexec"):
    for t, d, fs in os.walk(f"/overlayer/syshub/{top}"):
        for n in fs:
            p = os.path.join(t, n)
            if os.path.islink(p) or p.startswith("/overlayer/syshub/lib/modules"):
                continue
            try:
                with open(p, "rb") as f:
                    if f.read(4) != b"\x7fELF":
                        continue
            except OSError:
                continue
            i = interp(p)
            if "ld-linux" in i:
                bad.append(p)
# a file of the base, not one zex put in the userland on top of it
bad = [p for p in bad if not os.path.lexists(p.replace("/overlayer/syshub/", "/overlayer/zexlib/union/", 1))]
check("the base has no glibc-linked program", not bad, ", ".join(bad[:5]))

log("\n== Summary ==")
passed = sum(1 for _, ok in results if ok)
log(f"{passed}/{len(results)} checks passed")
for n, ok in results:
    if not ok:
        log(f"  FAILED: {n}")
out.close()
