app-builder-lib
Version:
electron-builder lib
252 lines • 10.7 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
exports.AsarPackager = void 0;
const asar_1 = require("@electron/asar");
const builder_util_1 = require("builder-util");
const fs_1 = require("builder-util/out/fs");
const fs = require("fs-extra");
const fs_extra_1 = require("fs-extra");
const path = require("path");
const appFileCopier_1 = require("../util/appFileCopier");
const unpackDetector_1 = require("./unpackDetector");
const stream_1 = require("stream");
const os = require("os");
const resolvePath = async (file) => (file && (await (0, fs_1.exists)(file)) ? fs.realpath(file).catch(() => path.resolve(file)) : undefined);
const resolvePaths = async (filepaths) => {
return Promise.all(filepaths.map(resolvePath)).then(paths => paths.filter((it) => it != null));
};
const DENYLIST = resolvePaths([
"/usr",
"/lib",
"/bin",
"/sbin",
"/etc",
"/tmp",
"/var", // block whole /var by default. If $HOME is under /var, it's explicitly in ALLOWLIST - https://github.com/electron-userland/electron-builder/issues/9025#issuecomment-3575380041
// macOS system directories
"/System",
"/Library",
"/private",
// Windows system directories
process.env.SystemRoot,
process.env.WINDIR,
]);
const ALLOWLIST = resolvePaths([
os.tmpdir(), // always allow temp dir
os.homedir(), // always allow home dir
]);
/** @internal */
class AsarPackager {
constructor(packager, config) {
this.packager = packager;
this.config = config;
this.outFile = path.join(config.resourcePath, `app.asar`);
}
async pack(fileSets) {
const orderedFileSets = [
// Write dependencies first to minimize offset changes to asar header
...fileSets.slice(1),
// Finish with the app files that change most often
fileSets[0],
].map(set => this.orderFileSet(set));
const streams = await this.processFileSets(orderedFileSets);
await this.executeElectronAsar(streams);
}
async executeElectronAsar(streams) {
// override logger temporarily to clean up console (electron/asar does some internal logging that blogs up the default electron-builder logs)
const consoleLogger = console.log;
console.log = (...args) => {
if (args[0] === "Ordering file has 100% coverage.") {
return; // no need to log, this means our ordering logic is working correctly
}
builder_util_1.log.info({ args }, "logging @electron/asar");
};
await (0, asar_1.createPackageFromStreams)(this.outFile, streams);
console.log = consoleLogger;
}
async processFileSets(fileSets) {
var _a;
const unpackedPaths = new Set();
if (this.config.options.smartUnpack !== false) {
for (const fileSet of fileSets) {
(0, unpackDetector_1.detectUnpackedDirs)(fileSet, unpackedPaths);
}
}
const results = [];
const resultsPaths = new Set();
for (const fileSet of fileSets) {
// Don't use Promise.all, we need to retain order of execution/iteration through the already-ordered fileset
for (const [index, file] of fileSet.files.entries()) {
const transformedData = (_a = fileSet.transformedFiles) === null || _a === void 0 ? void 0 : _a.get(index);
const stat = fileSet.metadata.get(file);
const destination = path.relative(this.config.defaultDestination, (0, appFileCopier_1.getDestinationPath)(file, fileSet));
const paths = Array.from(unpackedPaths).map(p => path.normalize(p));
const isChildDirectory = (fileOrDirPath) => paths.includes(path.normalize(fileOrDirPath)) || paths.some(unpackedPath => path.normalize(fileOrDirPath).startsWith(unpackedPath + path.sep));
const isUnpacked = (dir) => {
var _a, _b, _c;
const isChild = isChildDirectory(dir);
const isFileUnpacked = (_c = (_b = (_a = this.config).unpackPattern) === null || _b === void 0 ? void 0 : _b.call(_a, file, stat)) !== null && _c !== void 0 ? _c : false;
return isChild || isFileUnpacked;
};
this.processParentDirectories(isUnpacked, destination, results, resultsPaths);
const result = await this.processFileOrSymlink({
file,
destination,
fileSet,
transformedData,
stat,
isUnpacked,
});
if (result != null) {
results.push(result);
resultsPaths.add(result.path);
}
}
}
return results;
}
processParentDirectories(isUnpacked, destination, results, resultsPaths) {
// process parent directories
let superDir = path.dirname(path.normalize(destination));
while (superDir !== ".") {
const dir = {
type: "directory",
path: superDir,
unpacked: isUnpacked(superDir),
};
// add to results if not already present
if (!resultsPaths.has(dir.path)) {
results.push(dir);
resultsPaths.add(dir.path);
}
superDir = path.dirname(superDir);
}
}
async processFileOrSymlink(options) {
const { isUnpacked, transformedData, file, destination, stat } = options;
const unpacked = isUnpacked(destination);
if (!stat.isFile() && !stat.isSymbolicLink()) {
return { path: destination, unpacked, type: "directory" };
}
// write any data if provided, skip symlink check
if (transformedData != null) {
const streamGenerator = () => {
return new stream_1.Readable({
read() {
this.push(transformedData);
this.push(null);
},
});
};
const size = Buffer.byteLength(transformedData);
return { path: destination, streamGenerator, unpacked, type: "file", stat: { mode: stat.mode, size } };
}
// verify that the file is not a direct link or symlinked to access/copy a system file
await this.protectSystemAndUnsafePaths(file, await this.packager.info.getWorkspaceRoot());
const config = {
path: destination,
streamGenerator: () => fs.createReadStream(file),
unpacked,
stat,
};
// file, stream directly
if (!stat.isSymbolicLink()) {
return {
...config,
type: "file",
};
}
// okay, it must be a symlink. evaluate link to be relative to source file in asar
let link = await (0, fs_extra_1.readlink)(file);
if (path.isAbsolute(link)) {
link = path.relative(path.dirname(file), link);
}
return {
...config,
type: "link",
symlink: link,
};
}
orderFileSet(fileSet) {
const sortedFileEntries = Array.from(fileSet.files.entries());
sortedFileEntries.sort(([, a], [, b]) => {
if (a === b) {
return 0;
}
// Place addons last because their signature changes per build
const isAAddon = a.endsWith(".node");
const isBAddon = b.endsWith(".node");
if (isAAddon && !isBAddon) {
return 1;
}
if (isBAddon && !isAAddon) {
return -1;
}
// Otherwise order by name
return a < b ? -1 : 1;
});
let transformedFiles;
if (fileSet.transformedFiles) {
transformedFiles = new Map();
const indexMap = new Map();
for (const [newIndex, [oldIndex]] of sortedFileEntries.entries()) {
indexMap.set(oldIndex, newIndex);
}
for (const [oldIndex, value] of fileSet.transformedFiles) {
const newIndex = indexMap.get(oldIndex);
if (newIndex === undefined) {
const file = fileSet.files[oldIndex];
throw new Error(`Internal error: ${file} was lost while ordering asar`);
}
transformedFiles.set(newIndex, value);
}
}
const { src, destination, metadata } = fileSet;
return {
src,
destination,
metadata,
files: sortedFileEntries.map(([, file]) => file),
transformedFiles,
};
}
async checkAgainstRoots(target, allowRoots) {
const resolved = await resolvePath(target);
for (const root of allowRoots) {
const resolvedRoot = root;
if (resolved === resolvedRoot || (resolved === null || resolved === void 0 ? void 0 : resolved.startsWith(resolvedRoot + path.sep))) {
return true;
}
}
return false;
}
async protectSystemAndUnsafePaths(file, workspaceRoot) {
const resolved = await resolvePath(file);
const logFields = { source: file, realPath: resolved };
const isUnsafe = async () => {
const workspace = await resolvePath(workspaceRoot);
if (workspace && (resolved === null || resolved === void 0 ? void 0 : resolved.startsWith(workspace))) {
// if in workspace, always safe
return false;
}
const allowed = await this.checkAgainstRoots(file, await ALLOWLIST);
if (allowed) {
return false; // allowlist is priority
}
const denied = await this.checkAgainstRoots(file, await DENYLIST);
if (denied) {
builder_util_1.log.error(logFields, `denied access to system or unsafe path`);
return true;
}
// default
builder_util_1.log.debug(logFields, `path is outside of explicit safe paths, defaulting to safe`);
return false;
};
const unsafe = await isUnsafe();
if (unsafe) {
throw new Error(`Cannot copy file [${file}] symlinked to file [${resolved}] outside the package to a system or unsafe path`);
}
}
}
exports.AsarPackager = AsarPackager;
//# sourceMappingURL=asarUtil.js.map