From 7445a573aba443819be0eaaa4298bdcd0714ba1e Mon Sep 17 00:00:00 2001 From: seb Date: Sun, 18 Oct 2026 15:45:03 +0200 Subject: [PATCH] u --- src/tools/vfs.js | 642 ++++++++++++++++++----------------------------- 1 file changed, 243 insertions(+), 399 deletions(-) diff --git a/src/tools/vfs.js b/src/tools/vfs.js index be7caa6..f8450b4 100644 --- a/src/tools/vfs.js +++ b/src/tools/vfs.js @@ -1,62 +1,75 @@ import fs from 'fs'; import path from 'path'; import { getStore } from '../db/store.js'; -import env from '../config/env.js'; import { getDefaultSkills } from './skill.js'; -import { getDefaultApplets, DEFAULT_APPLETS_VERSION, APPLET_EXT } from './applet.js'; +import { getDefaultApplets, DEFAULT_APPLETS_VERSION } from './applet.js'; + +// #region Constants & Configuration +// MARK: - Constants & Configuration const FULL_HASH_REGEX = /^[a-zA-Z0-9]{8}$/; const VIRTUAL_IMAGE_FOLDERS = new Set(['image', 'images']); +const MIME_TYPES = Object.freeze({ + '.html': 'text/html; charset=utf-8', + '.htm': 'text/html; charset=utf-8', + '.css': 'text/css; charset=utf-8', + '.js': 'text/javascript; charset=utf-8', + '.mjs': 'text/javascript; charset=utf-8', + '.json': 'application/json; charset=utf-8', + '.map': 'application/json; charset=utf-8', + '.avif': 'image/avif', + '.webp': 'image/webp', + '.png': 'image/png', + '.jpg': 'image/jpeg', + '.jpeg': 'image/jpeg', + '.gif': 'image/gif', + '.svg': 'image/svg+xml', + '.ico': 'image/x-icon', + '.bmp': 'image/bmp', + '.pdf': 'application/pdf', + '.txt': 'text/plain; charset=utf-8', + '.text': 'text/plain; charset=utf-8', + '.log': 'text/plain; charset=utf-8', + '.csv': 'text/csv; charset=utf-8', + '.tsv': 'text/tab-separated-values; charset=utf-8', + '.xlsx': 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', + '.xls': 'application/vnd.ms-excel', + '.xlsm': 'application/vnd.ms-excel.sheet.macroEnabled.12', + '.xltx': 'application/vnd.openxmlformats-officedocument.spreadsheetml.template', + '.md': 'text/markdown; charset=utf-8', + '.skill': 'text/markdown; charset=utf-8', + '.xml': 'application/xml; charset=utf-8', + '.mp3': 'audio/mpeg', + '.wav': 'audio/wav', + '.ogg': 'audio/ogg', + '.mp4': 'video/mp4', + '.webm': 'video/webm', + '.woff': 'font/woff', + '.woff2': 'font/woff2', + '.ttf': 'font/ttf', + '.otf': 'font/otf', +}); + +// #endregion + +// #region MIME & Path Helpers +// MARK: - MIME & Path Helpers + export function getMimeType(filePath) { const ext = path.extname(filePath).toLowerCase(); - switch (ext) { - case '.html': - case '.htm': return 'text/html; charset=utf-8'; - case '.css': return 'text/css; charset=utf-8'; - case '.js': - case '.mjs': return 'text/javascript; charset=utf-8'; - case '.json': - case '.map': return 'application/json; charset=utf-8'; - case '.avif': return 'image/avif'; - case '.webp': return 'image/webp'; - case '.png': return 'image/png'; - case '.jpg': - case '.jpeg': return 'image/jpeg'; - case '.gif': return 'image/gif'; - case '.svg': return 'image/svg+xml'; - case '.ico': return 'image/x-icon'; - case '.bmp': return 'image/bmp'; - case '.pdf': return 'application/pdf'; - case '.txt': - case '.text': - case '.log': return 'text/plain; charset=utf-8'; - case '.csv': return 'text/csv; charset=utf-8'; - case '.tsv': return 'text/tab-separated-values; charset=utf-8'; - case '.xlsx': return 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'; - case '.xls': return 'application/vnd.ms-excel'; - case '.xlsm': return 'application/vnd.ms-excel.sheet.macroEnabled.12'; - case '.xltx': return 'application/vnd.openxmlformats-officedocument.spreadsheetml.template'; - case '.md': - case '.skill': return 'text/markdown; charset=utf-8'; - case '.xml': return 'application/xml; charset=utf-8'; - case '.mp3': return 'audio/mpeg'; - case '.wav': return 'audio/wav'; - case '.ogg': return 'audio/ogg'; - case '.mp4': return 'video/mp4'; - case '.webm': return 'video/webm'; - case '.woff': return 'font/woff'; - case '.woff2': return 'font/woff2'; - case '.ttf': return 'font/ttf'; - case '.otf': return 'font/otf'; - default: return 'application/octet-stream'; - } + return MIME_TYPES[ext] || 'application/octet-stream'; +} + +function cleanVirtualPath(p, stripDotSlash = false) { + let clean = p.trim().replace(/^vfs:\/+/i, '').replace(/^([a-zA-Z]:|\/)+/, ''); + if (stripDotSlash) clean = clean.replace(/^\.\/+/, ''); + return clean; } export function extractFullHash(p) { if (typeof p !== 'string' || !p.trim()) return null; - const clean = p.trim().replace(/^vfs:\/+/i, '').replace(/^([a-zA-Z]:|\/)+/, ''); - const parts = clean.split('/').filter(Boolean); + const parts = cleanVirtualPath(p).split('/').filter(Boolean); if (parts.length === 0) return null; const first = parts[0].toLowerCase(); @@ -65,10 +78,8 @@ export function extractFullHash(p) { const withoutExt = last.replace(/\.[a-zA-Z0-9]+$/, ''); // Under virtual image folder (e.g. image/aB1cD2eF, images/aB1cD2eF.avif) - if (VIRTUAL_IMAGE_FOLDERS.has(first)) { - if (parts.length === 2 && FULL_HASH_REGEX.test(withoutExt)) { - return withoutExt; - } + if (VIRTUAL_IMAGE_FOLDERS.has(first) && parts.length === 2 && FULL_HASH_REGEX.test(withoutExt)) { + return withoutExt; } // Direct hash (e.g. "aB1cD2eF" or "aB1cD2eF.avif") @@ -81,33 +92,27 @@ export function extractFullHash(p) { export function isReadOnlyDatabasePath(filePath) { if (typeof filePath !== 'string' || !filePath.trim()) return false; - const clean = filePath.trim().replace(/^vfs:\/+/i, '').replace(/^([a-zA-Z]:|\/)+/, '').replace(/^\.\/+/, ''); + const clean = cleanVirtualPath(filePath, true); const lower = clean.toLowerCase(); - if (lower === 'image' || lower.startsWith('image/')) return true; - if (extractFullHash(clean)) return true; - return false; + return lower === 'image' || lower.startsWith('image/') || Boolean(extractFullHash(clean)); } function assertNotReadOnlyDatabasePath(filePath) { if (isReadOnlyDatabasePath(filePath)) { - throw new Error( - `Access denied: "${filePath}" matches the read-only database image pattern and cannot be written to. Save artifacts under custom filenames (e.g. "candidate.avif" or "images/candidate.avif").` + throw new Error( + `Access denied: "${filePath}" matches the read-only database image pattern and cannot be written to. Save artifacts under custom filenames (e.g. "candidate.avif" or "images/candidate.avif").` ); } } +// #endregion + +// #region VFS Root Resolution +// MARK: - VFS Root Resolution + /** * Resolve the on-disk VFS root for the user's CURRENTLY SELECTED company. * - * This must stay in lockstep with the store, which scopes every `modelvfs` call by - * `google_auth.selected_company` read at call time — see `resolveSelectedCompanyAccess` in - * libdashstore. Callers therefore must NOT pass a `companyId`: a value captured earlier (at - * tool-construction time, or from a request made before a company switch) can disagree with - * what the store will act on, which would hydrate one company's directory from another - * company's rows and delete files from a directory the database never touched. - * - * One authority, read fresh on every call, for both the disk and the database halves. - * * @param {number} googleAuthId * @returns {Promise} Absolute `data/vfs/` path * @throws {Error} 'No company selected' when the user has no company selected @@ -117,9 +122,7 @@ export async function resolveVfsRoot(googleAuthId) { } /** - * Shared implementation of {@link resolveVfsRoot}. Takes an already-resolved store so callers - * that need it anyway (e.g. `prepareVfs`, which hydrates from the same store) do not pay for a - * second lookup. + * Shared implementation of {@link resolveVfsRoot}. * * @param {object} store * @param {number} googleAuthId @@ -134,125 +137,87 @@ async function vfsRootFor(store, googleAuthId) { return path.resolve(process.cwd(), 'data/vfs', String(companyId)); } +// #endregion + +// #region Default Skills & Applets Seeding +// MARK: - Default Skills & Applets Seeding + /** - * Loads default SKILL files into the selected company's on-disk VFS. - * - * @param {{ googleAuthId: number, store?: object, vfsRoot?: string }} ctx - * @returns {Promise>} List of loaded skill file names + * Internal helper to write or overwrite default files into the company's VFS. */ -export async function loadDefaultSkills({ googleAuthId, store: passedStore, vfsRoot: passedVfsRoot } = {}) { +async function seedVfsFiles({ googleAuthId, store: passedStore, vfsRoot: passedVfsRoot }, items, { overwrite, label }) { const store = passedStore || await getStore(); const vfsRoot = passedVfsRoot || await vfsRootFor(store, googleAuthId); fs.mkdirSync(vfsRoot, { recursive: true }); - const defaultSkills = getDefaultSkills(); - const loaded = []; - - for (const skill of defaultSkills) { + const written = []; + for (const { name, content } of items) { try { - const diskPath = path.resolve(vfsRoot, skill.name); - if (!fs.existsSync(diskPath)) { + const diskPath = path.resolve(vfsRoot, name); + if (overwrite || !fs.existsSync(diskPath)) { fs.mkdirSync(path.dirname(diskPath), { recursive: true }); - fs.writeFileSync(diskPath, skill.content, 'utf8'); - loaded.push(skill.name); + fs.writeFileSync(diskPath, content, 'utf8'); + written.push(name); } } catch (err) { - console.error(`[vfs] failed to write default skill "${skill.name}" to disk:`, err.message); + const action = overwrite ? `restore default ${label}` : `write default ${label}`; + const suffix = overwrite ? '' : ' to disk'; + console.error(`[vfs] failed to ${action} "${name}"${suffix}:`, err.message); } } + return written; +} - return loaded; +function getVersionedApplets() { + return getDefaultApplets().map((a) => ({ + name: `${DEFAULT_APPLETS_VERSION}-${a.name}`, + content: a.content, + })); +} + +/** + * Loads default SKILL files into the selected company's on-disk VFS. + * @param {{ googleAuthId: number, store?: object, vfsRoot?: string }} ctx + * @returns {Promise>} + */ +export async function loadDefaultSkills(ctx = {}) { + return seedVfsFiles(ctx, getDefaultSkills(), { overwrite: false, label: 'skill' }); } /** * Restores all shipped system default SKILL files into the company's VFS. - * * @param {{ googleAuthId: number, store?: object, vfsRoot?: string }} ctx - * @returns {Promise>} List of restored skill file names + * @returns {Promise>} */ -export async function restoreDefaultSkills({ googleAuthId, store: passedStore, vfsRoot: passedVfsRoot } = {}) { - const store = passedStore || await getStore(); - const vfsRoot = passedVfsRoot || await vfsRootFor(store, googleAuthId); - fs.mkdirSync(vfsRoot, { recursive: true }); - - const defaultSkills = getDefaultSkills(); - const restored = []; - - for (const skill of defaultSkills) { - try { - const diskPath = path.resolve(vfsRoot, skill.name); - fs.mkdirSync(path.dirname(diskPath), { recursive: true }); - fs.writeFileSync(diskPath, skill.content, 'utf8'); - restored.push(skill.name); - } catch (err) { - console.error(`[vfs] failed to restore default skill "${skill.name}":`, err.message); - } - } - - return restored; +export async function restoreDefaultSkills(ctx = {}) { + return seedVfsFiles(ctx, getDefaultSkills(), { overwrite: true, label: 'skill' }); } /** * Loads the shipped example `.applet.js` files into the selected company's VFS. - * - * Version-gated: each example is written under a `v1-` prefixed name and only when that exact - * file is absent, so an operator's edits survive and a new example revision ships as a new file. - * Must stay in step with {@link loadDefaultSkills}, which runs alongside it. - * * @param {{ googleAuthId: number, store?: object, vfsRoot?: string }} ctx - * @returns {Promise>} List of seeded applet file names + * @returns {Promise>} */ -export async function loadDefaultApplets({ googleAuthId, store: passedStore, vfsRoot: passedVfsRoot } = {}) { - const store = passedStore || await getStore(); - const vfsRoot = passedVfsRoot || await vfsRootFor(store, googleAuthId); - fs.mkdirSync(vfsRoot, { recursive: true }); - - const seeded = []; - for (const applet of getDefaultApplets()) { - const name = `${DEFAULT_APPLETS_VERSION}-${applet.name}`; - try { - const diskPath = path.resolve(vfsRoot, name); - if (!fs.existsSync(diskPath)) { - fs.writeFileSync(diskPath, applet.content, 'utf8'); - seeded.push(name); - } - } catch (err) { - console.error(`[vfs] failed to write default applet "${name}" to disk:`, err.message); - } - } - - return seeded; +export async function loadDefaultApplets(ctx = {}) { + return seedVfsFiles(ctx, getVersionedApplets(), { overwrite: false, label: 'applet' }); } /** * Restores all shipped system default example applets into the company's VFS. - * * @param {{ googleAuthId: number, store?: object, vfsRoot?: string }} ctx - * @returns {Promise>} List of restored applet file names + * @returns {Promise>} */ -export async function restoreDefaultApplets({ googleAuthId, store: passedStore, vfsRoot: passedVfsRoot } = {}) { - const store = passedStore || await getStore(); - const vfsRoot = passedVfsRoot || await vfsRootFor(store, googleAuthId); - fs.mkdirSync(vfsRoot, { recursive: true }); - - const restored = []; - for (const applet of getDefaultApplets()) { - const name = `${DEFAULT_APPLETS_VERSION}-${applet.name}`; - try { - const diskPath = path.resolve(vfsRoot, name); - fs.writeFileSync(diskPath, applet.content, 'utf8'); - restored.push(name); - } catch (err) { - console.error(`[vfs] failed to restore default applet "${name}":`, err.message); - } - } - - return restored; +export async function restoreDefaultApplets(ctx = {}) { + return seedVfsFiles(ctx, getVersionedApplets(), { overwrite: true, label: 'applet' }); } +// #endregion + +// #region Lifecycle & Initialization +// MARK: - Lifecycle & Initialization + /** * Initializes the VFS for the selected company with default SKILL files and example applets. - * * @param {{ googleAuthId: number, store?: object, vfsRoot?: string }} ctx * @returns {Promise} vfsRoot directory path */ @@ -269,7 +234,6 @@ export async function initCompanyVfs({ googleAuthId, store: passedStore, vfsRoot /** * Ensure the local directory for this company's VFS exists and the default skills and example * applets are loaded. - * * @param {{ googleAuthId: number }} ctx * @returns {Promise} vfsRoot directory path */ @@ -295,13 +259,42 @@ export async function prepareVfs({ googleAuthId }) { /** * Purely disk-based VFS; no database syncing needed. Kept as no-op for backward compatibility. - * * @param {{ googleAuthId?: number, vfsRoot?: string }} [ctx] */ -export async function syncVfsToDb({ googleAuthId, vfsRoot } = {}) { +export async function syncVfsToDb() { // Purely disk-based VFS; no database syncing. } +// #endregion + +// #region Sandboxed VFS Factory +// MARK: - Sandboxed VFS Factory + +/** + * Wraps a synchronous function into a Node-style callback function (`process.nextTick`). + * Automatically handles optional middle arguments (e.g. `(p, opt, cb)` vs `(p, cb)`). + */ +function cbWrap(syncFn, arityWithOpt = 3) { + return (...args) => { + let cb; + let callArgs = args; + if (typeof args[args.length - 1] === 'function') { + cb = args[args.length - 1]; + callArgs = args.slice(0, -1); + while (callArgs.length < arityWithOpt - 1) { + callArgs.push(undefined); + } + } + try { + const res = syncFn(...callArgs); + if (cb) process.nextTick(cb, null, res); + } catch (err) { + if (cb) process.nextTick(cb, err); + else throw err; + } + }; +} + /** * Creates a sandboxed VFS module object mirroring Node's `fs` and `fs/promises`. * @param {string} vfsRoot Absolute root, as returned by `prepareVfs`/`resolveVfsRoot` @@ -310,8 +303,9 @@ export async function syncVfsToDb({ googleAuthId, vfsRoot } = {}) { export function createVfs(vfsRoot, options = {}) { const { googleAuthId } = options; -// Node fs errors embed the REAL absolute on-disk path, which leaks the server's directory - // layout into the model-visible transcript. Rewrite them to use a stable `vfs://` prefix. + // #region Internal Sandbox Helpers + // MARK: - Internal Sandbox Helpers + const sanitizeFsError = (err, virtualPath) => { if (!err || typeof err.message !== 'string') return err; const virtual = typeof virtualPath === 'string' ? virtualPath : String(virtualPath); @@ -322,7 +316,7 @@ export function createVfs(vfsRoot, options = {}) { .join('/') .replace(/vfs:\/\/\/+/g, 'vfs:///'); return Object.assign(Object.create(Object.getPrototypeOf(err)), err, { - message, + message, path: virtual, }); }; @@ -331,11 +325,10 @@ export function createVfs(vfsRoot, options = {}) { if (typeof p !== 'string' || !p.trim()) { throw new Error('Path must be a non-empty string'); } - const cleanVirtual = p.trim().replace(/^vfs:\/+/i, '').replace(/^([a-zA-Z]:|\/)+/, ''); - const resolved = path.resolve(vfsRoot, cleanVirtual); + const resolved = path.resolve(vfsRoot, cleanVirtualPath(p)); const relative = path.relative(vfsRoot, resolved); if (relative.startsWith('..') || path.isAbsolute(relative)) { - throw new Error(`Access denied: path "${p}" escapes virtual filesystem boundary.`); + throw new Error(`Access denied: path "${p}" escapes virtual filesystem boundary.`); } return resolved; }; @@ -343,24 +336,47 @@ export function createVfs(vfsRoot, options = {}) { const syncFetchImage = (hash) => { if (!hash) return null; try { - // Read from storage or cache if available const cachePath = path.join(vfsRoot, 'images', `${hash}.avif`); - if (fs.existsSync(cachePath)) return fs.readFileSync(cachePath); - return null; + return fs.existsSync(cachePath) ? fs.readFileSync(cachePath) : null; } catch { return null; } }; + const formatBuffer = (buf, opt) => { + const encoding = typeof opt === 'string' ? opt : opt?.encoding; + return encoding ? buf.toString(encoding) : buf; + }; + + const writeCopyBuffer = (buf, src, dest, mode) => { + const destDisk = safeResolve(dest); + fs.mkdirSync(path.dirname(destDisk), { recursive: true }); + if (typeof mode === 'number' && (mode & fs.constants.COPYFILE_EXCL) && fs.existsSync(destDisk)) { + const err = new Error(`EEXIST: file already exists, copyfile '${src}' -> '${dest}'`); + err.code = 'EEXIST'; + err.path = dest; + err.syscall = 'copyfile'; + throw err; + } + try { + return fs.writeFileSync(destDisk, buf); + } catch (err) { + throw sanitizeFsError(err, dest); + } + }; + + // #endregion + + // #region VFS Core Operations + // MARK: - VFS Core Operations + const vfs = { + // --- Read Operations --- readFileSync: (p, opt) => { const hash = extractFullHash(p); if (hash) { const buf = syncFetchImage(hash); - if (buf) { - const encoding = typeof opt === 'string' ? opt : opt?.encoding; - return encoding ? buf.toString(encoding) : buf; - } + if (buf) return formatBuffer(buf, opt); } const diskPath = safeResolve(p); try { @@ -372,8 +388,8 @@ export function createVfs(vfsRoot, options = {}) { readFile: (p, opt, cb) => { const callback = typeof opt === 'function' ? opt : cb; - const options = typeof opt === 'function' ? undefined : opt; - vfs.promises.readFile(p, options) + const opts = typeof opt === 'function' ? undefined : opt; + vfs.promises.readFile(p, opts) .then((data) => { if (typeof callback === 'function') process.nextTick(callback, null, data); }) @@ -395,6 +411,7 @@ export function createVfs(vfsRoot, options = {}) { return fs.createReadStream(diskPath, opt); }, + // --- Write & Append Operations --- writeFileSync: (p, data, opt) => { assertNotReadOnlyDatabasePath(p); const diskPath = safeResolve(p); @@ -406,18 +423,6 @@ export function createVfs(vfsRoot, options = {}) { } }, - writeFile: (p, data, opt, cb) => { - const callback = typeof opt === 'function' ? opt : cb; - const options = typeof opt === 'function' ? undefined : opt; - try { - vfs.writeFileSync(p, data, options); - if (typeof callback === 'function') process.nextTick(callback, null); - } catch (err) { - if (typeof callback === 'function') process.nextTick(callback, err); - else throw err; - } - }, - createWriteStream: (p, opt) => { assertNotReadOnlyDatabasePath(p); const diskPath = safeResolve(p); @@ -436,39 +441,13 @@ export function createVfs(vfsRoot, options = {}) { } }, - appendFile: (p, data, opt, cb) => { - const callback = typeof opt === 'function' ? opt : cb; - const options = typeof opt === 'function' ? undefined : opt; - try { - vfs.appendFileSync(p, data, options); - if (typeof callback === 'function') process.nextTick(callback, null); - } catch (err) { - if (typeof callback === 'function') process.nextTick(callback, err); - else throw err; - } - }, - + // --- Copy, Rename & Truncate Operations --- copyFileSync: (src, dest, mode) => { assertNotReadOnlyDatabasePath(dest); const hash = extractFullHash(src); if (hash) { const buf = syncFetchImage(hash); - if (buf) { - const destDisk = safeResolve(dest); - fs.mkdirSync(path.dirname(destDisk), { recursive: true }); - if (typeof mode === 'number' && (mode & fs.constants.COPYFILE_EXCL) && fs.existsSync(destDisk)) { - const err = new Error(`EEXIST: file already exists, copyfile '${src}' -> '${dest}'`); - err.code = 'EEXIST'; - err.path = dest; - err.syscall = 'copyfile'; - throw err; - } - try { - return fs.writeFileSync(destDisk, buf); - } catch (err) { - throw sanitizeFsError(err, dest); - } - } + if (buf) return writeCopyBuffer(buf, src, dest, mode); } const srcDisk = safeResolve(src); const destDisk = safeResolve(dest); @@ -480,18 +459,6 @@ export function createVfs(vfsRoot, options = {}) { } }, - copyFile: (src, dest, mode, cb) => { - const callback = typeof mode === 'function' ? mode : cb; - const m = typeof mode === 'function' ? undefined : mode; - try { - vfs.copyFileSync(src, dest, m); - if (typeof callback === 'function') process.nextTick(callback, null); - } catch (err) { - if (typeof callback === 'function') process.nextTick(callback, err); - else throw err; - } - }, - cpSync: (src, dest, opt) => { assertNotReadOnlyDatabasePath(dest); const srcDisk = safeResolve(src); @@ -504,18 +471,6 @@ export function createVfs(vfsRoot, options = {}) { } }, - cp: (src, dest, opt, cb) => { - const callback = typeof opt === 'function' ? opt : cb; - const options = typeof opt === 'function' ? undefined : opt; - try { - vfs.cpSync(src, dest, options); - if (typeof callback === 'function') process.nextTick(callback, null); - } catch (err) { - if (typeof callback === 'function') process.nextTick(callback, err); - else throw err; - } - }, - renameSync: (oldPath, newPath) => { assertNotReadOnlyDatabasePath(oldPath); assertNotReadOnlyDatabasePath(newPath); @@ -529,16 +484,6 @@ export function createVfs(vfsRoot, options = {}) { } }, - rename: (oldPath, newPath, cb) => { - try { - vfs.renameSync(oldPath, newPath); - if (typeof cb === 'function') process.nextTick(cb, null); - } catch (err) { - if (typeof cb === 'function') process.nextTick(cb, err); - else throw err; - } - }, - truncateSync: (p, len) => { assertNotReadOnlyDatabasePath(p); const diskPath = safeResolve(p); @@ -549,32 +494,18 @@ export function createVfs(vfsRoot, options = {}) { } }, - truncate: (p, len, cb) => { - const callback = typeof len === 'function' ? len : cb; - const l = typeof len === 'function' ? undefined : len; - try { - vfs.truncateSync(p, l); - if (typeof callback === 'function') process.nextTick(callback, null); - } catch (err) { - if (typeof callback === 'function') process.nextTick(callback, err); - else throw err; - } - }, - + // --- Metadata & Access Operations --- accessSync: (p, mode) => { const hash = extractFullHash(p); - if (hash) { - const buf = syncFetchImage(hash); - if (buf) { - if (typeof mode === 'number' && (mode & fs.constants.W_OK)) { - const err = new Error(`EACCES: permission denied, access '${p}'`); - err.code = 'EACCES'; - err.path = p; - err.syscall = 'access'; - throw err; - } - return; + if (hash && syncFetchImage(hash)) { + if (typeof mode === 'number' && (mode & fs.constants.W_OK)) { + const err = new Error(`EACCES: permission denied, access '${p}'`); + err.code = 'EACCES'; + err.path = p; + err.syscall = 'access'; + throw err; } + return; } const diskPath = safeResolve(p); try { @@ -584,18 +515,6 @@ export function createVfs(vfsRoot, options = {}) { } }, - access: (p, mode, cb) => { - const callback = typeof mode === 'function' ? mode : cb; - const m = typeof mode === 'function' ? undefined : mode; - try { - vfs.accessSync(p, m); - if (typeof callback === 'function') process.nextTick(callback, null); - } catch (err) { - if (typeof callback === 'function') process.nextTick(callback, err); - else throw err; - } - }, - realpathSync: (p, opt) => { const diskPath = safeResolve(p); try { @@ -610,27 +529,11 @@ export function createVfs(vfsRoot, options = {}) { } }, - realpath: (p, opt, cb) => { - const callback = typeof opt === 'function' ? opt : cb; - const options = typeof opt === 'function' ? undefined : opt; - try { - const res = vfs.realpathSync(p, options); - if (typeof callback === 'function') process.nextTick(callback, null, res); - } catch (err) { - if (typeof callback === 'function') process.nextTick(callback, err); - else throw err; - } - }, - existsSync: (p) => { const hash = extractFullHash(p); - if (hash) { - const buf = syncFetchImage(hash); - if (buf) return true; - } + if (hash && syncFetchImage(hash)) return true; try { - const diskPath = safeResolve(p); - return fs.existsSync(diskPath); + return fs.existsSync(safeResolve(p)); } catch { return false; } @@ -665,32 +568,9 @@ export function createVfs(vfsRoot, options = {}) { } }, - stat: (p, opt, cb) => { - const callback = typeof opt === 'function' ? opt : cb; - const options = typeof opt === 'function' ? undefined : opt; - try { - const res = vfs.statSync(p, options); - if (typeof callback === 'function') process.nextTick(callback, null, res); - } catch (err) { - if (typeof callback === 'function') process.nextTick(callback, err); - else throw err; - } - }, - lstatSync: (p, opt) => vfs.statSync(p, opt), - lstat: (p, opt, cb) => { - const callback = typeof opt === 'function' ? opt : cb; - const options = typeof opt === 'function' ? undefined : opt; - try { - const res = vfs.lstatSync(p, options); - if (typeof callback === 'function') process.nextTick(callback, null, res); - } catch (err) { - if (typeof callback === 'function') process.nextTick(callback, err); - else throw err; - } - }, - + // --- Directory & Removal Operations --- readdirSync: (p = '.', opt) => { const clean = typeof p === 'string' ? p.trim().replace(/^([a-zA-Z]:|\/)+/, '') : ''; if (VIRTUAL_IMAGE_FOLDERS.has(clean.toLowerCase())) { @@ -706,10 +586,10 @@ export function createVfs(vfsRoot, options = {}) { readdir: (p = '.', opt, cb) => { const callback = typeof opt === 'function' ? opt : (typeof p === 'function' ? p : cb); - const options = typeof opt === 'function' ? undefined : opt; + const opts = typeof opt === 'function' ? undefined : opt; const pathArg = typeof p === 'function' ? '.' : p; try { - const res = vfs.readdirSync(pathArg, options); + const res = vfs.readdirSync(pathArg, opts); if (typeof callback === 'function') process.nextTick(callback, null, res); } catch (err) { if (typeof callback === 'function') process.nextTick(callback, err); @@ -727,18 +607,6 @@ export function createVfs(vfsRoot, options = {}) { } }, - mkdir: (p, opt, cb) => { - const callback = typeof opt === 'function' ? opt : cb; - const options = typeof opt === 'function' ? undefined : opt; - try { - const res = vfs.mkdirSync(p, options); - if (typeof callback === 'function') process.nextTick(callback, null, res); - } catch (err) { - if (typeof callback === 'function') process.nextTick(callback, err); - else throw err; - } - }, - unlinkSync: (p) => { assertNotReadOnlyDatabasePath(p); const diskPath = safeResolve(p); @@ -749,16 +617,6 @@ export function createVfs(vfsRoot, options = {}) { } }, - unlink: (p, cb) => { - try { - vfs.unlinkSync(p); - if (typeof cb === 'function') process.nextTick(cb, null); - } catch (err) { - if (typeof cb === 'function') process.nextTick(cb, err); - else throw err; - } - }, - rmdirSync: (p, opt) => { assertNotReadOnlyDatabasePath(p); const diskPath = safeResolve(p); @@ -769,18 +627,6 @@ export function createVfs(vfsRoot, options = {}) { } }, - rmdir: (p, opt, cb) => { - const callback = typeof opt === 'function' ? opt : cb; - const options = typeof opt === 'function' ? undefined : opt; - try { - vfs.rmdirSync(p, options); - if (typeof callback === 'function') process.nextTick(callback, null); - } catch (err) { - if (typeof callback === 'function') process.nextTick(callback, err); - else throw err; - } - }, - rmSync: (p, opt) => { assertNotReadOnlyDatabasePath(p); const diskPath = safeResolve(p); @@ -791,27 +637,17 @@ export function createVfs(vfsRoot, options = {}) { } }, - rm: (p, opt, cb) => { - const callback = typeof opt === 'function' ? opt : cb; - const options = typeof opt === 'function' ? undefined : opt; - try { - vfs.rmSync(p, options); - if (typeof callback === 'function') process.nextTick(callback, null); - } catch (err) { - if (typeof callback === 'function') process.nextTick(callback, err); - else throw err; - } - }, + // #endregion + + // #region VFS Promises API + // MARK: - VFS Promises API promises: { readFile: async (p, opt) => { const hash = extractFullHash(p); if (hash) { const buf = syncFetchImage(hash); - if (buf) { - const encoding = typeof opt === 'string' ? opt : opt?.encoding; - return encoding ? buf.toString(encoding) : buf; - } + if (buf) return formatBuffer(buf, opt); if (googleAuthId) { try { const store = await getStore(); @@ -822,8 +658,7 @@ export function createVfs(vfsRoot, options = {}) { fs.mkdirSync(path.join(vfsRoot, 'images'), { recursive: true }); fs.writeFileSync(cachePath, img.buffer); } catch { /* ignore cache write error */ } - const encoding = typeof opt === 'string' ? opt : opt?.encoding; - return encoding ? img.buffer.toString(encoding) : img.buffer; + return formatBuffer(img.buffer, opt); } } catch { // fall through @@ -839,20 +674,7 @@ export function createVfs(vfsRoot, options = {}) { const hash = extractFullHash(src); if (hash) { const buf = await vfs.promises.readFile(src); - const destDisk = safeResolve(dest); - fs.mkdirSync(path.dirname(destDisk), { recursive: true }); - if (typeof mode === 'number' && (mode & fs.constants.COPYFILE_EXCL) && fs.existsSync(destDisk)) { - const err = new Error(`EEXIST: file already exists, copyfile '${src}' -> '${dest}'`); - err.code = 'EEXIST'; - err.path = dest; - err.syscall = 'copyfile'; - throw err; - } - try { - return fs.writeFileSync(destDisk, buf); - } catch (err) { - throw sanitizeFsError(err, dest); - } + return writeCopyBuffer(buf, src, dest, mode); } return vfs.copyFileSync(src, dest, mode); }, @@ -871,16 +693,15 @@ export function createVfs(vfsRoot, options = {}) { constants: fs.constants, }, + // #endregion + + // #region External Sandbox Utilities + // MARK: - External Sandbox Utilities + /** * Resolve a virtual path to an absolute on-disk path for writing, applying the SAME - * containment + read-only guards as `writeFileSync`. Exposed so non-`fs` writers that - * bypass the VFS (e.g. `sharp(...).toFile()`) can be funnelled back inside the sandbox - * boundary instead of writing wherever the process CWD happens to be. - * - * Deliberately does NOT create parent directories: callers such as `sharp().toFile()` - * must behave like `fs.writeFileSync`, which errors with ENOENT when the parent is - * missing. Auto-creating the whole tree turns a typo'd path into silent success. - */ + * containment + read-only guards as `writeFileSync`. + */ resolveWritePath: (p) => { assertNotReadOnlyDatabasePath(p); return safeResolve(p); @@ -889,20 +710,43 @@ export function createVfs(vfsRoot, options = {}) { /** * Resolve a virtual path to an absolute on-disk path for reading, applying containment checks. */ - resolveReadPath: (p) => { - return safeResolve(p); - }, + resolveReadPath: (p) => safeResolve(p), /** * Sanitize on-disk error paths back to virtual paths for display in sandbox transcripts. */ sanitizeError: (err, virtualPath) => sanitizeFsError(err, virtualPath), + + constants: fs.constants, + + // #endregion }; + // #region Callback Wrapper Bindings + // MARK: - Callback Wrapper Bindings + + vfs.writeFile = cbWrap(vfs.writeFileSync, 4); + vfs.appendFile = cbWrap(vfs.appendFileSync, 4); + vfs.copyFile = cbWrap(vfs.copyFileSync, 4); + vfs.cp = cbWrap(vfs.cpSync, 4); + vfs.rename = cbWrap(vfs.renameSync, 3); + vfs.truncate = cbWrap(vfs.truncateSync, 3); + vfs.access = cbWrap(vfs.accessSync, 3); + vfs.realpath = cbWrap(vfs.realpathSync, 3); + vfs.stat = cbWrap(vfs.statSync, 3); + vfs.lstat = cbWrap(vfs.lstatSync, 3); + vfs.mkdir = cbWrap(vfs.mkdirSync, 3); + vfs.rmdir = cbWrap(vfs.rmdirSync, 3); + vfs.unlink = cbWrap(vfs.unlinkSync, 2); + vfs.rm = cbWrap(vfs.rmSync, 3); + vfs.realpathSync.native = (p, opt) => vfs.realpathSync(p, opt); vfs.realpath.native = (p, opt, cb) => vfs.realpath(p, opt, cb); vfs.promises.realpath.native = async (p, opt) => vfs.realpathSync(p, opt); - vfs.constants = fs.constants; + + // #endregion return vfs; } + +// #endregion \ No newline at end of file