diff --git a/src/asar.ts b/src/asar.ts index d60a922..1d375fc 100644 --- a/src/asar.ts +++ b/src/asar.ts @@ -285,10 +285,18 @@ export async function createPackageFromStreams(dest: string, streams: AsarStream mode: stream.stat.mode, unpack: stream.unpacked, }); - return filesystem.insertFile(filename, stream.streamGenerator, stream.unpacked, { - type: 'file', - stat: stream.stat, - }); + return filesystem.insertFile( + filename, + stream.streamGenerator, + stream.unpacked, + { + type: 'file', + stat: stream.stat, + }, + // `filename` is the destination path inside the archive, not a path + // on disk, so integrity must be computed from the stream. + { fromStream: true }, + ); case 'link': links.push({ filename, diff --git a/src/filesystem.ts b/src/filesystem.ts index 9a45cc8..8203e3f 100644 --- a/src/filesystem.ts +++ b/src/filesystem.ts @@ -115,6 +115,12 @@ export class Filesystem { file: CrawledFileType, options: { transform?: (filePath: string) => NodeJS.ReadWriteStream | void; + /** + * Set when the file content comes from a stream rather than a file on + * disk at `p`. In that case `p` is only the destination path inside the + * archive, so integrity must be computed from the stream. + */ + fromStream?: boolean; } = {}, ): Promise { const dirNode = this.searchNodeFromPath(path.dirname(p)) as FilesystemDirectoryEntry; @@ -145,14 +151,18 @@ export class Filesystem { node.executable = true; } - if (size <= BUFFER_HASH_THRESHOLD) { + if (!options.fromStream && size <= BUFFER_HASH_THRESHOLD) { // Fully synchronous fast path — no Promise, no stream, no microtask yield try { const fileBuffer = fs.readFileSync(p); - node.integrity = getFileIntegrityFromBuffer(fileBuffer); - file.cachedBuffer = fileBuffer; - this.offset += BigInt(size); - return Promise.resolve(); + // Only trust the buffer if it matches the size recorded in the header; + // otherwise the file at `p` is not the content being archived. + if (fileBuffer.length === size) { + node.integrity = getFileIntegrityFromBuffer(fileBuffer); + file.cachedBuffer = fileBuffer; + this.offset += BigInt(size); + return Promise.resolve(); + } } catch { // Fall through to stream path } diff --git a/test/api-spec.ts b/test/api-spec.ts index 0144d68..32a7aba 100644 --- a/test/api-spec.ts +++ b/test/api-spec.ts @@ -1,12 +1,15 @@ import { describe, it, beforeEach, expect } from 'vitest'; import { wrappedFs as fs } from '../src/wrapped-fs.js'; +import { createHash } from 'node:crypto'; import os from 'node:os'; +import path from 'node:path'; import { createPackage, createPackageFromStreams, createPackageWithOptions, extractAll, extractFile, + getRawHeader, listPackage, statFile, } from '../src/asar.js'; @@ -219,6 +222,44 @@ describe('api', () => { return compDirs('tmp/extractthis-read-stream-symlink/', src); }); + it('should compute stream package integrity from stream content, not from a same-named file in the CWD', async () => { + const realContent = 'REAL-APP-CONTENT\n'; + const decoyContent = 'DECOY-CONTENT-IN-CWD\n'; + + const work = await fs.mkdtemp(path.join(os.tmpdir(), 'asar-stream-integrity-')); + const srcFile = path.join(work, 'real.js'); + await fs.writeFile(srcFile, realContent); + + // a decoy file in the CWD at the same relative path as the archive destination path + const cwdDir = path.join(work, 'cwd'); + await fs.mkdirp(cwdDir); + await fs.writeFile(path.join(cwdDir, 'index.js'), decoyContent); + + const out = path.join(TEST_APPS_DIR, 'packthis-stream-integrity.asar'); + const stat = await fs.lstat(srcFile); + const originalCwd = process.cwd(); + process.chdir(cwdDir); + try { + await createPackageFromStreams(out, [ + { + path: 'index.js', + type: 'file', + unpacked: false, + streamGenerator: () => fs.createReadStream(srcFile), + stat, + }, + ]); + } finally { + process.chdir(originalCwd); + } + + expect(extractFile(out, 'index.js').toString()).toEqual(realContent); + const header = JSON.parse(getRawHeader(out).headerString); + expect(header.files['index.js'].integrity.hash).toEqual( + createHash('sha256').update(realContent).digest('hex'), + ); + }); + it('should throw when using NodeJS.ReadableStreams with symlink outside package', async () => { const src = 'test/input/packthis-with-bad-symlink/'; const streams = await createReadStreams(src);