Skip to content

Commit e975881

Browse files
committed
feat: don't pack into asar node module that contains executable/binary files
Close #602
1 parent 0fd6a3e commit e975881

File tree

9 files changed

+201
-47
lines changed

9 files changed

+201
-47
lines changed

package.json

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -69,9 +69,10 @@
6969
"electron-osx-sign": "^0.4.0-beta4",
7070
"electron-winstaller-fixed": "~3.1.0",
7171
"extract-zip": "^1.5.0",
72-
"fs-extra-p": "^1.0.5",
72+
"fs-extra-p": "^1.0.6",
7373
"hosted-git-info": "^2.1.5",
7474
"image-size": "^0.5.0",
75+
"isbinaryfile": "^3.0.0",
7576
"lodash.template": "^4.3.0",
7677
"mime": "^1.3.4",
7778
"minimatch": "^3.0.2",
@@ -120,7 +121,7 @@
120121
"pre-git": "^3.10.0",
121122
"should": "^10.0.0",
122123
"ts-babel": "^1.0.3",
123-
"tslint": "3.14.0-dev.1",
124+
"tslint": "3.14.0",
124125
"typescript": "^2.1.0-dev.20160726",
125126
"whitespace": "^2.0.0"
126127
},

src/asarUtil.ts

Lines changed: 175 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,21 @@
1-
import { AsarFileInfo, listPackage, statFile, AsarOptions, AsarFileMetadata, createPackageFromFiles } from "asar-electron-builder"
1+
import { AsarFileInfo, listPackage, statFile, AsarOptions } from "asar-electron-builder"
22
import { statOrNull } from "./util/util"
3-
import { lstat, readdir } from "fs-extra-p"
3+
import { lstat, readdir, readFile, mkdirp, Stats } from "fs-extra-p"
44
import { Promise as BluebirdPromise } from "bluebird"
55
import * as path from "path"
6-
import { Stats } from "fs"
76
import pathSorter = require("path-sort")
7+
import { log } from "./util/log"
8+
import { Minimatch } from "minimatch"
9+
10+
const isBinaryFile: any = BluebirdPromise.promisify(require("isbinaryfile"))
811

912
//noinspection JSUnusedLocalSymbols
1013
const __awaiter = require("./util/awaiter")
1114

1215
const concurrency = {concurrency: 50}
16+
const NODE_MODULES_PATTERN = path.sep + "node_modules" + path.sep
1317

