Skip to content

Commit

Permalink
fix: support recursive readdir in Asar files
Browse files Browse the repository at this point in the history
  • Loading branch information
codebytere committed Mar 13, 2024
1 parent 26752d3 commit 557c46b
Show file tree
Hide file tree
Showing 5 changed files with 252 additions and 6 deletions.
162 changes: 156 additions & 6 deletions lib/node/asar-fs-wrapper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -664,11 +664,17 @@ export const wrapFsWithAsar = (fs: Record<string, any>) => {

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;
}

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

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

Expand Down Expand Up @@ -705,12 +711,50 @@ 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) {
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 }));
}

const files = archive.readdir(filePath);
if (!files) {
return Promise.reject(createError(AsarError.NOT_FOUND, { asarPath, filePath }));
}

if ((options as ReaddirOptions)?.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);
};

type ReaddirSyncOptions = { encoding: BufferEncoding | null; withFileTypes?: false };
type ReaddirOptions = { encoding: BufferEncoding | null; withFileTypes?: false, recursive?: false };

const { readdirSync } = fs;
fs.readdirSync = function (pathArgument: string, options: ReaddirSyncOptions | BufferEncoding | null) {
fs.readdirSync = function (pathArgument: string, options: ReaddirOptions | BufferEncoding | null) {
if ((options as ReaddirOptions)?.recursive) {
return readdirSyncRecursive(pathArgument, options as ReaddirOptions);
}

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

if (options && (options as ReaddirSyncOptions).withFileTypes) {
if ((options as ReaddirOptions)?.withFileTypes) {
const dirents = [];
for (const file of files) {
const childPath = path.join(filePath, file);
Expand All @@ -741,7 +785,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 +832,111 @@ export const wrapFsWithAsar = (fs: Record<string, any>) => {
return (stats.type === AsarFileType.kDirectory) ? 1 : 0;
};

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

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]];

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);

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

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

if (readdirResult === undefined) return;

// TODO(codebytere): support recursive readdir with file types.
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
94 changes: 94 additions & 0 deletions spec/asar-spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -161,6 +161,7 @@ describe('asar package', function () {
fs = require('node:fs')
path = require('node:path')
fixtures = ${JSON.stringify(fixtures)}
asarDir = ${JSON.stringify(asarDir)}
// This is used instead of util.promisify for some tests to dodge the
Expand Down Expand Up @@ -897,6 +898,37 @@ describe('asar package', function () {
expect(dirs).to.deep.equal(['dir1', 'dir2', 'dir3', 'file1', 'file2', 'file3', 'link1', 'link2', 'ping.js']);
});

itremote('supports recursive readdirSync', async () => {
const dir = path.join(fixtures, 'recursive-asar');
const files = await fs.readdirSync(dir, { recursive: true });
expect(files).to.have.members([
'a.asar',
'nested',
'test.txt',
'a.asar/dir1',
'a.asar/dir2',
'a.asar/dir3',
'a.asar/file1',
'a.asar/file2',
'a.asar/file3',
'a.asar/link1',
'a.asar/link2',
'a.asar/ping.js',
'nested/hello.txt',
'a.asar/dir1/file1',
'a.asar/dir1/file2',
'a.asar/dir1/file3',
'a.asar/dir1/link1',
'a.asar/dir1/link2',
'a.asar/dir2/file1',
'a.asar/dir2/file2',
'a.asar/dir2/file3',
'a.asar/dir3/file1',
'a.asar/dir3/file2',
'a.asar/dir3/file3'
]);
});

itremote('reads dirs from a normal dir', function () {
const p = path.join(asarDir, 'a.asar', 'dir1');
const dirs = fs.readdirSync(p);
Expand Down Expand Up @@ -944,6 +976,37 @@ describe('asar package', function () {
expect(dirs).to.deep.equal(['dir1', 'dir2', 'dir3', 'file1', 'file2', 'file3', 'link1', 'link2', 'ping.js']);
});

itremote('supports recursive readdirSync', async () => {
const dir = path.join(fixtures, 'recursive-asar');
const files = await promisify(fs.readdir)(dir, { recursive: true });
expect(files).to.have.members([
'a.asar',
'nested',
'test.txt',
'a.asar/dir1',
'a.asar/dir2',
'a.asar/dir3',
'a.asar/file1',
'a.asar/file2',
'a.asar/file3',
'a.asar/link1',
'a.asar/link2',
'a.asar/ping.js',
'nested/hello.txt',
'a.asar/dir1/file1',
'a.asar/dir1/file2',
'a.asar/dir1/file3',
'a.asar/dir1/link1',
'a.asar/dir1/link2',
'a.asar/dir2/file1',
'a.asar/dir2/file2',
'a.asar/dir2/file3',
'a.asar/dir3/file1',
'a.asar/dir3/file2',
'a.asar/dir3/file3'
]);
});

itremote('supports withFileTypes', async () => {
const p = path.join(asarDir, 'a.asar');

Expand Down Expand Up @@ -1008,6 +1071,37 @@ describe('asar package', function () {
expect(dirs).to.deep.equal(['dir1', 'dir2', 'dir3', 'file1', 'file2', 'file3', 'link1', 'link2', 'ping.js']);
});

itremote('supports recursive readdir', async () => {
const dir = path.join(fixtures, 'recursive-asar');
const files = await fs.promises.readdir(dir, { recursive: true });
expect(files).to.have.members([
'a.asar',
'nested',
'test.txt',
'a.asar/dir1',
'a.asar/dir2',
'a.asar/dir3',
'a.asar/file1',
'a.asar/file2',
'a.asar/file3',
'a.asar/link1',
'a.asar/link2',
'a.asar/ping.js',
'nested/hello.txt',
'a.asar/dir1/file1',
'a.asar/dir1/file2',
'a.asar/dir1/file3',
'a.asar/dir1/link1',
'a.asar/dir1/link2',
'a.asar/dir2/file1',
'a.asar/dir2/file2',
'a.asar/dir2/file3',
'a.asar/dir3/file1',
'a.asar/dir3/file2',
'a.asar/dir3/file3'
]);
});

itremote('supports withFileTypes', async function () {
const p = path.join(asarDir, 'a.asar');
const dirs = await fs.promises.readdir(p, { withFileTypes: true });
Expand Down
Binary file added spec/fixtures/recursive-asar/a.asar
Binary file not shown.
1 change: 1 addition & 0 deletions spec/fixtures/recursive-asar/nested/hello.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
goodbye!
1 change: 1 addition & 0 deletions spec/fixtures/recursive-asar/test.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Hello!

0 comments on commit 557c46b

Please sign in to comment.