Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 12 additions & 4 deletions src/asar.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
20 changes: 15 additions & 5 deletions src/filesystem.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<void> {
const dirNode = this.searchNodeFromPath(path.dirname(p)) as FilesystemDirectoryEntry;
Expand Down Expand Up @@ -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
}
Expand Down
41 changes: 41 additions & 0 deletions test/api-spec.ts
Original file line number Diff line number Diff line change
@@ -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';
Expand Down Expand Up @@ -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);
Expand Down
Loading