From a98afa46f5c0958ad4e49ae77c52bebfe6e14eee Mon Sep 17 00:00:00 2001 From: Nystik <236107-Nystik@users.noreply.gitlab.com> Date: Mon, 23 Mar 2026 22:58:01 +0100 Subject: [PATCH] shim more buffer methods, and fs methods getting importer plugin to work, --- package-lock.json | 11 +- package.json | 1 + src/shims/electron/remote/dialog.js | 152 ++++++++++++++++- src/shims/fs/fd.js | 153 ++++++++++++++++++ src/shims/fs/index.js | 11 ++ src/shims/fs/promises.js | 43 +++++ src/shims/globals.js | 26 +++ src/shims/node/zlib.js | 138 ++++++++++++++++ src/shims/require.js | 2 + src/ui/components/layout/MessageDialog.svelte | 2 +- 10 files changed, 533 insertions(+), 6 deletions(-) create mode 100644 src/shims/fs/fd.js create mode 100644 src/shims/node/zlib.js diff --git a/package-lock.json b/package-lock.json index 34a010e..2810674 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,17 +1,18 @@ { "name": "ignis", - "version": "0.4.0", + "version": "0.5.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "ignis", - "version": "0.4.0", + "version": "0.5.0", "dependencies": { "chokidar": "^3.6.0", "compression": "^1.7.4", "cors": "^2.8.5", "express": "^4.21.0", + "pako": "^2.1.0", "ws": "^8.16.0" }, "devDependencies": { @@ -1406,6 +1407,12 @@ "node": ">= 0.8" } }, + "node_modules/pako": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/pako/-/pako-2.1.0.tgz", + "integrity": "sha512-w+eufiZ1WuJYgPXbV/PO3NCMEc3xqylkKHzp8bxp1uW4qaSNQUkwmLLEc3kKsfz8lpV1F8Ht3U1Cm+9Srog2ug==", + "license": "(MIT AND Zlib)" + }, "node_modules/parseurl": { "version": "1.3.3", "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", diff --git a/package.json b/package.json index 7184e70..afbe05f 100644 --- a/package.json +++ b/package.json @@ -13,6 +13,7 @@ "compression": "^1.7.4", "cors": "^2.8.5", "express": "^4.21.0", + "pako": "^2.1.0", "ws": "^8.16.0" }, "devDependencies": { diff --git a/src/shims/electron/remote/dialog.js b/src/shims/electron/remote/dialog.js index aae3013..38323e7 100644 --- a/src/shims/electron/remote/dialog.js +++ b/src/shims/electron/remote/dialog.js @@ -3,12 +3,158 @@ import { showConfirmDialog, showPromptDialog, } from "../../../ui/bootstrap.js"; +import { transport } from "../../fs/transport.js"; + +const IMPORTS_DIR = ".obsidian/imports"; + +let stagedFiles = []; + +function buildAcceptString(filters) { + if (!filters || filters.length === 0) { + return ""; + } + + const extensions = filters.flatMap((f) => f.extensions || []); + + if (extensions.includes("*")) { + return ""; + } + + return extensions.map((ext) => "." + ext).join(","); +} + +function pickFiles(accept, multiple) { + return new Promise((resolve) => { + const input = document.createElement("input"); + input.type = "file"; + input.multiple = multiple; + input.style.display = "none"; + + if (accept) { + input.accept = accept; + } + + input.addEventListener("change", () => { + const files = Array.from(input.files || []); + input.remove(); + resolve(files); + }); + + // User closed the picker without selecting + input.addEventListener("cancel", () => { + input.remove(); + resolve([]); + }); + + document.body.appendChild(input); + input.click(); + }); +} + +async function uploadToImports(file) { + const arrayBuffer = await file.arrayBuffer(); + const bytes = new Uint8Array(arrayBuffer); + const targetPath = IMPORTS_DIR + "/" + file.name; + + await transport.writeFile(targetPath, bytes); + + return "/" + targetPath; +} + +async function startWorkaroundFlow(options) { + const properties = options?.properties || []; + const multiple = properties.includes("multiSelections"); + const accept = buildAcceptString(options?.filters); + + const files = await pickFiles(accept, multiple); + + if (files.length === 0) { + return; + } + + const paths = []; + + for (const file of files) { + const vaultPath = await uploadToImports(file); + paths.push(vaultPath); + } + + stagedFiles = paths; + + const names = paths.map((p) => p.split("/").pop()).join(", "); + + console.log("[shim:dialog] Files staged for next sync call:", paths); + + await showMessageDialog( + "Files Ready", + `Uploaded: ${names}\n\nPlease retry the action that brought you here. ` + + "The files will be provided automatically.", + ); +} export const dialogShim = { async showOpenDialog(browserWindow, options) { - // TODO: implement custom modal with server-side file listing - console.log("[shim:dialog] showOpenDialog (stub):", options); - return { canceled: true, filePaths: [] }; + if (typeof browserWindow === "object" && !options) { + options = browserWindow; + } + + const properties = options?.properties || []; + const multiple = properties.includes("multiSelections"); + const accept = buildAcceptString(options?.filters); + + console.log("[shim:dialog] showOpenDialog - opening browser file picker"); + + const files = await pickFiles(accept, multiple); + + if (files.length === 0) { + return { canceled: true, filePaths: [] }; + } + + const filePaths = []; + + for (const file of files) { + const vaultPath = await uploadToImports(file); + filePaths.push(vaultPath); + } + + console.log("[shim:dialog] showOpenDialog - uploaded:", filePaths); + return { canceled: false, filePaths }; + }, + + showOpenDialogSync(browserWindow, options) { + if (typeof browserWindow === "object" && !options) { + options = browserWindow; + } + + // If files were staged from a previous workaround, return them immediately + if (stagedFiles.length > 0) { + const paths = stagedFiles; + stagedFiles = []; + console.log( + "[shim:dialog] showOpenDialogSync - returning staged files:", + paths, + ); + return paths; + } + + console.warn( + "[shim:dialog] showOpenDialogSync requires workaround in browser context", + ); + + // Fire-and-forget: show warning, then optionally start workaround flow + showConfirmDialog( + "Feature Not Available", + "This action requires a native file picker which is not available in the browser.", + "A workaround is available: upload your file first, then retry the action. " + + "Would you like to proceed?", + "Upload File", + ).then((confirmed) => { + if (confirmed) { + startWorkaroundFlow(options); + } + }); + + return undefined; }, async showSaveDialog(browserWindow, options) { diff --git a/src/shims/fs/fd.js b/src/shims/fs/fd.js new file mode 100644 index 0000000..56f53ca --- /dev/null +++ b/src/shims/fs/fd.js @@ -0,0 +1,153 @@ +// File descriptor shim - maps fake integer fds to in-memory file buffers. +// Enables libraries like yauzl that use fs.open/fs.read/fs.close to seek +// around files without loading them via readFileSync upfront. + +let nextFd = 100; +const openFiles = new Map(); + +export function createFdOps(metadataCache, contentCache, transport) { + function ensureData(path) { + const cached = contentCache.get(path); + + if (cached !== null) { + if (typeof cached === "string") { + return new TextEncoder().encode(cached); + } + + return cached; + } + + // Synchronous fetch fallback + console.warn("[shim:fs] fd open cache miss, using sync XHR:", path); + const data = transport.readFileSync(path); + contentCache.set(path, data); + + return data; + } + + function getEntry(fd) { + const entry = openFiles.get(fd); + + if (!entry) { + const err = new Error(`EBADF: bad file descriptor, fd ${fd}`); + err.code = "EBADF"; + throw err; + } + + return entry; + } + + // --- Sync --- + + function openSync(path, flags, mode) { + if (!metadataCache.has(path)) { + const err = new Error( + `ENOENT: no such file or directory, open '${path}'`, + ); + err.code = "ENOENT"; + throw err; + } + + const data = ensureData(path); + const fd = nextFd++; + openFiles.set(fd, { path, data }); + + return fd; + } + + function readSync(fd, buffer, offset, length, position) { + const entry = getEntry(fd); + const available = Math.min(length, entry.data.length - position); + + if (available <= 0) { + return 0; + } + + const slice = entry.data.subarray(position, position + available); + buffer.set(slice, offset); + + return available; + } + + function closeSync(fd) { + openFiles.delete(fd); + } + + function fstatSync(fd) { + const entry = getEntry(fd); + const stat = metadataCache.toStat(entry.path); + + if (stat) { + return stat; + } + + // Fallback: construct minimal stat from the buffer + return { + size: entry.data.length, + isFile: () => true, + isDirectory: () => false, + }; + } + + // --- Async (callback style) --- + + function open(path, flags, modeOrCb, cb) { + if (typeof modeOrCb === "function") { + cb = modeOrCb; + } + + try { + const fd = openSync(path, flags); + queueMicrotask(() => cb(null, fd)); + } catch (e) { + queueMicrotask(() => cb(e)); + } + } + + function read(fd, buffer, offset, length, position, cb) { + try { + const bytesRead = readSync(fd, buffer, offset, length, position); + queueMicrotask(() => cb(null, bytesRead, buffer)); + } catch (e) { + queueMicrotask(() => cb(e)); + } + } + + function close(fd, cb) { + try { + closeSync(fd); + + if (cb) { + queueMicrotask(() => cb(null)); + } + } catch (e) { + if (cb) { + queueMicrotask(() => cb(e)); + } + } + } + + function fstat(fd, optionsOrCb, cb) { + if (typeof optionsOrCb === "function") { + cb = optionsOrCb; + } + + try { + const stat = fstatSync(fd); + queueMicrotask(() => cb(null, stat)); + } catch (e) { + queueMicrotask(() => cb(e)); + } + } + + return { + openSync, + readSync, + closeSync, + fstatSync, + open, + read, + close, + fstat, + }; +} diff --git a/src/shims/fs/index.js b/src/shims/fs/index.js index eb51cc4..e4aa8ad 100644 --- a/src/shims/fs/index.js +++ b/src/shims/fs/index.js @@ -4,6 +4,7 @@ import { transport } from "./transport.js"; import { createFsPromises } from "./promises.js"; import { createFsSync } from "./sync.js"; import { createFsWatch } from "./watch.js"; +import { createFdOps } from "./fd.js"; import { constants } from "./constants.js"; const metadataCache = new MetadataCache(); @@ -12,6 +13,7 @@ const contentCache = new ContentCache(); const fsPromises = createFsPromises(metadataCache, contentCache, transport); const fsSync = createFsSync(metadataCache, contentCache, transport); const fsWatch = createFsWatch(transport); +const fdOps = createFdOps(metadataCache, contentCache, transport); export const fsShim = { promises: fsPromises, @@ -24,6 +26,15 @@ export const fsShim = { statSync: fsSync.statSync, readdirSync: fsSync.readdirSync, + open: fdOps.open, + openSync: fdOps.openSync, + read: fdOps.read, + readSync: fdOps.readSync, + close: fdOps.close, + closeSync: fdOps.closeSync, + fstat: fdOps.fstat, + fstatSync: fdOps.fstatSync, + watch: fsWatch.watch, constants, diff --git a/src/shims/fs/promises.js b/src/shims/fs/promises.js index de08f6d..0debde4 100644 --- a/src/shims/fs/promises.js +++ b/src/shims/fs/promises.js @@ -197,5 +197,48 @@ export function createFsPromises(metadataCache, contentCache, transport) { metadataCache.set(path, meta); } }, + + async open(path, flags) { + if (!metadataCache.has(path)) { + const err = new Error( + `ENOENT: no such file or directory, open '${path}'`, + ); + err.code = "ENOENT"; + throw err; + } + + const data = await this.readFile(path); + const fileData = + typeof data === "string" ? new TextEncoder().encode(data) : data; + + const fileStat = metadataCache.toStat(path) || { + size: fileData.length, + isFile: () => true, + isDirectory: () => false, + }; + + return { + async stat() { + return fileStat; + }, + + async read(buffer, offset, length, position) { + const available = Math.min(length, fileData.length - position); + + if (available <= 0) { + return { bytesRead: 0, buffer }; + } + + const slice = fileData.subarray(position, position + available); + buffer.set(slice, offset); + + return { bytesRead: available, buffer }; + }, + + async close() { + // Nothing to clean up - data is in memory + }, + }; + }, }; } diff --git a/src/shims/globals.js b/src/shims/globals.js index f3c4c4e..befdd64 100644 --- a/src/shims/globals.js +++ b/src/shims/globals.js @@ -24,6 +24,18 @@ function installBuffer() { return new Uint8Array(data); }, + alloc: function (size, fill, encoding) { + const buf = new Uint8Array(size); + + if (fill !== undefined) { + buf.fill(typeof fill === "string" ? fill.charCodeAt(0) : fill); + } + + return buf; + }, + allocUnsafe: function (size) { + return new Uint8Array(size); + }, concat: function (arrays) { const total = arrays.reduce((sum, a) => sum + a.length, 0); const result = new Uint8Array(total); @@ -39,6 +51,20 @@ function installBuffer() { isBuffer: function (obj) { return obj instanceof Uint8Array; }, + byteLength: function (str, encoding) { + return new TextEncoder().encode(str).length; + }, + isEncoding: function (encoding) { + return [ + "utf8", + "utf-8", + "ascii", + "binary", + "base64", + "hex", + "latin1", + ].includes((encoding || "").toLowerCase()); + }, }; } diff --git a/src/shims/node/zlib.js b/src/shims/node/zlib.js new file mode 100644 index 0000000..b310afa --- /dev/null +++ b/src/shims/node/zlib.js @@ -0,0 +1,138 @@ +// Zlib shim using pako for browser-side deflate/inflate/gzip/gunzip. +// Implements Node's zlib convenience functions (async callback + sync variants). +// Streaming classes (createDeflate, createGzip, etc.) are NOT implemented yet. + +import pako from "pako"; + +// --- Constants --- + +export const constants = { + Z_NO_FLUSH: 0, + Z_PARTIAL_FLUSH: 1, + Z_SYNC_FLUSH: 2, + Z_FULL_FLUSH: 3, + Z_FINISH: 4, + Z_BLOCK: 5, + Z_TREES: 6, + Z_OK: 0, + Z_STREAM_END: 1, + Z_NEED_DICT: 2, + Z_ERRNO: -1, + Z_STREAM_ERROR: -2, + Z_DATA_ERROR: -3, + Z_MEM_ERROR: -4, + Z_BUF_ERROR: -5, + Z_VERSION_ERROR: -6, + Z_NO_COMPRESSION: 0, + Z_BEST_SPEED: 1, + Z_BEST_COMPRESSION: 9, + Z_DEFAULT_COMPRESSION: -1, + Z_FILTERED: 1, + Z_HUFFMAN_ONLY: 2, + Z_RLE: 3, + Z_FIXED: 4, + Z_DEFAULT_STRATEGY: 0, + Z_DEFAULT_WINDOWBITS: 15, + Z_DEFAULT_MEMLEVEL: 8, +}; + +// --- Helpers --- + +function toUint8Array(buf) { + if (buf instanceof Uint8Array) { + return buf; + } + + if (typeof buf === "string") { + return new TextEncoder().encode(buf); + } + + if (buf instanceof ArrayBuffer) { + return new Uint8Array(buf); + } + + if (ArrayBuffer.isView(buf)) { + return new Uint8Array(buf.buffer, buf.byteOffset, buf.byteLength); + } + + return new Uint8Array(buf); +} + +function wrapAsync(syncFn) { + return function (buf, optionsOrCb, cb) { + if (typeof optionsOrCb === "function") { + cb = optionsOrCb; + optionsOrCb = {}; + } + + try { + const result = syncFn(buf, optionsOrCb || {}); + + if (cb) { + queueMicrotask(() => cb(null, result)); + } + } catch (e) { + if (cb) { + queueMicrotask(() => cb(e)); + } + } + }; +} + +// --- Sync functions --- + +export function deflateSync(buf, options) { + return pako.deflate(toUint8Array(buf), options); +} + +export function inflateSync(buf, options) { + return pako.inflate(toUint8Array(buf), options); +} + +export function deflateRawSync(buf, options) { + return pako.deflateRaw(toUint8Array(buf), options); +} + +export function inflateRawSync(buf, options) { + return pako.inflateRaw(toUint8Array(buf), options); +} + +export function gzipSync(buf, options) { + return pako.gzip(toUint8Array(buf), options); +} + +export function gunzipSync(buf, options) { + return pako.ungzip(toUint8Array(buf), options); +} + +export function unzipSync(buf, options) { + return pako.ungzip(toUint8Array(buf), options); +} + +// --- Async functions (callback style) --- + +export const deflate = wrapAsync(deflateSync); +export const inflate = wrapAsync(inflateSync); +export const deflateRaw = wrapAsync(deflateRawSync); +export const inflateRaw = wrapAsync(inflateRawSync); +export const gzip = wrapAsync(gzipSync); +export const gunzip = wrapAsync(gunzipSync); +export const unzip = wrapAsync(unzipSync); + +// --- Streaming stubs (not yet implemented) --- + +function notImplemented(name) { + return function () { + throw new Error( + `zlib.${name}() streaming is not yet implemented. Use the sync/callback variants instead.`, + ); + }; +} + +export const createDeflate = notImplemented("createDeflate"); +export const createInflate = notImplemented("createInflate"); +export const createDeflateRaw = notImplemented("createDeflateRaw"); +export const createInflateRaw = notImplemented("createInflateRaw"); +export const createGzip = notImplemented("createGzip"); +export const createGunzip = notImplemented("createGunzip"); +export const createUnzip = notImplemented("createUnzip"); diff --git a/src/shims/require.js b/src/shims/require.js index e414b87..637f230 100644 --- a/src/shims/require.js +++ b/src/shims/require.js @@ -9,6 +9,7 @@ import * as eventsShim from "./node/events.js"; import * as osShim from "./node/os.js"; import * as netShim from "./node/net.js"; import * as httpShim from "./node/http.js"; +import * as zlibShim from "./node/zlib.js"; import { wrapWithProxy, installDebugHelpers } from "./debug.js"; const rawRegistry = { @@ -25,6 +26,7 @@ const rawRegistry = { net: netShim, http: httpShim, https: httpShim, + zlib: zlibShim, }; const shimRegistry = {}; diff --git a/src/ui/components/layout/MessageDialog.svelte b/src/ui/components/layout/MessageDialog.svelte index 0386dab..f04f1a6 100644 --- a/src/ui/components/layout/MessageDialog.svelte +++ b/src/ui/components/layout/MessageDialog.svelte @@ -13,8 +13,8 @@ let modalRef; function onConfirm() { - dispatch("confirm"); modalRef.dismiss(); + dispatch("confirm"); } function onEscape() {