| // Generated by //js/private/node-patches:compile |
| "use strict"; |
| /** |
| * @license |
| * Copyright 2019 The Bazel Authors. All rights reserved. |
| * |
| * Licensed under the Apache License, Version 2.0 (the "License"); |
| * you may not use this file except in compliance with the License. |
| * |
| * You may obtain a copy of the License at |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| var __asyncValues = (this && this.__asyncValues) || function (o) { |
| if (!Symbol.asyncIterator) throw new TypeError("Symbol.asyncIterator is not defined."); |
| var m = o[Symbol.asyncIterator], i; |
| return m ? m.call(o) : (o = typeof __values === "function" ? __values(o) : o[Symbol.iterator](), i = {}, verb("next"), verb("throw"), verb("return"), i[Symbol.asyncIterator] = function () { return this; }, i); |
| function verb(n) { i[n] = o[n] && function (v) { return new Promise(function (resolve, reject) { v = o[n](v), settle(resolve, reject, v.done, v.value); }); }; } |
| function settle(resolve, reject, d, v) { Promise.resolve(v).then(function(v) { resolve({ value: v, done: d }); }, reject); } |
| }; |
| var __await = (this && this.__await) || function (v) { return this instanceof __await ? (this.v = v, this) : new __await(v); } |
| var __asyncGenerator = (this && this.__asyncGenerator) || function (thisArg, _arguments, generator) { |
| if (!Symbol.asyncIterator) throw new TypeError("Symbol.asyncIterator is not defined."); |
| var g = generator.apply(thisArg, _arguments || []), i, q = []; |
| return i = Object.create((typeof AsyncIterator === "function" ? AsyncIterator : Object).prototype), verb("next"), verb("throw"), verb("return", awaitReturn), i[Symbol.asyncIterator] = function () { return this; }, i; |
| function awaitReturn(f) { return function (v) { return Promise.resolve(v).then(f, reject); }; } |
| function verb(n, f) { if (g[n]) { i[n] = function (v) { return new Promise(function (a, b) { q.push([n, v, a, b]) > 1 || resume(n, v); }); }; if (f) i[n] = f(i[n]); } } |
| function resume(n, v) { try { step(g[n](v)); } catch (e) { settle(q[0][3], e); } } |
| function step(r) { r.value instanceof __await ? Promise.resolve(r.value.v).then(fulfill, reject) : settle(q[0][2], r); } |
| function fulfill(value) { resume("next", value); } |
| function reject(value) { resume("throw", value); } |
| function settle(f, v) { if (f(v), q.shift(), q.length) resume(q[0][0], q[0][1]); } |
| }; |
| Object.defineProperty(exports, "__esModule", { value: true }); |
| exports.patcher = patcher; |
| exports.isSubPath = isSubPath; |
| exports.escapeFunction = escapeFunction; |
| const path = require("path"); |
| const util = require("util"); |
| // using require here on purpose so we can override methods with any |
| // also even though imports are mutable in typescript the cognitive dissonance is too high because |
| // es modules |
| const fs = require('node:fs'); |
| const url = require('node:url'); |
| const esmModule = require('node:module'); |
| const HOP_NON_LINK = Symbol.for('HOP NON LINK'); |
| const HOP_NOT_FOUND = Symbol.for('HOP NOT FOUND'); |
| const PATCHED_FS_METHODS = [ |
| 'lstat', |
| 'lstatSync', |
| 'realpath', |
| 'realpathSync', |
| 'readlink', |
| 'readlinkSync', |
| 'readdir', |
| 'readdirSync', |
| 'opendir', |
| ]; |
| /** |
| * Function that patches the `fs` module to not escape the given roots. |
| * @returns a function to undo the patches. |
| */ |
| function patcher(roots) { |
| if (fs._unpatched) { |
| throw new Error('FS is already patched.'); |
| } |
| // Make the original version of the library available for when access to the |
| // unguarded file system is necessary, such as the esbuild plugin that |
| // protects against sandbox escaping that occurs through module resolution |
| // in the Go binary. See |
| // https://github.com/aspect-build/rules_esbuild/issues/58. |
| fs._unpatched = PATCHED_FS_METHODS.reduce((obj, method) => { |
| obj[method] = fs[method]; |
| return obj; |
| }, {}); |
| roots = roots || []; |
| roots = roots.filter((root) => fs.existsSync(root)); |
| if (!roots.length) { |
| if (process.env.VERBOSE_LOGS) { |
| console.error('fs patcher called without any valid root paths ' + __filename); |
| } |
| return function () { }; |
| } |
| const origLstat = fs.lstat.bind(fs); |
| const origLstatSync = fs.lstatSync.bind(fs); |
| const origReaddir = fs.readdir.bind(fs); |
| const origReaddirSync = fs.readdirSync.bind(fs); |
| const origReadlink = fs.readlink.bind(fs); |
| const origReadlinkSync = fs.readlinkSync.bind(fs); |
| const origRealpath = fs.realpath.bind(fs); |
| const origRealpathNative = fs.realpath |
| .native; |
| const origRealpathSync = fs.realpathSync.bind(fs); |
| const origRealpathSyncNative = fs.realpathSync |
| .native; |
| const { canEscape, isEscape } = escapeFunction(roots); |
| // ========================================================================= |
| // fs.lstat |
| // ========================================================================= |
| fs.lstat = function lstat(...args) { |
| // preserve error when calling function without required callback |
| if (typeof args[args.length - 1] !== 'function') { |
| return origLstat(...args); |
| } |
| const cb = once(args[args.length - 1]); |
| // override the callback |
| args[args.length - 1] = function lstatCb(err, stats) { |
| if (err) |
| return cb(err); |
| if (!stats.isSymbolicLink()) { |
| // the file is not a symbolic link so there is nothing more to do |
| return cb(null, stats); |
| } |
| args[0] = resolvePathLike(args[0]); |
| if (!canEscape(args[0])) { |
| // the file can not escaped the sandbox so there is nothing more to do |
| return cb(null, stats); |
| } |
| return guardedReadLink(args[0], guardedReadLinkCb); |
| function guardedReadLinkCb(str) { |
| if (str != args[0]) { |
| // there are one or more hops within the guards so there is nothing more to do |
| return cb(null, stats); |
| } |
| // there are no hops so lets report the stats of the real file; |
| // we can't use origRealPath here since that function calls lstat internally |
| // which can result in an infinite loop |
| return unguardedRealPath(args[0], unguardedRealPathCb); |
| function unguardedRealPathCb(err, str) { |
| if (err) { |
| if (err.code === 'ENOENT') { |
| // broken link so there is nothing more to do |
| return cb(null, stats); |
| } |
| return cb(err); |
| } |
| return origLstat(str, cb); |
| } |
| } |
| }; |
| origLstat(...args); |
| }; |
| fs.lstatSync = function lstatSync(...args) { |
| const stats = origLstatSync(...args); |
| if (!(stats === null || stats === void 0 ? void 0 : stats.isSymbolicLink())) { |
| // the file is not a symbolic link so there is nothing more to do |
| return stats; |
| } |
| args[0] = resolvePathLike(args[0]); |
| if (!canEscape(args[0])) { |
| // the file can not escaped the sandbox so there is nothing more to do |
| return stats; |
| } |
| const guardedReadLink = guardedReadLinkSync(args[0]); |
| if (guardedReadLink != args[0]) { |
| // there are one or more hops within the guards so there is nothing more to do |
| return stats; |
| } |
| try { |
| args[0] = unguardedRealPathSync(args[0]); |
| // there are no hops so lets report the stats of the real file; |
| // we can't use origRealPathSync here since that function calls lstat internally |
| // which can result in an infinite loop |
| return origLstatSync(...args); |
| } |
| catch (err) { |
| if (err.code === 'ENOENT') { |
| // broken link so there is nothing more to do |
| return stats; |
| } |
| throw err; |
| } |
| }; |
| // ========================================================================= |
| // fs.realpath |
| // ========================================================================= |
| fs.realpath = function realpath(...args) { |
| // preserve error when calling function without required callback |
| if (typeof args[args.length - 1] !== 'function') { |
| return origRealpath(...args); |
| } |
| const cb = once(args[args.length - 1]); |
| args[args.length - 1] = function realpathCb(err, str) { |
| if (err) |
| return cb(err); |
| const escapedRoot = isEscape(args[0], str); |
| if (escapedRoot) { |
| return guardedRealPath(args[0], cb, escapedRoot); |
| } |
| else { |
| return cb(null, str); |
| } |
| }; |
| origRealpath(...args); |
| }; |
| fs.realpath.native = function realpath_native(...args) { |
| // preserve error when calling function without required callback |
| if (typeof args[args.length - 1] !== 'function') { |
| return origRealpathNative(...args); |
| } |
| const cb = once(args[args.length - 1]); |
| args[args.length - 1] = function nativeCb(err, str) { |
| if (err) |
| return cb(err); |
| const escapedRoot = isEscape(args[0], str); |
| if (escapedRoot) { |
| return guardedRealPath(args[0], cb, escapedRoot); |
| } |
| else { |
| return cb(null, str); |
| } |
| }; |
| origRealpathNative(...args); |
| }; |
| fs.realpathSync = function realpathSync(...args) { |
| const str = origRealpathSync(...args); |
| const escapedRoot = isEscape(args[0], str); |
| if (escapedRoot) { |
| return guardedRealPathSync(args[0], escapedRoot); |
| } |
| return str; |
| }; |
| fs.realpathSync.native = function native_realpathSync(...args) { |
| const str = origRealpathSyncNative(...args); |
| const escapedRoot = isEscape(args[0], str); |
| if (escapedRoot) { |
| return guardedRealPathSync(args[0], escapedRoot); |
| } |
| return str; |
| }; |
| // ========================================================================= |
| // fs.readlink |
| // ========================================================================= |
| fs.readlink = function readlink(...args) { |
| // preserve error when calling function without required callback |
| if (typeof args[args.length - 1] !== 'function') { |
| return origReadlink(...args); |
| } |
| const cb = once(args[args.length - 1]); |
| args[args.length - 1] = function readlinkCb(err, p) { |
| if (err) |
| return cb(err); |
| const resolved = resolvePathLike(args[0]); |
| const str = path.resolve(path.dirname(resolved), p); |
| const escapedRoot = isEscape(resolved, str); |
| if (escapedRoot) { |
| return nextHop(str, readlinkNextHopCb); |
| function readlinkNextHopCb(next) { |
| if (!next) { |
| if (next == undefined) { |
| // The escape from the root is not mappable back into the root; throw EINVAL |
| return cb(enoent('readlink', args[0])); |
| } |
| else { |
| // The escape from the root is not mappable back into the root; throw EINVAL |
| return cb(einval('readlink', args[0])); |
| } |
| } |
| const r = path.resolve(path.dirname(resolved), path.relative(path.dirname(str), next)); |
| if (r != resolved && |
| !isEscape(resolved, r, [escapedRoot])) { |
| return cb(null, r); |
| } |
| // The escape from the root is not mappable back into the root; throw EINVAL |
| return cb(einval('readlink', args[0])); |
| } |
| } |
| else { |
| return cb(null, str); |
| } |
| }; |
| origReadlink(...args); |
| }; |
| fs.readlinkSync = function readlinkSync(...args) { |
| const resolved = resolvePathLike(args[0]); |
| const str = path.resolve(path.dirname(resolved), origReadlinkSync(...args)); |
| const escapedRoot = isEscape(resolved, str); |
| if (escapedRoot) { |
| const next = nextHopSync(str); |
| if (!next) { |
| if (next == undefined) { |
| // The escape from the root is not mappable back into the root; throw EINVAL |
| throw enoent('readlink', args[0]); |
| } |
| else { |
| // The escape from the root is not mappable back into the root; throw EINVAL |
| throw einval('readlink', args[0]); |
| } |
| } |
| const r = path.resolve(path.dirname(resolved), path.relative(path.dirname(str), next)); |
| if (r != resolved && !isEscape(resolved, r, [escapedRoot])) { |
| return r; |
| } |
| // The escape from the root is not mappable back into the root; throw EINVAL |
| throw einval('readlink', args[0]); |
| } |
| return str; |
| }; |
| // ========================================================================= |
| // fs.readdir |
| // ========================================================================= |
| fs.readdir = function readdir(...args) { |
| // preserve error when calling function without required callback |
| if (typeof args[args.length - 1] !== 'function') { |
| return origReaddir(...args); |
| } |
| const cb = once(args[args.length - 1]); |
| const p = resolvePathLike(args[0]); |
| args[args.length - 1] = function readdirCb(err, result) { |
| if (err) |
| return cb(err); |
| // user requested withFileTypes |
| if (result[0] && result[0].isSymbolicLink) { |
| Promise.all(result.map((v) => handleDirent(p, v))) |
| .then(() => { |
| cb(null, result); |
| }) |
| .catch((err) => { |
| cb(err); |
| }); |
| } |
| else { |
| // string array return for readdir. |
| cb(null, result); |
| } |
| }; |
| origReaddir(...args); |
| }; |
| fs.readdirSync = function readdirSync(...args) { |
| const res = origReaddirSync(...args); |
| const p = resolvePathLike(args[0]); |
| res.forEach((v) => { |
| handleDirentSync(p, v); |
| }); |
| return res; |
| }; |
| // ========================================================================= |
| // fs.opendir |
| // ========================================================================= |
| if (fs.opendir) { |
| const origOpendir = fs.opendir.bind(fs); |
| fs.opendir = function opendir(...args) { |
| // if this is not a function opendir should throw an error. |
| // we call it so we don't have to throw a mock |
| if (typeof args[args.length - 1] === 'function') { |
| const cb = once(args[args.length - 1]); |
| args[args.length - 1] = async function opendirCb(err, dir) { |
| try { |
| cb(err, err ? undefined : handleDir(dir)); |
| } |
| catch (err) { |
| cb(err); |
| } |
| }; |
| origOpendir(...args); |
| } |
| else { |
| return origOpendir(...args).then(handleDir); |
| } |
| }; |
| } |
| if (fs.opendirSync) { |
| const origOpendirSync = fs.opendirSync.bind(fs); |
| fs.opendirSync = function opendirSync(...args) { |
| const dir = origOpendirSync(...args); |
| return handleDir(dir); |
| }; |
| } |
| // ========================================================================= |
| // fs.promises |
| // ========================================================================= |
| /** |
| * patch fs.promises here. |
| * |
| * this requires a light touch because if we trigger the getter on older nodejs versions |
| * it will log an experimental warning to stderr |
| * |
| * `(node:62945) ExperimentalWarning: The fs.promises API is experimental` |
| * |
| * this api is available as experimental without a flag so users can access it at any time. |
| */ |
| const promisePropertyDescriptor = Object.getOwnPropertyDescriptor(fs, 'promises'); |
| let unpatchPromises; |
| if (promisePropertyDescriptor) { |
| const promises = {}; |
| promises.lstat = util.promisify(fs.lstat); |
| // NOTE: node core uses the newer realpath function fs.promises.native instead of fs.realPath |
| promises.realpath = util.promisify(fs.realpath.native); |
| promises.readlink = util.promisify(fs.readlink); |
| promises.readdir = util.promisify(fs.readdir); |
| if (fs.opendir) |
| promises.opendir = util.promisify(fs.opendir); |
| // handle experimental api warnings. |
| // only applies to version of node where promises is a getter property. |
| if (promisePropertyDescriptor.get) { |
| const oldGetter = promisePropertyDescriptor.get.bind(fs); |
| const cachedPromises = {}; |
| function promisePropertyGetter() { |
| const _promises = oldGetter(); |
| Object.assign(cachedPromises, _promises, promises); |
| return cachedPromises; |
| } |
| Object.defineProperty(fs, 'promises', Object.assign(Object.create(promisePropertyDescriptor), { |
| get: promisePropertyGetter, |
| })); |
| unpatchPromises = function unpatchFsPromises() { |
| Object.defineProperty(fs, 'promises', promisePropertyDescriptor); |
| }; |
| } |
| else { |
| const unpatchedPromises = Object.keys(promises).reduce((obj, method) => { |
| obj[method] = fs.promises[method]; |
| return obj; |
| }, Object.create(fs.promises)); |
| // api can be patched directly |
| Object.assign(fs.promises, promises); |
| unpatchPromises = function unpatchFsPromises() { |
| Object.assign(fs.promises, unpatchedPromises); |
| }; |
| } |
| } |
| // ========================================================================= |
| // helper functions for dirs |
| // ========================================================================= |
| function handleDir(dir) { |
| const p = path.resolve(dir.path); |
| const origIterator = dir[Symbol.asyncIterator].bind(dir); |
| dir[Symbol.asyncIterator] = function () { |
| return __asyncGenerator(this, arguments, function* () { |
| var _a, e_1, _b, _c; |
| try { |
| for (var _d = true, _f = __asyncValues(origIterator()), _g; _g = yield __await(_f.next()), _a = _g.done, !_a; _d = true) { |
| _c = _g.value; |
| _d = false; |
| const entry = _c; |
| yield __await(handleDirent(p, entry)); |
| yield yield __await(entry); |
| } |
| } |
| catch (e_1_1) { e_1 = { error: e_1_1 }; } |
| finally { |
| try { |
| if (!_d && !_a && (_b = _f.return)) yield __await(_b.call(_f)); |
| } |
| finally { if (e_1) throw e_1.error; } |
| } |
| }); |
| }; |
| const origRead = dir.read.bind(dir); |
| dir.read = function readWrapper() { |
| if (typeof arguments[0] === 'function') { |
| return handleDirReadCallback(arguments[0]); |
| } |
| else { |
| return handleDirReadPromise(); |
| } |
| }; |
| // read(cb: (err: NodeJS.ErrnoException | null, dirEnt: Dirent | null) => void): void; |
| function handleDirReadCallback(cb) { |
| origRead(function handleDirReadCb(err, entry) { |
| if (err) |
| return cb(err, null); |
| handleDirent(p, entry).then(() => { |
| cb(null, entry); |
| }, (err) => cb(err, null)); |
| }); |
| } |
| // read(): Promise<Dirent | null>; |
| async function handleDirReadPromise() { |
| const entry = await origRead(); |
| if (entry) { |
| await handleDirent(p, entry); |
| } |
| return entry; |
| } |
| const origReadSync = dir.readSync.bind(dir); |
| dir.readSync = function handleDirReadSync() { |
| return handleDirentSync(p, origReadSync()); |
| }; |
| return dir; |
| } |
| async function handleDirent(p, v) { |
| if (!v.isSymbolicLink()) { |
| return v; |
| } |
| const f = path.resolve(p, v.name); |
| return new Promise(function handleDirentExecutor(resolve, reject) { |
| return guardedReadLink(f, handleDirentReadLinkCb); |
| function handleDirentReadLinkCb(str) { |
| if (f != str) { |
| return resolve(v); |
| } |
| // There are no hops so we should hide the fact that the file is a symlink |
| v.isSymbolicLink = () => false; |
| origRealpath(f, function handleDirentRealpathCb(err, str) { |
| if (err) { |
| return reject(err); |
| } |
| fs.stat(str, function handleDirentStatCb(err, stat) { |
| if (err) { |
| return reject(err); |
| } |
| patchDirent(v, stat); |
| resolve(v); |
| }); |
| }); |
| } |
| }); |
| } |
| function handleDirentSync(p, v) { |
| if (v && v.isSymbolicLink) { |
| if (v.isSymbolicLink()) { |
| const f = path.resolve(p, v.name); |
| if (f == guardedReadLinkSync(f)) { |
| // There are no hops so we should hide the fact that the file is a symlink |
| v.isSymbolicLink = () => false; |
| const stat = fs.statSync(origRealpathSync(f)); |
| patchDirent(v, stat); |
| } |
| } |
| } |
| return v; |
| } |
| function nextHop(loc, cb) { |
| let nested = ''; |
| let maybe = loc; |
| let escapedHop = false; |
| readHopLink(maybe, function readNextHop(link) { |
| if (link === HOP_NOT_FOUND) { |
| return cb(undefined); |
| } |
| if (link !== HOP_NON_LINK) { |
| if (nested) { |
| link = link + path.sep + nested; |
| } |
| if (!isEscape(loc, link)) { |
| return cb(link); |
| } |
| if (!escapedHop) { |
| escapedHop = link; |
| } |
| } |
| const dirname = path.dirname(maybe); |
| if (!dirname || |
| dirname == maybe || |
| dirname == '.' || |
| dirname == '/') { |
| // not a link |
| return cb(escapedHop); |
| } |
| nested = path.basename(maybe) + (nested ? path.sep + nested : ''); |
| maybe = dirname; |
| readHopLink(maybe, readNextHop); |
| }); |
| } |
| const symlinkNoThrow = Object.freeze({ |
| throwIfNoEntry: false, |
| }); |
| const hopLinkCache = Object.create(null); |
| function readHopLinkSync(p) { |
| if (hopLinkCache[p]) { |
| return hopLinkCache[p]; |
| } |
| let link; |
| const pStats = origLstatSync(p, symlinkNoThrow); |
| if (!pStats) { |
| link = HOP_NOT_FOUND; |
| } |
| else if (pStats.isSymbolicLink()) { |
| link = origReadlinkSync(p); |
| if (link) { |
| if (!path.isAbsolute(link)) { |
| link = path.resolve(path.dirname(p), link); |
| } |
| } |
| else { |
| link = HOP_NON_LINK; |
| } |
| } |
| else { |
| link = HOP_NON_LINK; |
| } |
| hopLinkCache[p] = link; |
| return link; |
| } |
| function readHopLink(p, cb) { |
| if (hopLinkCache[p]) { |
| return cb(hopLinkCache[p]); |
| } |
| origReadlink(p, function readHopLinkCb(err, link) { |
| if (err) { |
| let result; |
| if (err.code === 'ENOENT') { |
| // file does not exist |
| result = HOP_NOT_FOUND; |
| } |
| else { |
| result = HOP_NON_LINK; |
| } |
| hopLinkCache[p] = result; |
| return cb(result); |
| } |
| if (link === undefined) { |
| hopLinkCache[p] = HOP_NON_LINK; |
| return cb(HOP_NON_LINK); |
| } |
| if (!path.isAbsolute(link)) { |
| link = path.resolve(path.dirname(p), link); |
| } |
| hopLinkCache[p] = link; |
| cb(link); |
| }); |
| } |
| function nextHopSync(loc) { |
| let nested = ''; |
| let maybe = loc; |
| let escapedHop = false; |
| for (;;) { |
| let link = readHopLinkSync(maybe); |
| if (link === HOP_NOT_FOUND) { |
| return false; |
| } |
| if (link !== HOP_NON_LINK) { |
| if (nested) { |
| link = link + path.sep + nested; |
| } |
| if (!isEscape(loc, link)) { |
| return link; |
| } |
| if (!escapedHop) { |
| escapedHop = link; |
| } |
| } |
| const dirname = path.dirname(maybe); |
| if (!dirname || |
| dirname == maybe || |
| dirname == '.' || |
| dirname == '/') { |
| // not a link |
| return escapedHop; |
| } |
| nested = path.basename(maybe) + (nested ? path.sep + nested : ''); |
| maybe = dirname; |
| } |
| } |
| function guardedReadLink(loc, cb) { |
| nextHop(loc, guardedReadLinkHopCb); |
| function guardedReadLinkHopCb(next) { |
| if (!next) { |
| // we're no longer hopping but we haven't escaped; |
| // something funky happened in the filesystem |
| return cb(loc); |
| } |
| if (isEscape(loc, next)) { |
| // this hop takes us out of the guard |
| return cb(loc); |
| } |
| return cb(next); |
| } |
| } |
| function guardedReadLinkSync(loc) { |
| const next = nextHopSync(loc); |
| if (!next) { |
| // we're no longer hopping but we haven't escaped; |
| // something funky happened in the filesystem |
| return loc; |
| } |
| if (isEscape(loc, next)) { |
| // this hop takes us out of the guard |
| return loc; |
| } |
| return next; |
| } |
| function unguardedRealPath(start, cb) { |
| // stringifyPathLike() to handle the "undefined" case (matches behavior as fs.realpath) |
| oneHop(stringifyPathLike(start), cb); |
| function oneHop(loc, cb) { |
| nextHop(loc, function oneHopeNextCb(next) { |
| if (next == undefined) { |
| // file does not exist (broken link) |
| return cb(enoent('realpath', start), undefined); |
| } |
| else if (!next) { |
| // we've hit a real file |
| return cb(null, loc); |
| } |
| oneHop(next, cb); |
| }); |
| } |
| } |
| function guardedRealPath(start, cb, escapedRoot) { |
| // stringifyPathLike() to handle the "undefined" case (matches behavior as fs.realpath) |
| oneHop(stringifyPathLike(start), cb); |
| function oneHop(loc, cb) { |
| nextHop(loc, function guardedRealPathHopCb(next) { |
| if (!next) { |
| return cb(enoent('realpath', start), undefined); |
| } |
| if (escapedRoot |
| ? isEscape(loc, next, [escapedRoot]) |
| : isEscape(loc, next)) { |
| // this hop takes us out of the guard |
| return cb(null, loc); |
| } |
| oneHop(next, cb); |
| }); |
| } |
| } |
| function unguardedRealPathSync(start) { |
| // stringifyPathLike() to handle the "undefined" case (matches behavior as fs.realpathSync) |
| for (let loc = stringifyPathLike(start), next;; loc = next) { |
| next = nextHopSync(loc); |
| if (next == undefined) { |
| // file does not exist (broken link) |
| throw enoent('realpath', start); |
| } |
| else if (!next) { |
| // we've hit a real file |
| return loc; |
| } |
| } |
| } |
| function guardedRealPathSync(start, escapedRoot) { |
| // stringifyPathLike() to handle the "undefined" case (matches behavior as fs.realpathSync) |
| for (let loc = stringifyPathLike(start), next;; loc = next) { |
| next = nextHopSync(loc); |
| if (!next) { |
| // we're no longer hopping but we haven't escaped |
| if (fs.existsSync(loc)) { |
| // we hit a real file within the guard and can go no further |
| return loc; |
| } |
| else { |
| // something funky happened in the filesystem; throw ENOENT |
| throw enoent('realpath', start); |
| } |
| } |
| if (escapedRoot |
| ? isEscape(loc, next, [escapedRoot]) |
| : isEscape(loc, next)) { |
| // this hop takes us out of the guard |
| return loc; |
| } |
| } |
| } |
| // Sync the esm modules to use the now patched fs cjs module. |
| // See: https://nodejs.org/api/esm.html#builtin-modules |
| esmModule.syncBuiltinESMExports(); |
| return function revertPatch() { |
| Object.assign(fs, fs._unpatched); |
| delete fs._unpatched; |
| if (unpatchPromises) { |
| unpatchPromises(); |
| } |
| // Re-sync the esm modules to revert to the unpatched module. |
| esmModule.syncBuiltinESMExports(); |
| }; |
| } |
| // ========================================================================= |
| // generic helper functions |
| // ========================================================================= |
| function isSubPath(parent, child) { |
| return (parent === child || |
| (child[parent.length] === path.sep && child.startsWith(parent))); |
| } |
| function stringifyPathLike(p) { |
| if (p instanceof URL) { |
| return url.fileURLToPath(p); |
| } |
| else { |
| return String(p); |
| } |
| } |
| function resolvePathLike(p) { |
| return path.resolve(stringifyPathLike(p)); |
| } |
| function normalizePathLike(p) { |
| const s = stringifyPathLike(p); |
| // TODO: are URLs always absolute? |
| if (!path.isAbsolute(s)) { |
| return path.resolve(s); |
| } |
| else { |
| return path.normalize(s); |
| } |
| } |
| function escapeFunction(_roots) { |
| // Ensure roots are always absolute. |
| // Sort to ensure escaping multiple roots chooses the longest one. |
| const defaultRoots = _roots |
| .map((root) => path.resolve(root)) |
| .sort((a, b) => b.length - a.length); |
| function fs_isEscape(linkPath, linkTarget, roots = defaultRoots) { |
| // linkPath is the path of the symlink file itself |
| // linkTarget is a path that the symlink points to one or more hops away |
| // linkTarget must already be normalized |
| linkPath = normalizePathLike(linkPath); |
| for (const root of roots) { |
| // If the link is in the root check if the realPath has escaped |
| if (isSubPath(root, linkPath) && !isSubPath(root, linkTarget)) { |
| return root; |
| } |
| } |
| return false; |
| } |
| function fs_canEscape(maybeLinkPath, roots = defaultRoots) { |
| // maybeLinkPath is the path which may be a symlink |
| // maybeLinkPath must already be normalized |
| for (const root of roots) { |
| // If the link is in the root check if the realPath has escaped |
| if (isSubPath(root, maybeLinkPath)) { |
| return true; |
| } |
| } |
| return false; |
| } |
| return { |
| isEscape: fs_isEscape, |
| canEscape: fs_canEscape, |
| }; |
| } |
| function once(fn) { |
| let called = false; |
| return function callOnce(...args) { |
| if (called) |
| return; |
| called = true; |
| let err = false; |
| try { |
| fn(...args); |
| } |
| catch (_e) { |
| err = _e; |
| } |
| // blow the stack to make sure this doesn't fall into any unresolved promise contexts |
| if (err) { |
| setImmediate(() => { |
| throw err; |
| }); |
| } |
| }; |
| } |
| function patchDirent(dirent, stat) { |
| // add all stat is methods to Dirent instances with their result. |
| for (const i in stat) { |
| if (i.startsWith('is') && typeof stat[i] === 'function') { |
| // |
| const result = stat[i](); |
| if (result) |
| dirent[i] = () => true; |
| else |
| dirent[i] = () => false; |
| } |
| } |
| } |
| function enoent(s, p) { |
| let err = new Error(`ENOENT: no such file or directory, ${s} '${p}'`); |
| err.errno = -2; |
| err.syscall = s; |
| err.code = 'ENOENT'; |
| err.path = p; |
| return err; |
| } |
| function einval(s, p) { |
| let err = new Error(`EINVAL: invalid argument, ${s} '${p}'`); |
| err.errno = -22; |
| err.syscall = s; |
| err.code = 'EINVAL'; |
| err.path = p; |
| return err; |
| } |