Files
workinf_Blender_Wasm/tools/web/native-resources.mjs
mes123456 f3e21c6b98
Some checks failed
M6 deployable RC / quick (push) Has been cancelled
M6 deployable RC / chromium (push) Has been cancelled
M6 deployable RC / release (push) Has been cancelled
chore: checkpoint web Blender migration source, tools and docs
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>
2026-11-10 11:18:01 +08:00

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();