Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: support recursive readdir in Asar files #41582

Merged
merged 1 commit into from
Mar 19, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
272 changes: 260 additions & 12 deletions lib/node/asar-fs-wrapper.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { Buffer } from 'buffer';
import { constants } from 'fs';
import { Dirent, constants } from 'fs';
import * as path from 'path';
import * as util from 'util';

Expand Down Expand Up @@ -46,7 +46,17 @@ process._getOrCreateArchive = getOrCreateArchive;

const asarRe = /\.asar/i;

const { getValidatedPath } = __non_webpack_require__('internal/fs/utils');
const {
getValidatedPath,
getOptions,
getDirent
} = __non_webpack_require__('internal/fs/utils');

const {
validateBoolean,
validateFunction
} = __non_webpack_require__('internal/validators');

// In the renderer node internals use the node global URL but we do not set that to be
// the global URL instance. We need to do instanceof checks against the internal URL impl
const { URL: NodeURL } = __non_webpack_require__('internal/url');
Expand Down Expand Up @@ -78,6 +88,22 @@ const gid = process.getgid?.() ?? 0;

const fakeTime = new Date();

function getDirents (p: string, { 0: names, 1: types }: any[][]): Dirent[] {
const info = splitPath(p);
const len = names.length;
for (let i = 0; i < len; i++) {
if (info.isAsar) {
const archive = getOrCreateArchive(info.asarPath);
const stats = archive!.stat(p);
if (!stats) continue;
names[i] = getDirent(p, names[i], stats.type);
} else {
names[i] = getDirent(p, names[i], types[i]);
}
}
return names;
}

