Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: Respect unpack minimatch for symlinks within previously unpacked directories #341

Merged
merged 4 commits into from
Jan 7, 2025
Merged
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
49 changes: 32 additions & 17 deletions src/asar.ts
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,8 @@ export async function createPackageFromFiles(
});

const filesystem = new Filesystem(src);
const files: { filename: string; unpack: boolean }[] = [];
const files: disk.BasicFilesArray = [];
const links: disk.BasicFilesArray = [];
const unpackDirs: string[] = [];

let filenamesSorted: string[] = [];
Expand Down Expand Up @@ -140,37 +141,51 @@ export async function createPackageFromFiles(
}
const file = metadata[filename];

const shouldUnpackPath = function (
relativePath: string,
unpack: string | undefined,
unpackDir: string | undefined,
) {
let shouldUnpack = false;
if (unpack) {
shouldUnpack = minimatch(filename, unpack, { matchBase: true });
}
if (!shouldUnpack && unpackDir) {
shouldUnpack = isUnpackedDir(relativePath, unpackDir, unpackDirs);
}
return shouldUnpack;
};

let shouldUnpack: boolean;
switch (file.type) {
case 'directory':
if (options.unpackDir) {
shouldUnpack = isUnpackedDir(path.relative(src, filename), options.unpackDir, unpackDirs);
} else {
shouldUnpack = false;
}
shouldUnpack = shouldUnpackPath(path.relative(src, filename), undefined, options.unpackDir);
filesystem.insertDirectory(filename, shouldUnpack);
break;
case 'file':
shouldUnpack = false;
if (options.unpack) {
shouldUnpack = minimatch(filename, options.unpack, { matchBase: true });
}
if (!shouldUnpack && options.unpackDir) {
const dirName = path.relative(src, path.dirname(filename));
shouldUnpack = isUnpackedDir(dirName, options.unpackDir, unpackDirs);
}
files.push({ filename: filename, unpack: shouldUnpack });
shouldUnpack = shouldUnpackPath(
path.relative(src, path.dirname(filename)),
options.unpack,
options.unpackDir,
);
files.push({ filename, unpack: shouldUnpack });
return filesystem.insertFile(filename, shouldUnpack, file, options);
case 'link':
filesystem.insertLink(filename);
shouldUnpack = shouldUnpackPath(
path.relative(src, filename),
options.unpack,
options.unpackDir,
);
links.push({ filename, unpack: shouldUnpack });
filesystem.insertLink(filename, shouldUnpack);
break;
}
return Promise.resolve();
};

const insertsDone = async function () {
await fs.mkdirp(path.dirname(dest));
return disk.writeFilesystem(dest, filesystem, files, metadata);
return disk.writeFilesystem(dest, filesystem, { files, links }, metadata);
};