14-
function walk(dirPath: string, consumer: (file: string, stat: Stats) => void, filter: (file: string) => boolean): BluebirdPromise<any> {
18+
function walk(dirPath: string, consumer: (file: string, stat: Stats) => void, filter: (file: string) => boolean, addRootToResult?: boolean): BluebirdPromise<Array<string>> {
1519
return readdir(dirPath)
1620
.then(names => {
1721
return BluebirdPromise.map(names, name => {
@@ -21,31 +25,184 @@ function walk(dirPath: string, consumer: (file: string, stat: Stats) => void, fi
2125
}
2226

2327
return lstat(filePath)
24-
.then(stat => {
28+
.then((stat): any => {
2529
consumer(filePath, stat)
2630
if (stat.isDirectory()) {
27-
return walk(filePath, consumer, filter)
31+
return walk(filePath, consumer, filter, true)
2832
}
29-
return null
33+
return filePath
3034
})
3135
}, concurrency)
3236
})
37+
.then(list => {
38+
list.sort((a, b) => {
39+
// files before directories
40+
if (Array.isArray(a) && Array.isArray(b)) {
41+
return 0
42+
}
43+
else if (a == null || Array.isArray(a)) {
44+
return 1
45+
}
46+
else if (b == null || Array.isArray(b)) {
47+
return -1
48+
}
49+
else {
50+
return a.localeCompare(b)
51+
}
52+
})
53+
54+
const result: Array<string> = addRootToResult ? [dirPath] : []
55+
for (let item of list) {
56+
if (item != null) {
57+
if (Array.isArray(item)) {
58+
result.push.apply(result, item)
59+
}
60+
else {
61+
result.push(item)
62+
}
63+
}
64+
}
65+
return result
66+
})
3367
}
3468

3569
export async function createAsarArchive(src: string, resourcesPath: string, options: AsarOptions, filter: (file: string) => boolean): Promise<any> {
36-
const metadata: { [key: string]: AsarFileMetadata; } = {}
37-
const files: Array<string> = []
38-
await walk(src, (it, stat) => {
39-
files.push(it)
40-
metadata[it] = {
41-
type: stat.isFile() ? "file" : (stat.isDirectory() ? "directory" : "link"),
42-
stat: stat,
43-
}
44-
},
45-
filter)
70+
const metadata = new Map<string, Stats>()
71+
const files = await walk(src, (it, stat) => {
72+
metadata.set(it, stat)
73+
}, filter)
4674

4775
// sort files to minimize file change (i.e. asar file is not changed dramatically on small change)
48-
await BluebirdPromise.promisify(createPackageFromFiles)(src, path.join(resourcesPath, "app.asar"), pathSorter(files), metadata, options)
76+
await createPackageFromFiles(src, path.join(resourcesPath, "app.asar"), options.ordering == null ? files : await order(src, files, options), metadata, options)
77+
}
78+
79+
const Filesystem = require("asar-electron-builder/lib/filesystem")
80+
const writeFilesystem: any = BluebirdPromise.promisify(require("asar-electron-builder/lib/disk").writeFilesystem)
81+
82+
function isUnpackDir(path: string, pattern: Minimatch, rawPattern: string): boolean {
83+
return path.indexOf(rawPattern) === 0 || pattern.match(path)
84+
}
85+
86+
async function order(src: string, filenames: Array<string>, options: any) {
87+
const orderingFiles = (await readFile(options.ordering, "utf8")).split("\n").map(function (line) {
88+
if (line.indexOf(':') !== -1) {
89+
line = line.split(':').pop()!
90+
}
91+
line = line.trim()
92+
if (line[0] === '/') {
93+
line = line.slice(1)
94+
}
95+
return line
96+
})
97+
98+
const ordering: Array<string> = []
99+
for (let file of orderingFiles) {
100+
let pathComponents = file.split(path.sep)
101+
let str = src
102+
for (let pathComponent of pathComponents) {
103+
str = path.join(str, pathComponent)
104+
ordering.push(str)
105+
}
106+
}
107+
108+
const filenamesSorted: Array<string> = []
109+
let missing = 0
110+
const total = filenames.length
111+
for (let file of ordering) {
112+
if (!filenamesSorted.includes(file) && filenames.includes(file)) {
113+
filenamesSorted.push(file)
114+
}
115+
}
116+
for (let file of filenames) {
117+
if (!filenamesSorted.includes(file)) {
118+
filenamesSorted.push(file)
119+
missing += 1;
120+
}
121+
}
122+
log(`Ordering file has ${((total - missing) / total * 100)}% coverage.`)
123+
return filenamesSorted
124+
}
125+
126+
async function createPackageFromFiles(src: string, dest: string, files: Array<string>, metadata: Map<string, Stats>, options: any) {
127+
// search auto unpacked dir
128+
const autoUnpackDirs = new Set<string>()
129+
if (options.smartUnpack !== false) {
130+
for (let file of files) {
131+
const index = file.lastIndexOf(NODE_MODULES_PATTERN)
132+
if (index < 0) {
133+
continue
134+
}
135+
136+
const nextSlashIndex = file.indexOf(path.sep, index + NODE_MODULES_PATTERN.length + 1)
137+
if (nextSlashIndex < 0) {
138+
continue
139+
}
140+
141+
const nodeModuleDir = file.substring(0, nextSlashIndex)
142+
if (autoUnpackDirs.has(nodeModuleDir) || !metadata.get(file)!.isFile()) {
143+
continue
144+
}
145+
146+
const ext = path.extname(file)
147+
let shouldUnpack = false
148+
if (ext === ".dll" || ext === ".exe") {
149+
shouldUnpack = true
150+
}
151+
else if (ext === "") {
152+
shouldUnpack = await isBinaryFile(file)
153+
}
154+
155+
if (!shouldUnpack) {
156+
continue
157+
}
158+
159+
log(`${path.relative(src, nodeModuleDir)} is not packed into asar archive - contains executable code`)
160+
autoUnpackDirs.add(nodeModuleDir)
161+
}
162+
}
163+
164+
const unpackDir = options.unpackDir == null ? null : new Minimatch(options.unpackDir)
165+
const unpack = options.unpack == null ? null : new Minimatch(options.unpack, {
166+
matchBase: true
167+
})
168+
169+
const regularFiles: Array<any> = []
170+
const filesystem = new Filesystem(src)
171+
for (let file of files) {
172+
const stat = metadata.get(file)!
173+
if (stat.isFile()) {
174+
let shouldUnpack = unpack != null && unpack.match(file)
175+
if (!shouldUnpack) {
176+
const dir = path.dirname(file)
177+
shouldUnpack = autoUnpackDirs.has(dir) || (unpackDir != null && isUnpackDir(path.relative(src, dir), unpackDir, options.unpackDir))
178+
}
179+
180+
regularFiles.push({
181+
filename: file,
182+
unpack: shouldUnpack
183+
})
184+
filesystem.insertFile(file, shouldUnpack, stat)
185+
}
186+
else if (stat.isDirectory()) {
187+
let unpacked = autoUnpackDirs.has(file) || (unpackDir != null && isUnpackDir(path.relative(src, file), unpackDir, options.unpackDir))
188+
if (!unpacked) {
189+
for (let d of autoUnpackDirs) {
190+
if (file.length > (d.length + 2) && file[d.length] === path.sep && file.startsWith(d)) {
191+
unpacked = true
192+
autoUnpackDirs.add(file)
193+
break
194+
}
195+
}
196+
}
197+
filesystem.insertDirectory(file, unpacked)
198+
}
199+
else if (stat.isSymbolicLink()) {
200+
filesystem.insertLink(file, stat)
201+
}
202+
}
203+
204+
await mkdirp(path.dirname(dest))
205+
await writeFilesystem(dest, filesystem, regularFiles)
49206
}
50207

51208
export async function checkFileInPackage(asarFile: string, relativeFile: string) {

src/packager/dirPackager.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,9 +17,9 @@ export interface ElectronPackagerOptions {
1717

1818
appInfo: AppInfo
1919

20-
icon?: string;
20+
icon?: string
2121

22-
"helper-bundle-id"?: string | null;
22+
"helper-bundle-id"?: string | null
2323

2424
ignore?: any
2525
}

src/platformPackager.ts

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -295,14 +295,15 @@ export abstract class PlatformPackager<DC extends PlatformSpecificBuildOptions>
295295
}
296296

297297
if (result == null || result === true) {
298-
return {
298+
result = {
299299
unpack: buildMetadata["asar-unpack"],
300300
unpackDir: buildMetadata["asar-unpack-dir"]
301301
}
302302
}
303-
else {
304-
return result
305-
}
303+
304+
return deepAssign(result, {
305+
306+
})
306307
}
307308

308309
private expandPattern(pattern: string, arch: Arch): string {

src/util/log.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -91,7 +91,7 @@ class Logger {
9191
cursor.hide()
9292

9393
let out = ""
94-
let firstPendingLineIndex = 0;
94+
let firstPendingLineIndex = 0
9595
while (firstPendingLineIndex < prevLineCount) {
9696
let line = this.lines[firstPendingLineIndex]
9797
if (line.promise == null || !line.promise.isPending()) {

src/util/util.ts

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ export function spawnNpmProduction(command: string, appDir: string, env?: any):
4242

4343
return spawn(npmExecPath, npmExecArgs, {
4444
cwd: appDir,
45-
stdio: "inherit",
45+
stdio: ["ignore", "pipe", process.stderr],
4646
env: env || process.env
4747
})
4848
}
@@ -107,9 +107,7 @@ export function doSpawn(command: string, args: Array<string>, options?: SpawnOpt
107107

108108
export function spawn(command: string, args?: Array<string> | null, options?: SpawnOptions): BluebirdPromise<any> {
109109
return new BluebirdPromise<any>((resolve, reject) => {
110-
const notNullArgs = args || []
111-
const childProcess = doSpawn(command, notNullArgs, options)
112-
handleProcess("close", childProcess, command, resolve, reject)
110+
handleProcess("close", doSpawn(command, args || [], options), command, resolve, reject)
113111
})
114112
}
115113

test/src/helpers/avaEx.ts

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -4,15 +4,15 @@ import test from "ava-tf"
44

55
declare module "ava-tf" {
66
namespace test {
7-
export const ifNotWindows: typeof test;
8-
export const ifOsx: typeof test;
9-
export const ifNotCi: typeof test;
10-
export const ifCi: typeof test;
11-
export const ifNotCiOsx: typeof test;
12-
export const ifDevOrWinCi: typeof test;
13-
export const ifWinCi: typeof test;
14-
export const ifDevOrLinuxCi: typeof test;
15-
export const ifNotTravis: typeof test;
7+
export const ifNotWindows: typeof test
8+
export const ifOsx: typeof test
9+
export const ifNotCi: typeof test
10+
export const ifCi: typeof test
11+
export const ifNotCiOsx: typeof test
12+
export const ifDevOrWinCi: typeof test
13+
export const ifWinCi: typeof test
14+
export const ifDevOrLinuxCi: typeof test
15+
export const ifNotTravis: typeof test
1616
}
1717
}
1818

tslint.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@
3737
"ban-keywords"
3838
],
3939
"ordered-imports": false,
40-
"semicolon": "never",
40+
"semicolon": [true, "never"],
4141
"trailing-comma": false,
4242
"object-literal-sort-keys": false,
4343
"no-var-requires": false,

typings/asar.d.ts

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -6,21 +6,18 @@ declare module "asar-electron-builder" {
66
size: number
77
}
88

9-
interface AsarFileMetadata {
10-
type: "file" | "directory" | "link"
11-
stat?: Stats
12-
}
13-
149
interface AsarOptions {
1510
unpack?: string
1611
unpackDir?: string
1712
dot?: boolean
13+
14+
ordering?: string | null
1815
}
1916

2017
export function listPackage(archive: string): Array<string>
2118

2219
// followLinks defaults to true
2320
export function statFile(archive: string, filename: string, followLinks?: boolean): AsarFileInfo | null
2421

25-
export function createPackageFromFiles(src: string, dest: string, filenames: Array<string>, metadata: { [key: string]: AsarFileMetadata;}, options: AsarOptions, callback: (error?: Error) => void): void
22+
// export function createPackageFromFiles(src: string, dest: string, filenames: Array<string>, metadata: { [key: string]: AsarFileMetadata;}, options: AsarOptions, callback: (error?: Error) => void): void
2623
}

0 commit comments

Comments
 (0)