import { spawnSync } from 'node:child_process'; import * as e from './common.mjs'; export { e }; export const digest=e.sha256_file, replace=e.replace; export function files(tree){ if(!e.fs.existsSync(tree))return []; const result=[]; function visit(p){ for(const name of e.fs.readdirSync(p).sort()){ const full=e.path.join(p, name), s=e.fs.lstatSync(full); if(s.isSymbolicLink())e.fail('Symlink in managed build: '+full); if(s.isDirectory())visit(full); else if(s.isFile())result.push(full); } } visit(tree); return result.sort(); } export const manifest=tree=>files(tree).filter(p=>e.path.basename(p)!=='SHA256SUMS').map(p=>`${digest(p)} ./${e.path.relative(tree,p)}\n`).join(''); export const CLIENT_FILES=['StarRail.exe', 'GameAssembly.dll', 'StarRail_Data/il2cpp_data/Metadata/global-metadata.dat', 'StarRail_Data/il2cpp_data/Metadata/startup-metadata.dat', 'StarRail_Data/StreamingAssets/BinaryVersion.bytes']; const REQUIRED=['server/gameserver', 'server/dispatch', 'server/config.json', 'server/hotfix.json', 'server/resources/AvatarConfig.json', 'protocol/root.zig', 'protocol/protocol.pb.zig', 'client-patch/launcher.exe', 'client-patch/hkrpg.dll', 'dump/client.json', 'dump/design-data/catalogue.json', 'BUILD-INFO.txt']; export function verify_bundle(bundle, version){ const info=Object.fromEntries(e.readText(e.path.join(bundle, 'BUILD-INFO.txt')).split('\n').filter(l=>l.includes('=')).map(l=>{ const at=l.indexOf('='); return [l.slice(0, at), l.slice(at+1)]; })); if(info.client_version!==version||info.mode!=='full')e.fail('Cached bundle does not match this full client profile'); const seen=new Set(); for(const line of e.readText(e.path.join(bundle, 'SHA256SUMS')).trimEnd().split('\n')){ const m=line.match(/^([0-9a-f]{64}) (?:\.\/)?(.+)$/); if(!m)e.fail('Invalid bundle manifest'); const [, checksum, name]=m; if(e.path.isAbsolute(name)||name.split('/').includes('..')||name.includes('\\')||seen.has(name))e.fail('Invalid or duplicate bundle path'); seen.add(name); if(digest(e.path.join(bundle, name))!==checksum)e.fail('Bundle checksum mismatch: '+name); } const actual=new Set(files(bundle).filter(p=>e.path.basename(p)!=='SHA256SUMS').map(p=>e.path.relative(bundle, p))); if(!e.subset(new Set(REQUIRED), seen)||seen.size!==actual.size||!e.subset(seen, actual))e.fail('Incomplete bundle manifest'); return info; } export function verify_client(bundle, client, version, mode){ const identity=e.loadJSON(e.path.join(bundle, 'dump/client.json')), names=identity.files.map(x=>x.path); if(identity.version!==version||names.length!==CLIENT_FILES.length||new Set(names).size!==CLIENT_FILES.length||!CLIENT_FILES.every(n=>names.includes(n)))e.fail('Incomplete or wrong client identity'); const found=e.fs.readFileSync(e.path.join(client, CLIENT_FILES.at(-1))).toString('latin1').match(/(?new Date().toISOString().replace(/[-:]/g, '').replace(/\.\d+Z$/, 'Z')+'-'+globalThis.crypto.randomUUID().replaceAll('-', '').slice(0, 8); export function snapshot(root, excluded=new Set()){ return Object.fromEntries(files(root).filter(p=>!excluded.has(p)).map(p=>[e.path.relative(root, p), digest(p)])); } export const equal=(a, b)=>{ const ak=Object.keys(a).sort(), bk=Object.keys(b).sort(); return ak.length===bk.length&&ak.every((k, i)=>k===bk[i]&&a[k]===b[k]); }; export function transaction(targets, backup, publish, check, reason='gridfight'){ const saved=[]; for(const [area, target]of targets){ let previous=null; if(e.fs.existsSync(target)){ previous=e.path.join(backup, area, e.path.basename(target)+'.bak.'+reason+'-'+e.path.basename(backup)); e.fs.mkdirSync(e.path.dirname(previous), { recursive:true }); e.fs.copyFileSync(target, previous); if(digest(previous)!==digest(target))e.fail('Backup verification failed'); } saved.push([target, previous]); } const published=new Set(); try{ publish((p, b)=>{ replace(p, b); published.add(p); }); check(); } catch(error){ for(const [p, prior]of saved.toReversed())if(published.has(p)){ if(prior)replace(p, e.fs.readFileSync(prior)); else e.fs.rmSync(p, { force:true }); } throw error; } }