Reimplementazione fedele di cellar-cli.py (enne2/cellar @ f5216b1) in C99 POSIX.1-2008, mono-thread, senza dipendenze esterne: miniz e' vendored per deflate/inflate raw, mentre contenitore gzip, multipart/form-data in streaming, tar PAX/ustar, JSON e client HTTP/1.1 sono scritti a mano. - 7 comandi (list, upload, download, install, scan-local, wizard-upload, wizard-install) con stdout/stderr/exit code identici al client Python - 30/30 test di parita' (tests/parity_test.sh) contro due server mock indipendenti: output a confronto, alberi installati, interop tar con tarfile di Python e con GNU tar, archivio C equivalente a quello Python - build statiche x86-64 / i686 / aarch64: nessun simbolo GLIBC richiesto, nessuna syscall moderna (statx/openat2/memfd_create/getrandom), LFS 64 bit, resolver DNS di riserva (hosts + query UDP) per glibc statica senza NSS - miglioramento rispetto all'originale: upload multipart in streaming invece di leggere l'intero archivio in RAM - divergenze volute documentate nel README (help piu' sintetico, mtime dei symlink non ripristinato come tarfile, EOF nei prompt, niente TLS) Build: make | make 32 | make arm64 | make test | make verify
67 lines
1.9 KiB
Python
Executable File
67 lines
1.9 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
"""Confronta due archivi tar.gz membro per membro (parita' writer C vs Python).
|
|
|
|
Uso: tar_compare.py a.tar.gz b.tar.gz [--tolerance 1e-6]
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
import argparse
|
|
import hashlib
|
|
import sys
|
|
import tarfile
|
|
|
|
|
|
def load(path: str):
|
|
out = {}
|
|
with tarfile.open(path, "r:gz") as tf:
|
|
for m in tf.getmembers():
|
|
digest = "-"
|
|
if m.isreg():
|
|
fh = tf.extractfile(m)
|
|
if fh is not None:
|
|
digest = hashlib.sha256(fh.read()).hexdigest()
|
|
out[m.name] = {
|
|
"type": m.type.decode() if isinstance(m.type, bytes) else m.type,
|
|
"mode": m.mode,
|
|
"uid": m.uid,
|
|
"gid": m.gid,
|
|
"size": m.size,
|
|
"link": m.linkname,
|
|
"mtime": m.mtime,
|
|
"sha": digest,
|
|
}
|
|
return out
|
|
|
|
|
|
def main() -> int:
|
|
ap = argparse.ArgumentParser()
|
|
ap.add_argument("a")
|
|
ap.add_argument("b")
|
|
ap.add_argument("--tolerance", type=float, default=1e-6)
|
|
args = ap.parse_args()
|
|
a, b = load(args.a), load(args.b)
|
|
ok = True
|
|
for name in sorted(set(a) | set(b)):
|
|
if name not in a:
|
|
print(f"SOLO IN B: {name}")
|
|
ok = False
|
|
continue
|
|
if name not in b:
|
|
print(f"SOLO IN A: {name}")
|
|
ok = False
|
|
continue
|
|
x, y = a[name], b[name]
|
|
for field in ("type", "mode", "uid", "gid", "size", "link", "sha"):
|
|
if x[field] != y[field]:
|
|
print(f"DIFF {name}: {field} A={x[field]!r} B={y[field]!r}")
|
|
ok = False
|
|
if abs(x["mtime"] - y["mtime"]) > args.tolerance:
|
|
print(f"DIFF {name}: mtime A={x['mtime']:.7f} B={y['mtime']:.7f}")
|
|
ok = False
|
|
print("archivi equivalenti" if ok else "archivi DIVERSI")
|
|
return 0 if ok else 1
|
|
|
|
|
|
if __name__ == "__main__":
|
|
raise SystemExit(main())
|