candypunk/NanoJev-Web
126
1"""Verify or export the exact public files listed in MANIFEST.json."""2import argparse,gzip,hashlib,json,tarfile3from pathlib import Path4ROOT=Path(__file__).resolve().parents[1]5FORBIDDEN={'.local','.runtime','.venv','node_modules','__pycache__','.git','runs','bench'}6def digest(path):7 h=hashlib.sha256()8 with path.open('rb') as f:9 for block in iter(lambda:f.read(8*1024*1024),b''):h.update(block)10 return h.hexdigest()11def verify():12 manifest=json.loads((ROOT/'MANIFEST.json').read_text())13 for name,info in manifest['files'].items():14 p=Path(name)15 if name=='bench.command' or p.is_absolute() or '..' in p.parts or set(p.parts)&FORBIDDEN or any(x.startswith('.env') for x in p.parts):raise ValueError('Forbidden manifest path')16 file=ROOT/p17 if file.is_symlink() or not file.is_file() or ROOT not in file.resolve().parents:raise ValueError('Release file missing or not a regular contained file')18 if file.stat().st_size!=info['bytes'] or digest(file)!=info['sha256']:raise ValueError('Release checksum mismatch: '+name)19 return manifest20 21def export(path,manifest):22 path=path.resolve()23 if path==ROOT or ROOT in path.parents:raise ValueError('Export outside the release directory')24 path.parent.mkdir(parents=True,exist_ok=True)25 if path.exists():raise ValueError('Output already exists; choose a new filename')26 with path.open('xb') as raw,gzip.GzipFile(filename='',fileobj=raw,mode='wb',mtime=0,compresslevel=6) as gz,tarfile.open(fileobj=gz,mode='w') as archive:27 for name in sorted([*manifest['files'],'MANIFEST.json']):28 file=ROOT/name;info=tarfile.TarInfo('NanoJev-Web/'+name)29 info.size=file.stat().st_size;info.mode=0o755 if name.endswith('.command') or name.endswith('.sh') else 0o64430 info.uid=info.gid=0;info.uname=info.gname='';info.mtime=031 with file.open('rb') as src:archive.addfile(info,src)32 checksum=digest(path);path.with_suffix(path.suffix+'.sha256').write_text(checksum+' '+path.name+'\n')33 print(json.dumps({'archive':path.name,'bytes':path.stat().st_size,'sha256':checksum}))34if __name__=='__main__':35 p=argparse.ArgumentParser(description=__doc__);p.add_argument('--export',type=Path);a=p.parse_args();m=verify()36 print(json.dumps({'verified':True,'files':len(m['files']),'bytes':sum(i['bytes'] for i in m['files'].values())}))37 if a.export:export(a.export,m)38 