Accumulated uncommitted work: GHOST Web backend, WebGL/GPU web layer, web workflow engine and protocol, tooling, docs and task records. Large generated evidence, user-testing archives, build snapshots and imported test assets remain untracked. Co-Authored-By: Claude Code <noreply@anthropic.com>
89 lines
6.1 KiB
JavaScript
89 lines
6.1 KiB
JavaScript
import fs from 'node:fs';
|
|
import path from 'node:path';
|
|
import os from 'node:os';
|
|
import crypto from 'node:crypto';
|
|
|
|
const bundles=new Map();
|
|
const LIMIT=256*1024*1024;
|
|
const hash=data=>crypto.createHash('sha256').update(data).digest('hex');
|
|
export function resourcePath(value){
|
|
if(typeof value!=='string'||value.length>1024||value.includes('\\')||value.includes('\0')||value.startsWith('/')||/^[A-Za-z]:/.test(value)||value.split('/').some(p=>!p||p==='.'||p==='..'))throw new Error('RESOURCE_PATH_INVALID');
|
|
return value;
|
|
}
|
|
export function createResourceBundle(data){
|
|
if(data.length<8||data.subarray(0,4).toString()!=='BAR1')throw new Error('RESOURCE_ENVELOPE_INVALID');
|
|
const length=data.readUInt32LE(4);
|
|
if(length>2*1024*1024||data.length>LIMIT+2*1024*1024||8+length>data.length)throw new Error('RESOURCE_BUDGET');
|
|
const metadata=JSON.parse(data.subarray(8,8+length).toString());
|
|
if(!Array.isArray(metadata.files)||metadata.files.length>4096||bundles.size>=8)throw new Error('RESOURCE_CAPACITY');
|
|
const files=[],names=new Set(),aliases=new Set();let offset=8+length;
|
|
for(const item of metadata.files){
|
|
const name=resourcePath(item.path);
|
|
if(names.has(name)||!Number.isSafeInteger(item.bytes)||item.bytes<0||offset+item.bytes>data.length||!/^[a-f0-9]{64}$/.test(item.sha256))throw new Error('RESOURCE_MANIFEST_INVALID');
|
|
if(item.sourcePath!==undefined&&(typeof item.sourcePath!=='string'||item.sourcePath.length>2048||item.sourcePath.includes('\0')||aliases.has(item.sourcePath)))throw new Error('RESOURCE_ALIAS_INVALID');
|
|
names.add(name);if(item.sourcePath)aliases.add(item.sourcePath);
|
|
const content=data.subarray(offset,offset+item.bytes);offset+=item.bytes;
|
|
if(hash(content)!==item.sha256)throw new Error('RESOURCE_HASH_MISMATCH');
|
|
files.push({path:name,bytes:item.bytes,sha256:item.sha256,...(item.sourcePath?{sourcePath:item.sourcePath}:{}),content});
|
|
}
|
|
if(offset!==data.length)throw new Error('RESOURCE_TRAILING_DATA');
|
|
const root=fs.mkdtempSync(path.join(os.tmpdir(),'blender-resources-'));
|
|
try{for(const file of files){const dest=path.join(root,file.path);fs.mkdirSync(path.dirname(dest),{recursive:true});fs.writeFileSync(dest,file.content,{mode:0o600});delete file.content;}}
|
|
catch(error){fs.rmSync(root,{recursive:true,force:true});throw error;}
|
|
const token=crypto.randomBytes(32).toString('hex'),manifest={files},sha256=hash(Buffer.from(JSON.stringify(manifest)));
|
|
bundles.set(token,{root,manifest,sha256,touched:Date.now()});
|
|
return {token,sha256,files};
|
|
}
|
|
export function materializeResources(binding,workspace){
|
|
if(!binding)return;
|
|
const bundle=bundles.get(binding.token);
|
|
if(!bundle||bundle.sha256!==binding.sha256)throw new Error('RESOURCE_BUNDLE_EXPIRED_OR_CHANGED');
|
|
bundle.touched=Date.now();
|
|
fs.cpSync(bundle.root,path.join(workspace,'resources'),{recursive:true,errorOnExist:true});
|
|
fs.writeFileSync(path.join(workspace,'resources.json'),JSON.stringify(bundle.manifest));
|
|
}
|
|
export function releaseResourceBundle(binding){
|
|
const bundle=bundles.get(binding?.token);if(!bundle||bundle.sha256!==binding.sha256)return;
|
|
bundles.delete(binding.token);fs.rmSync(bundle.root,{recursive:true,force:true});
|
|
}
|
|
export function describeResourceBundle(binding){
|
|
const bundle=bundles.get(binding?.token);
|
|
if(!bundle||bundle.sha256!==binding.sha256)throw new Error('RESOURCE_BUNDLE_EXPIRED_OR_CHANGED');
|
|
bundle.touched=Date.now();return {sha256:bundle.sha256,files:bundle.manifest.files};
|
|
}
|
|
export function exportResourceBundle(binding){
|
|
describeResourceBundle(binding);
|
|
const bundle=bundles.get(binding.token);
|
|
const buffers=bundle.manifest.files.map(file=>{
|
|
const data=fs.readFileSync(path.join(bundle.root,file.path));
|
|
if(data.length!==file.bytes||hash(data)!==file.sha256)throw new Error('RESOURCE_HASH_MISMATCH');
|
|
return data;
|
|
});
|
|
bundle.touched=Date.now();
|
|
const metadata=Buffer.from(JSON.stringify(bundle.manifest)),header=Buffer.alloc(8);
|
|
header.write('BAR1');header.writeUInt32LE(metadata.length,4);
|
|
return Buffer.concat([header,metadata,...buffers]);
|
|
}
|
|
export function captureResourceBundle(workspace,metadataOnly=false){
|
|
const directory=path.join(workspace,'resources');if(!fs.existsSync(directory))return undefined;
|
|
const directoryInfo=fs.lstatSync(directory);if(!directoryInfo.isDirectory()||directoryInfo.isSymbolicLink())throw new Error('RESOURCE_OUTPUT_DIRECTORY_INVALID');
|
|
const manifestPath=path.join(workspace,'resources.json');if(fs.existsSync(manifestPath)){const info=fs.lstatSync(manifestPath);if(!info.isFile()||info.isSymbolicLink()||info.size>2*1024*1024)throw new Error('RESOURCE_OUTPUT_MANIFEST_INVALID');}
|
|
const old=fs.existsSync(path.join(workspace,'resources.json'))?JSON.parse(fs.readFileSync(path.join(workspace,'resources.json'),'utf8')).files:[];
|
|
const aliases=new Map(old.map(file=>[file.path,file.sourcePath]));
|
|
const files=[],buffers=[];let total=0;
|
|
const visit=folder=>{for(const entry of fs.readdirSync(folder,{withFileTypes:true})){
|
|
const absolute=path.join(folder,entry.name);const stat=fs.lstatSync(absolute);
|
|
if(stat.isSymbolicLink())throw new Error('RESOURCE_OUTPUT_SYMLINK');
|
|
if(stat.isDirectory())visit(absolute);
|
|
else if(stat.isFile()){
|
|
total+=stat.size;if(total>LIMIT||files.length>=4096)throw new Error('RESOURCE_OUTPUT_BUDGET');
|
|
const relative=path.relative(directory,absolute).split(path.sep).join('/');resourcePath(relative);
|
|
const data=fs.readFileSync(absolute);if(!metadataOnly)buffers.push(data);files.push({path:relative,bytes:data.length,sha256:hash(data),...(aliases.get(relative)?{sourcePath:aliases.get(relative)}:{})});
|
|
}
|
|
}};visit(directory);
|
|
const metadata=Buffer.from(JSON.stringify({files}));if(metadata.length>2*1024*1024)throw new Error('RESOURCE_OUTPUT_MANIFEST_BUDGET');if(metadataOnly)return {files};
|
|
const header=Buffer.alloc(8);header.write('BAR1');header.writeUInt32LE(metadata.length,4);
|
|
return createResourceBundle(Buffer.concat([header,metadata,...buffers]));
|
|
}
|
|
const expiry=setInterval(()=>{for(const [token,bundle] of bundles)if(Date.now()-bundle.touched>60*60*1000)releaseResourceBundle({token,sha256:bundle.sha256});},60000);expiry.unref();
|