const names = filenamesSorted.slice();
Expand Down
28 changes: 24 additions & 4 deletions src/disk.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,14 +37,17 @@ export type InputMetadata = {

export type BasicFilesArray = { filename: string; unpack: boolean }[];

export type FilesystemFilesAndLinks = { files: BasicFilesArray; links: BasicFilesArray };

const writeFileListToStream = async function (
dest: string,
filesystem: Filesystem,
out: NodeJS.WritableStream,
fileList: BasicFilesArray,
lists: FilesystemFilesAndLinks,
metadata: InputMetadata,
) {
for (const file of fileList) {
const { files, links } = lists;
for (const file of files) {
if (file.unpack) {
// the file should not be packed into archive
const filename = path.relative(filesystem.getRootPath(), file.filename);
Expand All @@ -53,13 +56,30 @@ const writeFileListToStream = async function (
await streamTransformedFile(file.filename, out, metadata[file.filename].transformed);
}
}
const unpackedSymlinks = links.filter((f) => f.unpack);
for (const file of unpackedSymlinks) {
// the symlink needs to be recreated outside in .unpacked
const filename = path.relative(filesystem.getRootPath(), file.filename);
const link = await fs.readlink(file.filename);
// if symlink is within subdirectories, then we need to recreate dir structure
await fs.mkdirp(path.join(`${dest}.unpacked`, path.dirname(filename)));
// create symlink within unpacked dir
await fs.symlink(link, path.join(`${dest}.unpacked`, filename)).catch(async (error) => {
if (error.code === 'EPERM' && error.syscall === 'symlink') {
throw new Error(
'Could not create symlinks for unpacked assets. On Windows, consider activating Developer Mode to allow non-admin users to create symlinks by following the instructions at https://docs.microsoft.com/en-us/windows/apps/get-started/enable-your-device-for-development.',
);
}
throw error;
});
}
return out.end();
};

export async function writeFilesystem(
dest: string,
filesystem: Filesystem,
fileList: BasicFilesArray,
lists: FilesystemFilesAndLinks,
metadata: InputMetadata,
) {
const headerPickle = Pickle.createEmpty();
Expand All @@ -76,7 +96,7 @@ export async function writeFilesystem(
out.write(sizeBuf);
return out.write(headerBuf, () => resolve());
});
return writeFileListToStream(dest, filesystem, out, fileList, metadata);
return writeFileListToStream(dest, filesystem, out, lists, metadata);
}

export interface FileRecord extends FilesystemFileEntry {
Expand Down
6 changes: 5 additions & 1 deletion src/filesystem.ts
Original file line number Diff line number Diff line change
Expand Up @@ -156,7 +156,7 @@ export class Filesystem {
this.offset += BigInt(size);
}

insertLink(p: string) {
insertLink(p: string, shouldUnpack: boolean) {
const symlink = fs.readlinkSync(p);
// /var => /private/var
const parentPath = fs.realpathSync(path.dirname(p));
Expand All @@ -165,6 +165,10 @@ export class Filesystem {
throw new Error(`${p}: file "${link}" links out of the package`);
}
const node = this.searchNodeFromPath(p) as FilesystemLinkEntry;
const dirNode = this.searchNodeFromPath(path.dirname(p)) as FilesystemDirectoryEntry;
if (shouldUnpack || dirNode.unpacked) {
node.unpacked = true;
}
node.link = link;
return link;
}
Expand Down
12 changes: 11 additions & 1 deletion src/wrapped-fs.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,14 @@
const fs = 'electron' in process.versions ? require('original-fs') : require('fs');

const promisifiedMethods = ['lstat', 'mkdtemp', 'readFile', 'stat', 'writeFile'];
const promisifiedMethods = [
'lstat',
'mkdtemp',
'readFile',
'stat',
'writeFile',
'symlink',
'readlink',
];

type AsarFS = typeof import('fs') & {
mkdirp(dir: string): Promise<void>;
Expand All @@ -10,6 +18,8 @@ type AsarFS = typeof import('fs') & {
readFile: (typeof import('fs'))['promises']['readFile'];
stat: (typeof import('fs'))['promises']['stat'];
writeFile: (typeof import('fs'))['promises']['writeFile'];
symlink: (typeof import('fs'))['promises']['symlink'];
readlink: (typeof import('fs'))['promises']['readlink'];
};

const promisified = {} as AsarFS;
Expand Down
19 changes: 19 additions & 0 deletions test/cli-spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ const rimraf = require('rimraf');
const compDirs = require('./util/compareDirectories');
const compFileLists = require('./util/compareFileLists');
const compFiles = require('./util/compareFiles');
const createSymlinkApp = require('./util/createSymlinkApp');

const exec = promisify(childProcess.exec);

Expand Down Expand Up @@ -188,4 +189,22 @@ describe('command line interface', function () {
) === false,
);
});
it('should unpack static framework with all underlying symlinks unpacked', async () => {
const { tmpPath } = createSymlinkApp('app');
await execAsar(
`p ${tmpPath} tmp/packthis-with-symlink.asar --unpack *.txt --unpack-dir var --exclude-hidden`,
);

assert.ok(fs.existsSync('tmp/packthis-with-symlink.asar.unpacked/private/var/file.txt'));
assert.ok(fs.existsSync('tmp/packthis-with-symlink.asar.unpacked/private/var/app/file.txt'));
assert.strictEqual(
fs.readlinkSync('tmp/packthis-with-symlink.asar.unpacked/private/var/app/file.txt'),
path.join('..', 'file.txt'),
);
assert.strictEqual(
fs.readlinkSync('tmp/packthis-with-symlink.asar.unpacked/var'),
path.join('private', 'var'),
);
assert.ok(fs.existsSync('tmp/packthis-with-symlink.asar.unpacked/var/file.txt'));
});
});
24 changes: 2 additions & 22 deletions test/filesystem-spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ const assert = require('assert');
const fs = require('../lib/wrapped-fs').default;
const path = require('path');
const rimraf = require('rimraf');
const createSymlinkedApp = require('./util/createSymlinkApp');

const Filesystem = require('../lib/filesystem').Filesystem;

Expand All @@ -13,28 +14,7 @@ describe('filesystem', function () {
});

it('should does not throw an error when the src path includes a symbol link', async () => {
/**
* Directory structure:
* tmp
* ├── private
* │ └── var
* │ ├── app
* │ │ └── file.txt -> ../file.txt
* │ └── file.txt
* └── var -> private/var
*/
const tmpPath = path.join(__dirname, '..', 'tmp');
const privateVarPath = path.join(tmpPath, 'private', 'var');
const varPath = path.join(tmpPath, 'var');
fs.mkdirSync(privateVarPath, { recursive: true });
fs.symlinkSync(path.relative(tmpPath, privateVarPath), varPath);

const originFilePath = path.join(varPath, 'file.txt');
fs.writeFileSync(originFilePath, 'hello world');
const appPath = path.join(varPath, 'app');
fs.mkdirpSync(appPath);
fs.symlinkSync('../file.txt', path.join(appPath, 'file.txt'));

const { appPath, varPath } = createSymlinkedApp('filesystem');
const filesystem = new Filesystem(varPath);
assert.doesNotThrow(() => {
filesystem.insertLink(path.join(appPath, 'file.txt'));
Expand Down
30 changes: 30 additions & 0 deletions test/util/createSymlinkApp.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
const path = require('path');
const fs = require('../../lib/wrapped-fs').default;
const rimraf = require('rimraf');
/**
* Directory structure:
* tmp
* ├── private
* │ └── var
* │ ├── app
* │ │ └── file.txt -> ../file.txt
* │ └── file.txt
* └── var -> private/var
*/
module.exports = (testName) => {
const tmpPath = path.join(__dirname, '../..', 'tmp', testName || 'app');
const privateVarPath = path.join(tmpPath, 'private', 'var');
const varPath = path.join(tmpPath, 'var');

rimraf.sync(tmpPath, fs);

fs.mkdirSync(privateVarPath, { recursive: true });
fs.symlinkSync(path.relative(tmpPath, privateVarPath), varPath);

const originFilePath = path.join(varPath, 'file.txt');
fs.writeFileSync(originFilePath, 'hello world');
const appPath = path.join(varPath, 'app');
fs.mkdirpSync(appPath);
fs.symlinkSync('../file.txt', path.join(appPath, 'file.txt'));
return { appPath, tmpPath, varPath };
};