230 lines
7.0 KiB
JavaScript
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');
|