import fs from 'node:fs'; import path from 'node:path'; import os from 'node:os'; import crypto from 'node:crypto'; const profile = path.join(process.env.WEB_BLENDER_PROFILE_ROOT ?? path.join(os.homedir(), '.local', 'share', 'web-blender', '5.2'), 'config'); const digest = bytes => crypto.createHash('sha256').update(bytes).digest('hex'); export function nativeProfileDirectories(){ const result={}; for(const name of ['scripts','extensions','datafiles']){ const directory=path.join(path.dirname(profile),name);fs.mkdirSync(directory,{recursive:true,mode:0o700});result[name]=directory; } return result; } function readSaved() { const file = path.join(profile, 'userpref.blend'); if (!fs.existsSync(file)) return null; const stat = fs.lstatSync(file); if (!stat.isFile() || stat.isSymbolicLink() || stat.size > 16 * 1024 * 1024) throw new Error('PREFERENCES_FILE_INVALID'); return fs.readFileSync(file); } export function prepareNativePreferences(workspace) { const config = path.join(workspace, 'config'); if(fs.existsSync(config)){const stat=fs.lstatSync(config);if(!stat.isDirectory()||stat.isSymbolicLink())throw new Error('PREFERENCES_DIRECTORY_INVALID');}else fs.mkdirSync(config,{mode:0o700}); for(const name of ['userpref.blend','startup.blend']){const file=path.join(config,name);if(fs.existsSync(file)){const stat=fs.lstatSync(file);if(!stat.isFile()||stat.isSymbolicLink())throw new Error('PREFERENCES_WORKSPACE_FILE_INVALID');}} const bytes = readSaved(); if (bytes) fs.writeFileSync(path.join(config, 'userpref.blend'), bytes); const startup=path.join(profile,'startup.blend');let startupSha256=null; if(fs.existsSync(startup)){ const stat=fs.lstatSync(startup);if(!stat.isFile()||stat.isSymbolicLink()||stat.size>256*1024*1024)throw new Error('STARTUP_PROFILE_INVALID'); const data=fs.readFileSync(startup);startupSha256=digest(data);fs.writeFileSync(path.join(config,'startup.blend'),data); } fs.writeFileSync(path.join(workspace, 'preferences-base.json'), JSON.stringify({sha256: bytes ? digest(bytes) : null,startupSha256})); return config; } export function persistNativePreferences(workspace, artifact) { fs.mkdirSync(profile, {recursive: true, mode: 0o700}); const lock = path.join(profile, '.save-lock'); try { fs.mkdirSync(lock); } catch { throw new Error('PREFERENCES_SAVE_BUSY_RETRY'); } try { const base = JSON.parse(fs.readFileSync(path.join(workspace, 'preferences-base.json'), 'utf8')); const previous = readSaved(); if ((previous ? digest(previous) : null) !== base.sha256) throw new Error('PREFERENCES_CHANGED_IN_ANOTHER_SESSION_RECONNECT'); const bytes = fs.readFileSync(path.join(workspace, artifact.id)); if (bytes.length > 16 * 1024 * 1024 || digest(bytes) !== artifact.sha256) throw new Error('PREFERENCES_ARTIFACT_INVALID'); const temporary = path.join(profile, crypto.randomUUID() + '.tmp'); try { fs.writeFileSync(temporary, bytes, {mode: 0o600, flag: 'wx'}); fs.renameSync(temporary, path.join(profile, 'userpref.blend')); } finally { fs.rmSync(temporary, {force: true}); } fs.writeFileSync(path.join(workspace, 'preferences-base.json'), JSON.stringify({...base,sha256: artifact.sha256})); } finally { fs.rmdirSync(lock); } } export function persistNativeStartup(workspace){ fs.mkdirSync(profile,{recursive:true,mode:0o700});const lock=path.join(profile,'.save-lock'); try{fs.mkdirSync(lock);}catch{throw new Error('PREFERENCES_SAVE_BUSY_RETRY');} try{ const baseFile=path.join(workspace,'preferences-base.json'),base=JSON.parse(fs.readFileSync(baseFile,'utf8')); const destination=path.join(profile,'startup.blend'),source=path.join(workspace,'config','startup.blend'); const current=fs.existsSync(destination)?digest(fs.readFileSync(destination)):null; if(current!==(base.startupSha256??null))throw new Error('STARTUP_CHANGED_IN_ANOTHER_SESSION_RECONNECT'); const stat=fs.lstatSync(source);if(!stat.isFile()||stat.isSymbolicLink()||stat.size>256*1024*1024)throw new Error('STARTUP_PROFILE_INVALID'); const data=fs.readFileSync(source),temporary=path.join(profile,crypto.randomUUID()+'.tmp'); try{fs.writeFileSync(temporary,data,{mode:0o600,flag:'wx'});fs.renameSync(temporary,destination);}finally{fs.rmSync(temporary,{force:true});} fs.writeFileSync(baseFile,JSON.stringify({...base,startupSha256:digest(data)})); }finally{fs.rmdirSync(lock);} }