Repository navigation
Description
Three small problems in the read/extract side of the API, all on 4.3.2 (current main).
1. Unpacked executables lose their executable bit in extractAll
Filesystem.insertFile returns early for unpacked files without recording executable, so the header entry has no executable: true and extractAll writes the file as 0644. The copy in app.asar.unpacked is still 0755, so it is only the extract path that loses it. Packed (non-unpacked) executables keep their mode.
// npm i @electron/asar@4.3.2, save as repro.mjs; in an empty dir first run: mkdir app && printf '#!/bin/sh\n' > app/run.sh && chmod 755 app/run.sh
import * as asar from '@electron/asar';
import fs from 'node:fs';
await asar.createPackageWithOptions('app', 'app.asar', { unpack: '*.sh' });
console.log(asar.statFile('app.asar', 'run.sh').executable);
console.log((fs.statSync('app.asar.unpacked/run.sh').mode & 0o777).toString(8));
asar.extractAll('app.asar', 'out');
console.log((fs.statSync('out/run.sh').mode & 0o777).toString(8));Actual: undefined, 755, 644.
Expected: true, 755, 755, the same as for a packed executable (asar extract should give back the tree that was packed).
2. Paths more than one level below a directory symlink cannot be read
#318 made getNode follow a link when it is the immediate parent, but searchNodeFromDirectory still walks every earlier component as a plain directory and throws when one of them is a link.
// mkdir -p src/real/sub && echo hi > src/real/sub/f.txt && echo top > src/real/g.txt && ln -s real src/linkdir
import * as asar from '@electron/asar';
await asar.createPackage('src', 'a.asar');
console.log(asar.extractFile('a.asar', 'linkdir/g.txt').toString()); // works
console.log(asar.extractFile('a.asar', 'linkdir/sub/f.txt').toString()); // throwsActual: Error: Unexpected directory state while traversing: linkdir/sub.
Expected: hi\n, the same as real/sub/f.txt (and what fs.readFileSync('src/linkdir/sub/f.txt') returns).
3. Looking up a missing path inserts it into the cached header
searchNodeFromDirectory creates any missing intermediate directory while it searches. The archive's Filesystem is cached, so a failed statFile/extractFile leaves an empty directory behind that later shows up in listPackage and is created by extractAll.
// same a.asar as above
import * as asar from '@electron/asar';
import fs from 'node:fs';
try { asar.statFile('a.asar', 'missing/x.txt'); } catch (e) { console.log(e.message); }
console.log(asar.listPackage('a.asar', {}).includes('/missing'));
asar.extractAll('a.asar', 'out2');
console.log(fs.existsSync('out2/missing'));Actual: "missing/x.txt" was not found in this archive, then true, true.
Expected: the error, then false, false; a read should not change the archive's listing.
Related: since the header read from disk is a plain JSON.parse object, asar.statFile('a.asar', 'toString') returns Object.prototype.toString instead of throwing "not found".
Version: @electron/asar@4.3.2 (main e621701), Node.js v26.13.0.
Found with a differential-testing tool; the report was drafted with AI assistance and reviewed and edited by me.
Credit: https://ucsbplse.github.io/
Activity