Files
2026-10-30 23:54:07 +07:00

230 lines
7.0 KiB
JavaScript

import fs from 'node:fs';
import path from 'node:path';
import os from 'node:os';
import {
createHash,
randomUUID
} from 'node:crypto';
import {
fileURLToPath
} from 'node:url';
export {
fs,
path,
os
};
export const HERE=path.dirname(fileURLToPath(import.meta.url)), ROOT=path.resolve(HERE, '../..');
export class DecodeError extends Error {
}
export const fail=m=>{
throw new DecodeError(m);
};
export const sha256=b=>createHash('sha256').update(b).digest('hex');
export function sha256_file(p){
const fd=fs.openSync(p, 'r'),
h=createHash('sha256'),
b=Buffer.alloc(1024*1024);
try{
let n;
while((n=fs.readSync(fd, b, 0, b.length, null)))h.update(b.subarray(0, n));
return h.digest('hex');
} finally{
fs.closeSync(fd);
}
}
export const readText=p=>fs.readFileSync(p, 'utf8').replace(/^\uFEFF/, '');
export const escapeRE=s=>s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
export const matches=(s, re)=>[...s.matchAll(re)];
export const small=v=>typeof v==='bigint'&&v<=BigInt(Number.MAX_SAFE_INTEGER)&&v>=BigInt(Number.MIN_SAFE_INTEGER)?Number(v):v;
export const zigzag=v=>small((BigInt(v)>>1n)^-(BigInt(v)&1n));
export const cat=(...parts)=>Buffer.concat(parts.flat());
export function dict(entries=[]){
const keys=[],
target={
};
const out=new Proxy(target, {
ownKeys:()=>keys,
set(t, k, v){
if(!Object.hasOwn(t, k))keys.push(k);
Reflect.defineProperty(t,k,{value:v,writable:true,enumerable:true,configurable:true});
return true;
},
deleteProperty(t, k){
const i=keys.indexOf(k);
if(i>=0)keys.splice(i, 1);
return delete t[k];
}
});
for(const [k, v]of entries)out[String(k)]=v;
return out;
}
export class Float {
constructor(value){
this.value=value;
}
}
export function json(value, indent=2, ascii=false){
const quote=s=>{
let q=JSON.stringify(s);
return ascii?q.replace(/[\u007f-\uffff]/g, c=>'\\u'+c.charCodeAt(0).toString(16).padStart(4, '0')):q;
};
const walk=(v, d)=>{
if(v===null)return 'null';
if(v instanceof Float){
let s=String(v.value);
return s.includes('.')||s.includes('e')?s:s+'.0';
}
if(typeof v==='bigint')return String(v);
if(typeof v==='string')return quote(v);
if(typeof v==='number'){
if(!Number.isFinite(v))fail('Non-finite JSON number');
return String(v);
}
if(typeof v==='boolean')return String(v);
if(typeof v!=='object')fail('Unsupported JSON value');
const arr=Array.isArray(v),
entries=arr?v.map(x=>walk(x, d+1)):Object.keys(v).map(k=>quote(k)+': '+walk(v[k], d+1));
if(!entries.length)return arr?'[]':'{}';
const pad=' '.repeat(indent*(d+1));
return (arr?'[':'{')+'\n'+pad+entries.join(',\n'+pad)+'\n'+' '.repeat(indent*d)+(arr?']':'}');
};
return walk(value, 0)+'\n';
}
// Parse integers losslessly and retain dictionary insertion order, including numeric keys.
export function parseJSON(text){
let i=0;
const ws=()=>{
while(/\s/.test(text[i]??'')&&i<text.length)i++;
};
function value(){
ws();
let c=text[i];
if(c==='"'){
let start=i++;
while(i<text.length){
if(text[i++]==='"')return JSON.parse(text.slice(start, i));
if(text[i-1]==='\\')i++;
}
fail('Unterminated JSON string');
}
if(c==='{'||c==='['){
i++;
const a=c==='[',
out=a?[]:dict();
ws();
if(text[i]===(a?']':'}')){
i++;
return out;
}
for(; ; ){
if(a)out.push(value());
else{
const k=value();
ws();
if(text[i++]!==':')fail('Invalid JSON object');
out[k]=value();
}
ws();
c=text[i++];
if(c===(a?']':'}'))return out;
if(c!==',')fail('Invalid JSON separator');
}
}
for(const [s, v]of [['true', true], ['false', false], ['null', null]])if(text.startsWith(s, i)){
i+=s.length;
return v;
}
const m=text.slice(i).match(/^-?(?:0|[1-9]\d*)(?:\.\d+)?(?:[eE][+-]?\d+)?/);
if(!m)fail('Invalid JSON value');
i+=m[0].length;
return /[.eE]/.test(m[0])?Number(m[0]):small(BigInt(m[0]));
}
const out=value();
ws();
if(i!==text.length)fail('Trailing JSON');
return out;
}
export const loadJSON=p=>parseJSON(readText(p));
export function replace(p, data){
fs.mkdirSync(path.dirname(p), {
recursive:true
});
const tmp=path.join(path.dirname(p), '.tmp-'+randomUUID());
let fd;
try{
fd=fs.openSync(tmp, 'wx', 0o600);
fs.writeFileSync(fd, data);
fs.fsyncSync(fd);
fs.closeSync(fd);
fd=undefined;
fs.chmodSync(tmp, 0o644);
fs.renameSync(tmp, p);
} finally{
if(fd!==undefined)fs.closeSync(fd);
fs.rmSync(tmp, {
force:true
});
}
}
export const atomic_json=(p, v, indent=2, ascii=false)=>replace(p, json(v, indent, ascii));
export const globBytes=dir=>fs.existsSync(dir)?fs.readdirSync(dir).filter(n=>n.endsWith('.bytes')).sort().map(n=>path.join(dir, n)):[];
export function cli(required=[], defaults={
}, bools=[], multi=[]){
const a={
...defaults
};
for(let i=2; i<process.argv.length; i++){
const flag=process.argv[i];
if(flag==='--help'||flag==='-h'){
console.log(`Usage: node ${path.basename(process.argv[1])} ${required.map(k=>'--'+k+' VALUE').join(' ')}\nOptions: ${[...Object.keys(defaults),...bools,...multi].map(k=>'--'+k).join(', ')}`);
process.exit(0);
}
const m=flag.match(/^--([\w-]+)(?:=(.*))?$/);
if(!m)fail('Unknown argument '+flag);
const k=m[1];
if(![...required, ...Object.keys(defaults), ...bools, ...multi].includes(k))fail('Unknown argument '+flag);
if(bools.includes(k)){
if(m[2]!==undefined)fail('Flag takes no value: --'+k);
a[k]=true;
continue;
}
const v=m[2]??process.argv[++i];
if(v===undefined||v.startsWith('--'))fail('Missing value '+flag);
if(multi.includes(k))(a[k]??=[]).push(v);
else if(typeof defaults[k]==='number'||k==='season-id'){
if(!/^[+-]?\d+$/.test(v)||!Number.isSafeInteger(Number(v)))fail('Expected integer for --'+k);
a[k]=Number(v);
} else a[k]=v;
}
for(const k of required)if(a[k]===undefined)fail('Required --'+k);
return a;
}
export function runMain(url, fn){
if(process.argv[1]&&path.resolve(process.argv[1])===fileURLToPath(url)){
try{
const result=fn();
if(result?.catch)result.catch(e=>{
console.error('Extraction stopped: '+e.message);
process.exitCode=1;
});
} catch(e){
console.error('Extraction stopped: '+e.message);
process.exitCode=1;
}
}
}
export const subset=(a, b)=>[...a].every(v=>b.has(v));
export const uniqueValues=(rows, key)=>new Set(rows.map(key)).size===rows.length;
export function lookup(rows, key, label='native key'){
const out=new Map();
for(const r of rows){
const k=key(r);
if(out.has(k))fail('Duplicate '+label);
out.set(k, r);
}
return out;
}
export const need=(map, key)=>map.get(key)??fail('Missing native reference '+key);
export const resource=name=>path.join(ROOT, 'resources/grid_fight', name);
export const catalogDefault=path.join(ROOT, 'protocol/evidence/cw-mechanics/catalog.json');