enum AsarFileType {
kFile = (constants as any).UV_DIRENT_FILE,
kDirectory = (constants as any).UV_DIRENT_DIR,
Expand Down Expand Up @@ -662,13 +688,27 @@ export const wrapFsWithAsar = (fs: Record<string, any>) => {
return (encoding) ? buffer.toString(encoding) : buffer;
};

type ReaddirOptions = { encoding: BufferEncoding | null; withFileTypes?: false, recursive?: false } | undefined | null;
type ReaddirCallback = (err: NodeJS.ErrnoException | null, files: string[]) => void;

const { readdir } = fs;
fs.readdir = function (pathArgument: string, options?: { encoding?: string | null; withFileTypes?: boolean } | null, callback?: Function) {
const pathInfo = splitPath(pathArgument);
if (typeof options === 'function') {
callback = options;
options = undefined;
fs.readdir = function (pathArgument: string, options: ReaddirOptions, callback: ReaddirCallback) {
callback = typeof options === 'function' ? options : callback;
validateFunction(callback, 'callback');

options = getOptions(options);
pathArgument = getValidatedPath(pathArgument);

if (options?.recursive != null) {
validateBoolean(options?.recursive, 'options.recursive');
}

if (options?.recursive) {
nextTick(callback!, [null, readdirSyncRecursive(pathArgument, options)]);
return;
}

const pathInfo = splitPath(pathArgument);
if (!pathInfo.isAsar) return readdir.apply(this, arguments);
const { asarPath, filePath } = pathInfo;

Expand Down Expand Up @@ -705,12 +745,62 @@ export const wrapFsWithAsar = (fs: Record<string, any>) => {
nextTick(callback!, [null, files]);
};

fs.promises.readdir = util.promisify(fs.readdir);
const { readdir: readdirPromise } = require('fs').promises;
fs.promises.readdir = async function (pathArgument: string, options: ReaddirOptions) {
options = getOptions(options);
pathArgument = getValidatedPath(pathArgument);

if (options?.recursive != null) {
validateBoolean(options?.recursive, 'options.recursive');
}

if (options?.recursive) {
return readdirRecursive(pathArgument, options);
}

const pathInfo = splitPath(pathArgument);
if (!pathInfo.isAsar) return readdirPromise(pathArgument, options);
const { asarPath, filePath } = pathInfo;

const archive = getOrCreateArchive(asarPath);
if (!archive) {
return Promise.reject(createError(AsarError.INVALID_ARCHIVE, { asarPath }));
}

type ReaddirSyncOptions = { encoding: BufferEncoding | null; withFileTypes?: false };
const files = archive.readdir(filePath);
if (!files) {
return Promise.reject(createError(AsarError.NOT_FOUND, { asarPath, filePath }));
}

if (options?.withFileTypes) {
const dirents = [];
for (const file of files) {
const childPath = path.join(filePath, file);
const stats = archive.stat(childPath);
if (!stats) {
throw createError(AsarError.NOT_FOUND, { asarPath, filePath: childPath });
}
dirents.push(new fs.Dirent(file, stats.type));
}
return Promise.resolve(dirents);
}

return Promise.resolve(files);
};

const { readdirSync } = fs;
fs.readdirSync = function (pathArgument: string, options: ReaddirSyncOptions | BufferEncoding | null) {
fs.readdirSync = function (pathArgument: string, options: ReaddirOptions) {
options = getOptions(options);
pathArgument = getValidatedPath(pathArgument);

if (options?.recursive != null) {
validateBoolean(options?.recursive, 'options.recursive');
}

if (options?.recursive) {
return readdirSyncRecursive(pathArgument, options);
}

const pathInfo = splitPath(pathArgument);
if (!pathInfo.isAsar) return readdirSync.apply(this, arguments);
const { asarPath, filePath } = pathInfo;
Expand All @@ -725,7 +815,7 @@ export const wrapFsWithAsar = (fs: Record<string, any>) => {
throw createError(AsarError.NOT_FOUND, { asarPath, filePath });
}

if (options && (options as ReaddirSyncOptions).withFileTypes) {
if (options?.withFileTypes) {
const dirents = [];
for (const file of files) {
const childPath = path.join(filePath, file);
Expand All @@ -741,7 +831,8 @@ export const wrapFsWithAsar = (fs: Record<string, any>) => {
return files;
};

const { internalModuleReadJSON } = internalBinding('fs');
const binding = internalBinding('fs');
const { internalModuleReadJSON, kUsePromises } = binding;
internalBinding('fs').internalModuleReadJSON = (pathArgument: string) => {
const pathInfo = splitPath(pathArgument);
if (!pathInfo.isAsar) return internalModuleReadJSON(pathArgument);
Expand Down Expand Up @@ -787,6 +878,163 @@ export const wrapFsWithAsar = (fs: Record<string, any>) => {
return (stats.type === AsarFileType.kDirectory) ? 1 : 0;
};

async function readdirRecursive (originalPath: string, options: ReaddirOptions) {
const result: any[] = [];

const pathInfo = splitPath(originalPath);
let queue: [string, string[]][] = [];

let initialItem = [];
if (pathInfo.isAsar) {
const archive = getOrCreateArchive(pathInfo.asarPath);
if (!archive) return result;
const files = archive.readdir(pathInfo.filePath);
if (!files) return result;
initialItem = files;
} else {
initialItem = await binding.readdir(
path.toNamespacedPath(originalPath),
options!.encoding,
!!options!.withFileTypes,
kUsePromises
);
}

queue = [[originalPath, initialItem]];

if (options?.withFileTypes) {
while (queue.length > 0) {
// @ts-expect-error this is a valid array destructure assignment.
const { 0: pathArg, 1: readDir } = queue.pop();
for (const dirent of getDirents(pathArg, readDir)) {
result.push(dirent);
if (dirent.isDirectory()) {
const direntPath = path.join(pathArg, dirent.name);
const info = splitPath(direntPath);
let archive;
let readdirResult;
if (info.isAsar) {
archive = getOrCreateArchive(info.asarPath);
if (!archive) continue;
readdirResult = archive.readdir(info.filePath);
} else {
readdirResult = await binding.readdir(
direntPath,
options.encoding,
true,
kUsePromises
);
}
queue.push([direntPath, readdirResult]);
}
}
}
} else {
while (queue.length > 0) {
// @ts-expect-error this is a valid array destructure assignment.
const { 0: pathArg, 1: readDir } = queue.pop();
for (const ent of readDir) {
const direntPath = path.join(pathArg, ent);
const stat = internalBinding('fs').internalModuleStat(direntPath);
result.push(path.relative(originalPath, direntPath));

if (stat === 1) {
const subPathInfo = splitPath(direntPath);
let item = [];
if (subPathInfo.isAsar) {
const archive = getOrCreateArchive(subPathInfo.asarPath);
if (!archive) return;
const files = archive.readdir(subPathInfo.filePath);
if (!files) return result;
item = files;
} else {
item = await binding.readdir(
path.toNamespacedPath(direntPath),
options!.encoding,
false,
kUsePromises
);
}
queue.push([direntPath, item]);
}
}
}
}

return result;
}

function readdirSyncRecursive (basePath: string, options: ReaddirOptions) {
const withFileTypes = Boolean(options!.withFileTypes);
const encoding = options!.encoding;

const readdirResults: string[] = [];
const pathsQueue = [basePath];

function read (pathArg: string) {
let readdirResult;
const pathInfo = splitPath(pathArg);

let archive;
if (pathInfo.isAsar) {
const { asarPath, filePath } = pathInfo;
archive = getOrCreateArchive(asarPath);
if (!archive) return;

readdirResult = archive.readdir(filePath);
} else {
readdirResult = binding.readdir(
path.toNamespacedPath(pathArg),
encoding,
withFileTypes
);
}

if (readdirResult === undefined) return;

if (withFileTypes) {
// Calling `readdir` with `withFileTypes=true`, the result is an array of arrays.
// The first array is the names, and the second array is the types.
// They are guaranteed to be the same length; hence, setting `length` to the length
// of the first array within the result.
const length = readdirResult[0].length;
for (let i = 0; i < length; i++) {
let dirent;
if (pathInfo.isAsar) {
const stats = archive!.stat(pathArg);
if (!stats) continue;
dirent = getDirent(pathArg, readdirResult[0][i], stats.type);
} else {
dirent = getDirent(pathArg, readdirResult[0][i], readdirResult[1][i]);
}

readdirResults.push(dirent);
if (dirent.isDirectory()) {
pathsQueue.push(path.join(dirent.path, dirent.name));
}
}
} else {
for (let i = 0; i < readdirResult.length; i++) {
const resultPath = path.join(pathArg, readdirResult[i]);
const relativeResultPath = path.relative(basePath, resultPath);
const stat = internalBinding('fs').internalModuleStat(resultPath);
readdirResults.push(relativeResultPath);

// 1 indicates directory
if (stat === 1) {
pathsQueue.push(resultPath);
}
}
}
}

for (let i = 0; i < pathsQueue.length; i++) {
read(pathsQueue[i]);
}

return readdirResults;
}

// Calling mkdir for directory inside asar archive should throw ENOTDIR
// error, but on Windows it throws ENOENT.
if (process.platform === 'win32') {
Expand Down