From 15167faeeefb5e9ea534f3a310c56c4a4ae1fe2b Mon Sep 17 00:00:00 2001 From: Darshan Sen Date: Thu, 22 Sep 2022 19:37:48 +0530 Subject: [PATCH] FS hooks proposal This proposal documents a Loaders API-like API for providing hooks into the filesystem. --- docs/fs-hooks.md | 124 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 124 insertions(+) create mode 100644 docs/fs-hooks.md diff --git a/docs/fs-hooks.md b/docs/fs-hooks.md new file mode 100644 index 0000000..818b130 --- /dev/null +++ b/docs/fs-hooks.md @@ -0,0 +1,124 @@ +# FS hooks + +This provides a way for userland code to overload the read-only operations on a virtual file system. +The internals of the `fs` API will be changed to make calls to these hooks. + +## `load(path, type)` + +* `path`: [``](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Data_structures#String_type) The requested file path. +* `type`: `` The possible file types. If `undefined`, retry with the original `fs` API. +* Returns: + * `contents`: [``](https://nodejs.org/api/buffer.html#class-buffer) The contents of the file path. + * `type` : `` The type of the file. If `undefined`, retry with the original `fs` API. + * `mode`: [``](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Data_structures#Number_type) The file mode bit mask. + +### Example usage + +
+ +Exposing VFS code to JS via process._singleExecutableApplicationCode + +The embedded VFS can be loaded and exposed to JS through `process._singleExecutableApplicationCode` in +https://github.com/nodejs/node/blob/962b9abbe3126202d92d0f8a03adad9f51836dbb/src/node.cc#L320. + +```cc +// Refs: https://github.com/postmanlabs/postject/blob/a9d8f3de44129d105208c9ad550c549e61ff82be/postject-api.h +#include + +... + + size_t single_executable_application_size = 0; + const char* single_executable_application_code = + static_cast(postject_find_resource( + "NODE_JS_CODE", &single_executable_application_size, nullptr)); + if (single_executable_application_code != nullptr) { + Isolate* isolate = env->isolate(); + Local context = env->context(); + Local buffer = + Buffer::New( + isolate, + const_cast(single_executable_application_code), + single_executable_application_size, + [](char* data, void* hint) {}, + nullptr) + .ToLocalChecked(); + READONLY_PROPERTY( + env->process_object(), "_singleExecutableApplicationCode", buffer); + return StartExecution(env, "internal/main/single_executable_application"); + } + +... +``` +
+ + +```mjs +// asar-loader.mjs + +// See https://github.com/electron/node-chromium-pickle-js/blob/238613902005ebac2481f295db59e980f8a236ac/lib/pickle.js +import Pickle from './pickle.mjs'; + +const headerSizeLength = 8; + +function readHeader (content) { + const sizeBuf = content.subarray(0, headerSizeLength) + const sizePickle = new Pickle(sizeBuf) + const size = sizePickle.createIterator().readUInt32() + + const headerBuf = content.subarray(headerSizeLength, headerSizeLength + size) + const headerPickle = new Pickle(headerBuf) + const header = headerPickle.createIterator().readString() + + return { headerString: header, header: JSON.parse(header), headerSize: size } +} + +const asarBuffer = process._singleExecutableApplicationCode; +const asar = readHeader(asarBuffer); + +const prefix = process.execPath; + +export function load(path, { FileType, DirectoryType, NotFound }) { + if (!path.startsWith(prefix)) { + // File not present inside the VFS; retry operation with original fs. + return; + } + + const request = path.slice(prefix.length + 1).split('/')); + + let obj = asar.header; + for (let i = 0; i < request.length; ++i) { + obj = obj.files[request[i]]; + if (!obj) { + // File not found. + return { type: NotFound }; + } + } + + if (obj.files && (!obj.offset || !obj.size)) { + // Directory found. + return { + type: DirectoryType, + contents: obj.files, + mode: 0o755 // ASAR doesn't support file modes yet, so this returns a default value. + }; + } + + const { offset, size } = obj; + const pre = headerSizeLength + asar.headerSize; + const start = pre + Number(offset); + const end = start + Number(size); + const contents = asarBuffer.subarray(start, end); + + return { + type: FileType, + contents: asarBuffer.subarray(start, end), + mode: 0o644 // ASAR doesn't support file modes yet, so this returns a default value. + }; +} +``` + +The hook can be used by passing the file path through the `--experimental-fs-hook` CLI flag. + +```sh +node --experimental-fs-hook ./asar-loader.mjs +```