From 040acaaf3026aec8148aa010283e650e629d682b Mon Sep 17 00:00:00 2001 From: Shelley Vohr Date: Thu, 21 Mar 2024 15:22:40 +0100 Subject: [PATCH] fix: support `withFileTypes` in `fs.{readdir|readdirSync}` (#41627) fix: support withFileTypes in fs.{readdir|readdirSync} --- lib/node/asar-fs-wrapper.ts | 87 ++++++++++++++++++++++++------------- spec/asar-spec.ts | 61 +++++++++++++++++++++++++- 2 files changed, 115 insertions(+), 33 deletions(-) diff --git a/lib/node/asar-fs-wrapper.ts b/lib/node/asar-fs-wrapper.ts index fda03e62e132..666f52cbff6a 100644 --- a/lib/node/asar-fs-wrapper.ts +++ b/lib/node/asar-fs-wrapper.ts @@ -89,18 +89,19 @@ 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++) { + for (let i = 0; i < names.length; i++) { + let type = types[i]; + const info = splitPath(path.join(p, names[i])); if (info.isAsar) { const archive = getOrCreateArchive(info.asarPath); - const stats = archive!.stat(p); + if (!archive) continue; + const stats = archive.stat(info.filePath); if (!stats) continue; - names[i] = getDirent(p, names[i], stats.type); - } else { - names[i] = getDirent(p, names[i], types[i]); + type = stats.type; } + names[i] = getDirent(p, names[i], type); } + return names; } @@ -883,6 +884,7 @@ export const wrapFsWithAsar = (fs: Record) => { const pathInfo = splitPath(originalPath); let queue: [string, string[]][] = []; + const withFileTypes = Boolean(options?.withFileTypes); let initialItem = []; if (pathInfo.isAsar) { @@ -890,19 +892,29 @@ export const wrapFsWithAsar = (fs: Record) => { if (!archive) return result; const files = archive.readdir(pathInfo.filePath); if (!files) return result; + + // If we're in an asar dir, we need to ensure the result is in the same format as the + // native call to readdir withFileTypes i.e. an array of arrays. initialItem = files; + if (withFileTypes) { + initialItem = [ + [...initialItem], initialItem.map((p: string) => { + return internalBinding('fs').internalModuleStat(path.join(originalPath, p)); + }) + ]; + } } else { initialItem = await binding.readdir( path.toNamespacedPath(originalPath), options!.encoding, - !!options!.withFileTypes, + withFileTypes, kUsePromises ); } queue = [[originalPath, initialItem]]; - if (options?.withFileTypes) { + if (withFileTypes) { while (queue.length > 0) { // @ts-expect-error this is a valid array destructure assignment. const { 0: pathArg, 1: readDir } = queue.pop(); @@ -911,16 +923,22 @@ export const wrapFsWithAsar = (fs: Record) => { 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); + const archive = getOrCreateArchive(info.asarPath); if (!archive) continue; - readdirResult = archive.readdir(info.filePath); + const files = archive.readdir(info.filePath); + if (!files) continue; + + readdirResult = [ + [...files], files.map((p: string) => { + return internalBinding('fs').internalModuleStat(path.join(direntPath, p)); + }) + ]; } else { readdirResult = await binding.readdir( direntPath, - options.encoding, + options!.encoding, true, kUsePromises ); @@ -973,15 +991,24 @@ export const wrapFsWithAsar = (fs: Record) => { function read (pathArg: string) { let readdirResult; - const pathInfo = splitPath(pathArg); - let archive; + const pathInfo = splitPath(pathArg); if (pathInfo.isAsar) { const { asarPath, filePath } = pathInfo; - archive = getOrCreateArchive(asarPath); + const archive = getOrCreateArchive(asarPath); if (!archive) return; readdirResult = archive.readdir(filePath); + if (!readdirResult) return; + // If we're in an asar dir, we need to ensure the result is in the same format as the + // native call to readdir withFileTypes i.e. an array of arrays. + if (withFileTypes) { + readdirResult = [ + [...readdirResult], readdirResult.map((p: string) => { + return internalBinding('fs').internalModuleStat(path.join(pathArg, p)); + }) + ]; + } } else { readdirResult = binding.readdir( path.toNamespacedPath(pathArg), @@ -993,21 +1020,22 @@ export const wrapFsWithAsar = (fs: Record) => { 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); + const resultPath = path.join(pathArg, readdirResult[0][i]); + const info = splitPath(resultPath); + + let type = readdirResult[1][i]; + if (info.isAsar) { + const archive = getOrCreateArchive(info.asarPath); + if (!archive) return; + const stats = archive.stat(info.filePath); if (!stats) continue; - dirent = getDirent(pathArg, readdirResult[0][i], stats.type); - } else { - dirent = getDirent(pathArg, readdirResult[0][i], readdirResult[1][i]); + type = stats.type; } + const dirent = getDirent(pathArg, readdirResult[0][i], type); + readdirResults.push(dirent); if (dirent.isDirectory()) { pathsQueue.push(path.join(dirent.path, dirent.name)); @@ -1018,12 +1046,9 @@ export const wrapFsWithAsar = (fs: Record) => { 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); - } + readdirResults.push(relativeResultPath); + if (stat === 1) pathsQueue.push(resultPath); } } } diff --git a/spec/asar-spec.ts b/spec/asar-spec.ts index beb064493339..879ed287b8c5 100644 --- a/spec/asar-spec.ts +++ b/spec/asar-spec.ts @@ -898,9 +898,28 @@ describe('asar package', function () { expect(dirs).to.deep.equal(['dir1', 'dir2', 'dir3', 'file1', 'file2', 'file3', 'link1', 'link2', 'ping.js']); }); - itremote('supports recursive readdirSync', async () => { + itremote('supports recursive readdirSync withFileTypes', () => { const dir = path.join(fixtures, 'recursive-asar'); - const files = await fs.readdirSync(dir, { recursive: true }); + const files = fs.readdirSync(dir, { recursive: true, withFileTypes: true }); + + expect(files).to.have.length(24); + + for (const file of files) { + expect(file).to.be.an.instanceOf(fs.Dirent); + } + + const paths = files.map((a: any) => a.name); + expect(paths).to.have.members([ + 'a.asar', 'nested', 'test.txt', 'dir1', 'dir2', 'dir3', + 'file1', 'file2', 'file3', 'link1', 'link2', 'ping.js', + 'hello.txt', 'file1', 'file2', 'file3', 'link1', 'link2', + 'file1', 'file2', 'file3', 'file1', 'file2', 'file3' + ]); + }); + + itremote('supports recursive readdirSync', () => { + const dir = path.join(fixtures, 'recursive-asar'); + const files = fs.readdirSync(dir, { recursive: true }); expect(files).to.have.members([ 'a.asar', 'nested', @@ -1007,6 +1026,25 @@ describe('asar package', function () { ]); }); + itremote('supports readdir withFileTypes', async () => { + const dir = path.join(fixtures, 'recursive-asar'); + const files = await promisify(fs.readdir)(dir, { recursive: true, withFileTypes: true }); + + expect(files).to.have.length(24); + + for (const file of files) { + expect(file).to.be.an.instanceOf(fs.Dirent); + } + + const paths = files.map((a: any) => a.name); + expect(paths).to.have.members([ + 'a.asar', 'nested', 'test.txt', 'dir1', 'dir2', 'dir3', + 'file1', 'file2', 'file3', 'link1', 'link2', 'ping.js', + 'hello.txt', 'file1', 'file2', 'file3', 'link1', 'link2', + 'file1', 'file2', 'file3', 'file1', 'file2', 'file3' + ]); + }); + itremote('supports withFileTypes', async () => { const p = path.join(asarDir, 'a.asar'); @@ -1102,6 +1140,25 @@ describe('asar package', function () { ]); }); + itremote('supports readdir withFileTypes', async () => { + const dir = path.join(fixtures, 'recursive-asar'); + const files = await fs.promises.readdir(dir, { recursive: true, withFileTypes: true }); + + expect(files).to.have.length(24); + + for (const file of files) { + expect(file).to.be.an.instanceOf(fs.Dirent); + } + + const paths = files.map((a: any) => a.name); + expect(paths).to.have.members([ + 'a.asar', 'nested', 'test.txt', 'dir1', 'dir2', 'dir3', + 'file1', 'file2', 'file3', 'link1', 'link2', 'ping.js', + 'hello.txt', 'file1', 'file2', 'file3', 'link1', 'link2', + 'file1', 'file2', 'file3', 'file1', 'file2', 'file3' + ]); + }); + itremote('supports withFileTypes', async function () { const p = path.join(asarDir, 'a.asar'); const dirs = await fs.promises.readdir(p, { withFileTypes: true });