cross-packer 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +661 -0
- package/README.md +151 -0
- package/README.zh-CN.md +151 -0
- package/dist/archive/ar.d.ts +12 -0
- package/dist/archive/seven-za.d.ts +32 -0
- package/dist/archive/tar.d.ts +30 -0
- package/dist/archive/zip-reader.d.ts +17 -0
- package/dist/asar/dependency/collect.d.ts +2 -0
- package/dist/asar/dependency/detector.d.ts +8 -0
- package/dist/asar/dependency/hoister.d.ts +27 -0
- package/dist/asar/dependency/utils.d.ts +59 -0
- package/dist/asar/fileset/matcher.d.ts +12 -0
- package/dist/asar/fileset/platform-fileset.d.ts +36 -0
- package/dist/asar/fileset/transformer.d.ts +2 -0
- package/dist/asar/fileset/unpack-detector.d.ts +10 -0
- package/dist/asar/fileset/walker.d.ts +14 -0
- package/dist/asar/pack.d.ts +23 -0
- package/dist/asar/packer/app-collector.d.ts +2 -0
- package/dist/asar/packer/collector.d.ts +18 -0
- package/dist/asar/packer/concurrency.d.ts +1 -0
- package/dist/asar/packer/integrity.d.ts +8 -0
- package/dist/asar/packer/nm-collector.d.ts +7 -0
- package/dist/asar/packer/stream-builder.d.ts +18 -0
- package/dist/cli.d.ts +2 -0
- package/dist/config/index.d.ts +97 -0
- package/dist/electron/dist-zip.d.ts +18 -0
- package/dist/electron/download.d.ts +21 -0
- package/dist/index.d.ts +11 -0
- package/dist/packer/common/app-update-yml.d.ts +5 -0
- package/dist/packer/common/extra-resources.d.ts +39 -0
- package/dist/packer/common/staging.d.ts +3 -0
- package/dist/packer/common/template.d.ts +2 -0
- package/dist/packer/linux/linux.d.ts +2 -0
- package/dist/packer/mac/mac.d.ts +23 -0
- package/dist/packer/win/nsis.d.ts +24 -0
- package/dist/packer/win/resedit.d.ts +5 -0
- package/dist/packer/win/toolchain.d.ts +8 -0
- package/dist/packer/win/win.d.ts +12 -0
- package/dist/pipeline/pipeline.d.ts +41 -0
- package/dist/pipeline/targets.d.ts +28 -0
- package/dist/shared/hash.d.ts +2 -0
- package/dist/shared/logger.d.ts +39 -0
- package/dist/shared/platform.d.ts +26 -0
- package/dist/shared/types.d.ts +68 -0
- package/dist/update/blockmap.d.ts +13 -0
- package/dist/update/metadata.d.ts +20 -0
- package/dist/update/version.d.ts +7 -0
- package/package.json +71 -0
- package/src/archive/ar.ts +41 -0
- package/src/archive/seven-za.ts +160 -0
- package/src/archive/tar.ts +142 -0
- package/src/archive/zip-reader.ts +61 -0
- package/src/asar/dependency/collect.ts +437 -0
- package/src/asar/dependency/detector.ts +73 -0
- package/src/asar/dependency/hoister.ts +714 -0
- package/src/asar/dependency/utils.ts +225 -0
- package/src/asar/fileset/matcher.ts +101 -0
- package/src/asar/fileset/platform-fileset.ts +416 -0
- package/src/asar/fileset/transformer.ts +154 -0
- package/src/asar/fileset/unpack-detector.ts +38 -0
- package/src/asar/fileset/walker.ts +115 -0
- package/src/asar/pack.ts +186 -0
- package/src/asar/packer/app-collector.ts +74 -0
- package/src/asar/packer/collector.ts +134 -0
- package/src/asar/packer/concurrency.ts +24 -0
- package/src/asar/packer/integrity.ts +53 -0
- package/src/asar/packer/nm-collector.ts +194 -0
- package/src/asar/packer/stream-builder.ts +723 -0
- package/src/cli.ts +182 -0
- package/src/config/index.ts +365 -0
- package/src/electron/dist-zip.ts +65 -0
- package/src/electron/download.ts +121 -0
- package/src/index.ts +14 -0
- package/src/packer/common/app-update-yml.ts +21 -0
- package/src/packer/common/extra-resources.ts +83 -0
- package/src/packer/common/staging.ts +33 -0
- package/src/packer/common/template.ts +24 -0
- package/src/packer/linux/linux.ts +329 -0
- package/src/packer/mac/mac.ts +495 -0
- package/src/packer/win/nsis.ts +244 -0
- package/src/packer/win/resedit.ts +60 -0
- package/src/packer/win/toolchain.ts +63 -0
- package/src/packer/win/win.ts +421 -0
- package/src/pipeline/pipeline.ts +149 -0
- package/src/pipeline/targets.ts +67 -0
- package/src/shared/hash.ts +15 -0
- package/src/shared/logger.ts +91 -0
- package/src/shared/platform.ts +41 -0
- package/src/shared/types.ts +82 -0
- package/src/update/blockmap.ts +247 -0
- package/src/update/metadata.ts +97 -0
- package/src/update/version.ts +59 -0
- package/templates/linux/app.desktop +11 -0
- package/templates/linux/control +9 -0
- package/templates/linux/postinst +10 -0
- package/templates/linux/postrm +8 -0
- package/templates/linux/preinst +49 -0
- package/templates/linux/prerm +43 -0
- package/templates/mac/Info.plist +18 -0
- package/templates/mac/app-update.yml +4 -0
- package/templates/win/nsis/README.md +51 -0
- package/templates/win/nsis/THIRD-PARTY-NOTICES.md +20 -0
- package/templates/win/nsis/assistedInstaller.nsh +154 -0
- package/templates/win/nsis/assistedMessages.yml +368 -0
- package/templates/win/nsis/common.nsh +147 -0
- package/templates/win/nsis/empty-license.txt +1 -0
- package/templates/win/nsis/include/FileAssociation.nsh +123 -0
- package/templates/win/nsis/include/StdUtils.nsh +496 -0
- package/templates/win/nsis/include/StrContains.nsh +48 -0
- package/templates/win/nsis/include/UAC.nsh +299 -0
- package/templates/win/nsis/include/allowOnlyOneInstallerInstance.nsh +164 -0
- package/templates/win/nsis/include/extractAppPackage.nsh +138 -0
- package/templates/win/nsis/include/getProcessInfo.nsh +157 -0
- package/templates/win/nsis/include/installUtil.nsh +248 -0
- package/templates/win/nsis/include/installer.nsh +247 -0
- package/templates/win/nsis/include/nsProcess.nsh +28 -0
- package/templates/win/nsis/include/webPackage.nsh +64 -0
- package/templates/win/nsis/installSection.nsh +110 -0
- package/templates/win/nsis/installer.nsi +132 -0
- package/templates/win/nsis/messages.yml +240 -0
- package/templates/win/nsis/multiUser.nsh +134 -0
- package/templates/win/nsis/multiUserUi.nsh +230 -0
- package/templates/win/nsis/oneClick.nsh +19 -0
- package/templates/win/nsis/portable.nsi +91 -0
- package/templates/win/nsis/uninstaller.nsh +263 -0
- package/templates/win/nsis-header.nsh +57 -0
|
@@ -0,0 +1,723 @@
|
|
|
1
|
+
import crypto from 'node:crypto'
|
|
2
|
+
import { createReadStream, createWriteStream, existsSync } from 'node:fs'
|
|
3
|
+
import {
|
|
4
|
+
access,
|
|
5
|
+
mkdir as mkdirAsync,
|
|
6
|
+
readFile as readFileAsync,
|
|
7
|
+
readlink as readlinkAsync,
|
|
8
|
+
realpath as realpathAsync,
|
|
9
|
+
writeFile as writeFileAsync,
|
|
10
|
+
} from 'node:fs/promises'
|
|
11
|
+
import os from 'node:os'
|
|
12
|
+
import path from 'node:path'
|
|
13
|
+
import { pipeline } from 'node:stream/promises'
|
|
14
|
+
import { EntryKind, type FileFilter, type FileSet, type FileStat } from '../../shared/types.ts'
|
|
15
|
+
import { detectUnpackedDirs } from '../fileset/unpack-detector.ts'
|
|
16
|
+
import { mapWithConcurrency } from './concurrency.ts'
|
|
17
|
+
|
|
18
|
+
const SYSTEM_DENYLIST = [
|
|
19
|
+
'/usr',
|
|
20
|
+
'/lib',
|
|
21
|
+
'/bin',
|
|
22
|
+
'/sbin',
|
|
23
|
+
'/etc',
|
|
24
|
+
'/tmp',
|
|
25
|
+
'/var',
|
|
26
|
+
'/System',
|
|
27
|
+
'/Library',
|
|
28
|
+
'/private',
|
|
29
|
+
]
|
|
30
|
+
const SMALL_FILE_THRESHOLD = Number(
|
|
31
|
+
process.env.CROSS_PACKER_ASAR_SMALL_FILE_THRESHOLD ?? 512 * 1024,
|
|
32
|
+
)
|
|
33
|
+
const INTEGRITY_CONCURRENCY = Number(
|
|
34
|
+
process.env.CROSS_PACKER_ASAR_INTEGRITY_CONCURRENCY ?? Math.max(4, os.cpus().length),
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
/** A fileSet plus transformed contents collected during packing. */
|
|
38
|
+
interface PackedFileSet extends FileSet {
|
|
39
|
+
transformedFiles?: Map<number, string>
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
interface IntegrityInfo {
|
|
43
|
+
algorithm: 'SHA256'
|
|
44
|
+
hash: string
|
|
45
|
+
blockSize: number
|
|
46
|
+
blocks: string[]
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
interface HeaderDirNode {
|
|
50
|
+
files: Record<string, HeaderNode>
|
|
51
|
+
unpacked?: boolean
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
interface HeaderFileNode {
|
|
55
|
+
size: number
|
|
56
|
+
offset?: string
|
|
57
|
+
unpacked?: boolean
|
|
58
|
+
integrity?: IntegrityInfo
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
interface HeaderLinkNode {
|
|
62
|
+
link: string
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
type HeaderNode = HeaderDirNode | HeaderFileNode | HeaderLinkNode
|
|
66
|
+
|
|
67
|
+
type DirectoryStream = { type: EntryKind.DIRECTORY; path: string; unpacked: boolean }
|
|
68
|
+
type FileStream = {
|
|
69
|
+
type: EntryKind.FILE
|
|
70
|
+
path: string
|
|
71
|
+
unpacked: boolean
|
|
72
|
+
file?: string
|
|
73
|
+
data?: Buffer
|
|
74
|
+
stat: FileStat
|
|
75
|
+
}
|
|
76
|
+
type LinkStream = {
|
|
77
|
+
type: EntryKind.LINK
|
|
78
|
+
path: string
|
|
79
|
+
unpacked: boolean
|
|
80
|
+
file: string
|
|
81
|
+
symlink: string
|
|
82
|
+
stat: FileStat
|
|
83
|
+
}
|
|
84
|
+
type FileStreamEntry = DirectoryStream | FileStream | LinkStream
|
|
85
|
+
|
|
86
|
+
interface DataEntry {
|
|
87
|
+
type: 'buffer' | 'file'
|
|
88
|
+
data?: Buffer
|
|
89
|
+
path?: string
|
|
90
|
+
size: number
|
|
91
|
+
pending?: boolean
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
interface PendingFileIntegrity {
|
|
95
|
+
fileEntry: HeaderFileNode
|
|
96
|
+
dataEntry: DataEntry
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// Module-level cache for the resolved workspace root, keyed by its raw path.
|
|
100
|
+
let cachedWorkspaceRootPath: string | null | undefined = null
|
|
101
|
+
let cachedResolvedWorkspaceRoot: string | undefined
|
|
102
|
+
|
|
103
|
+
const DENYLIST: Promise<string[]> = resolvePaths([
|
|
104
|
+
...SYSTEM_DENYLIST,
|
|
105
|
+
process.env.SystemRoot,
|
|
106
|
+
process.env.WINDIR,
|
|
107
|
+
])
|
|
108
|
+
|
|
109
|
+
const ALLOWLIST: Promise<string[]> = resolvePaths([os.tmpdir(), os.homedir()])
|
|
110
|
+
|
|
111
|
+
export interface PackAsarOptions {
|
|
112
|
+
outputPath: string
|
|
113
|
+
unpackPattern?: FileFilter
|
|
114
|
+
smartUnpack?: boolean
|
|
115
|
+
unpackDirs?: string[]
|
|
116
|
+
defaultDestination?: string
|
|
117
|
+
workspaceRoot?: string
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
export async function packAsarFromFileSets(
|
|
121
|
+
fileSets: PackedFileSet[],
|
|
122
|
+
options: PackAsarOptions,
|
|
123
|
+
): Promise<{ asarPath: string; unpackedDir: string | undefined }> {
|
|
124
|
+
const {
|
|
125
|
+
outputPath,
|
|
126
|
+
unpackPattern,
|
|
127
|
+
smartUnpack = true,
|
|
128
|
+
unpackDirs = [],
|
|
129
|
+
defaultDestination,
|
|
130
|
+
workspaceRoot,
|
|
131
|
+
} = options
|
|
132
|
+
|
|
133
|
+
const autoUnpackDirs = new Set<string>(unpackDirs)
|
|
134
|
+
if (smartUnpack) {
|
|
135
|
+
for (const fileSet of fileSets) {
|
|
136
|
+
detectUnpackedDirs({ files: fileSet.files, metadata: fileSet.metadata }, autoUnpackDirs)
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
const normalizedUnpackedPaths = Array.from(autoUnpackDirs).map((p) =>
|
|
141
|
+
defaultDestination ? path.normalize(path.relative(defaultDestination, p)) : path.normalize(p),
|
|
142
|
+
)
|
|
143
|
+
const streams = await buildStreamsFromSets(
|
|
144
|
+
fileSets,
|
|
145
|
+
normalizedUnpackedPaths,
|
|
146
|
+
unpackPattern,
|
|
147
|
+
defaultDestination,
|
|
148
|
+
workspaceRoot,
|
|
149
|
+
)
|
|
150
|
+
|
|
151
|
+
await executeElectronAsar(outputPath, streams)
|
|
152
|
+
|
|
153
|
+
const unpackedDir = existsSync(`${outputPath}.unpacked`) ? `${outputPath}.unpacked` : undefined
|
|
154
|
+
return { asarPath: outputPath, unpackedDir }
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function getDestinationPath(file: string, fileSet: { src: string; destination: string }): string {
|
|
158
|
+
if (file === fileSet.src) return fileSet.destination
|
|
159
|
+
if (file.startsWith(fileSet.src)) {
|
|
160
|
+
return path.join(fileSet.destination, path.relative(fileSet.src, file))
|
|
161
|
+
}
|
|
162
|
+
return fileSet.destination
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
async function buildStreamsFromSets(
|
|
166
|
+
fileSets: PackedFileSet[],
|
|
167
|
+
normalizedUnpackedPaths: string[],
|
|
168
|
+
unpackPattern: FileFilter | undefined,
|
|
169
|
+
defaultDestination: string | undefined,
|
|
170
|
+
workspaceRoot: string | undefined,
|
|
171
|
+
): Promise<FileStreamEntry[]> {
|
|
172
|
+
const orderedFileSets = [...fileSets.slice(1), fileSets[0]].map((set) => orderFileSetInSet(set))
|
|
173
|
+
|
|
174
|
+
const resultsMap = new Map<string, FileStreamEntry>()
|
|
175
|
+
const streamOrdering: string[] = []
|
|
176
|
+
|
|
177
|
+
// Check whether a destination file should be packed outside the asar.
|
|
178
|
+
const isUnpacked = (
|
|
179
|
+
destination: string,
|
|
180
|
+
file: string | undefined,
|
|
181
|
+
stat: FileStat | undefined,
|
|
182
|
+
): boolean => {
|
|
183
|
+
const normalizedDir = path.normalize(destination)
|
|
184
|
+
|
|
185
|
+
if (
|
|
186
|
+
file != null &&
|
|
187
|
+
!isEmptyOrSpaces(file) &&
|
|
188
|
+
stat &&
|
|
189
|
+
stat.type === EntryKind.FILE &&
|
|
190
|
+
typeof unpackPattern === 'function' &&
|
|
191
|
+
unpackPattern(file, stat)
|
|
192
|
+
) {
|
|
193
|
+
return true
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
for (const unpackedPath of normalizedUnpackedPaths) {
|
|
197
|
+
if (normalizedDir === unpackedPath || normalizedDir.startsWith(unpackedPath + path.sep)) {
|
|
198
|
+
return true
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
return false
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
for (const fileSet of orderedFileSets) {
|
|
205
|
+
const { files, metadata, transformedFiles, src, destination } = fileSet
|
|
206
|
+
const orderedFiles = files
|
|
207
|
+
|
|
208
|
+
for (let index = 0; index < orderedFiles.length; index++) {
|
|
209
|
+
const file = orderedFiles[index]
|
|
210
|
+
const stat = metadata.get(file)
|
|
211
|
+
if (!stat) continue
|
|
212
|
+
|
|
213
|
+
const dest = getDestinationPath(file, { src, destination })
|
|
214
|
+
const relativeDest = defaultDestination
|
|
215
|
+
? path.relative(defaultDestination, dest)
|
|
216
|
+
: path.relative(destination, dest)
|
|
217
|
+
|
|
218
|
+
ensureParentDirectories(relativeDest, resultsMap, streamOrdering)
|
|
219
|
+
|
|
220
|
+
const transformedData = transformedFiles ? transformedFiles.get(index) : undefined
|
|
221
|
+
|
|
222
|
+
const result = await processFileOrSymlink({
|
|
223
|
+
file,
|
|
224
|
+
destination: relativeDest,
|
|
225
|
+
stat,
|
|
226
|
+
isUnpacked,
|
|
227
|
+
workspaceRoot,
|
|
228
|
+
transformedData,
|
|
229
|
+
})
|
|
230
|
+
|
|
231
|
+
if (result && !resultsMap.has(result.path)) {
|
|
232
|
+
resultsMap.set(result.path, result)
|
|
233
|
+
streamOrdering.push(result.path)
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
for (const entry of resultsMap.values()) {
|
|
239
|
+
if (entry.unpacked) {
|
|
240
|
+
markParentDirectoriesAsUnpacked(entry.path, resultsMap, isUnpacked)
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
return streamOrdering.reduce((streams: FileStreamEntry[], p) => {
|
|
245
|
+
const stream = resultsMap.get(p)
|
|
246
|
+
if (stream != null) {
|
|
247
|
+
streams.push(stream)
|
|
248
|
+
}
|
|
249
|
+
return streams
|
|
250
|
+
}, [])
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
function orderFileSetInSet(fileSet: PackedFileSet): PackedFileSet {
|
|
254
|
+
const sortedFileEntries = Array.from(fileSet.files.entries())
|
|
255
|
+
sortedFileEntries.sort(([, a], [, b]) => {
|
|
256
|
+
if (a === b) return 0
|
|
257
|
+
const isAAddon = a.endsWith('.node')
|
|
258
|
+
const isBAddon = b.endsWith('.node')
|
|
259
|
+
if (isAAddon && !isBAddon) return 1
|
|
260
|
+
if (isBAddon && !isAAddon) return -1
|
|
261
|
+
return a < b ? -1 : 1
|
|
262
|
+
})
|
|
263
|
+
|
|
264
|
+
let transformedFiles: Map<number, string> | undefined
|
|
265
|
+
if (fileSet.transformedFiles) {
|
|
266
|
+
transformedFiles = new Map()
|
|
267
|
+
const indexMap = new Map<number, number>()
|
|
268
|
+
for (const [newIndex, [oldIndex]] of sortedFileEntries.entries()) {
|
|
269
|
+
indexMap.set(oldIndex, newIndex)
|
|
270
|
+
}
|
|
271
|
+
for (const [oldIndex, value] of fileSet.transformedFiles) {
|
|
272
|
+
const newIndex = indexMap.get(oldIndex)
|
|
273
|
+
if (newIndex === undefined) {
|
|
274
|
+
throw new Error(`Internal error: ${fileSet.files[oldIndex]} was lost while ordering asar`)
|
|
275
|
+
}
|
|
276
|
+
transformedFiles.set(newIndex, value)
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
return {
|
|
281
|
+
src: fileSet.src,
|
|
282
|
+
destination: fileSet.destination,
|
|
283
|
+
metadata: fileSet.metadata,
|
|
284
|
+
files: sortedFileEntries.map(([, file]) => file),
|
|
285
|
+
transformedFiles,
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
async function processFileOrSymlink({
|
|
290
|
+
file,
|
|
291
|
+
destination,
|
|
292
|
+
stat,
|
|
293
|
+
isUnpacked,
|
|
294
|
+
workspaceRoot,
|
|
295
|
+
transformedData,
|
|
296
|
+
}: {
|
|
297
|
+
file: string
|
|
298
|
+
destination: string
|
|
299
|
+
stat: FileStat
|
|
300
|
+
isUnpacked: (destination: string, file: string | undefined, stat: FileStat | undefined) => boolean
|
|
301
|
+
workspaceRoot: string | undefined
|
|
302
|
+
transformedData?: string
|
|
303
|
+
}): Promise<FileStreamEntry | undefined> {
|
|
304
|
+
const unpacked = isUnpacked(destination, file, stat)
|
|
305
|
+
|
|
306
|
+
if (stat.type !== EntryKind.FILE && stat.type !== EntryKind.LINK) {
|
|
307
|
+
return { path: destination, unpacked, type: EntryKind.DIRECTORY }
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
if (transformedData != null) {
|
|
311
|
+
const buf = Buffer.from(transformedData, 'utf8')
|
|
312
|
+
const size = buf.length
|
|
313
|
+
return {
|
|
314
|
+
path: destination,
|
|
315
|
+
data: buf,
|
|
316
|
+
unpacked,
|
|
317
|
+
type: EntryKind.FILE,
|
|
318
|
+
stat: { type: EntryKind.FILE, mode: stat.mode, size },
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
await protectSystemAndUnsafePaths(file, workspaceRoot, stat)
|
|
323
|
+
|
|
324
|
+
if (stat.type !== EntryKind.LINK) {
|
|
325
|
+
const realFile = stat.realFilePath || file
|
|
326
|
+
return {
|
|
327
|
+
path: destination,
|
|
328
|
+
file: realFile,
|
|
329
|
+
unpacked,
|
|
330
|
+
type: EntryKind.FILE,
|
|
331
|
+
stat,
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
let link: string
|
|
336
|
+
try {
|
|
337
|
+
link = await readlinkAsync(file)
|
|
338
|
+
} catch (e) {
|
|
339
|
+
throw new Error(`Cannot read symlink "${file}": ${e instanceof Error ? e.message : String(e)}`)
|
|
340
|
+
}
|
|
341
|
+
if (path.isAbsolute(link)) {
|
|
342
|
+
link = path.relative(path.dirname(file), link)
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
return {
|
|
346
|
+
path: destination,
|
|
347
|
+
file,
|
|
348
|
+
unpacked,
|
|
349
|
+
type: EntryKind.LINK,
|
|
350
|
+
symlink: link,
|
|
351
|
+
stat,
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
function ensureParentDirectories(
|
|
356
|
+
destination: string,
|
|
357
|
+
resultsMap: Map<string, FileStreamEntry>,
|
|
358
|
+
streamOrdering: string[],
|
|
359
|
+
): void {
|
|
360
|
+
const parents: string[] = []
|
|
361
|
+
let current = path.dirname(path.normalize(destination))
|
|
362
|
+
|
|
363
|
+
while (current !== '.') {
|
|
364
|
+
parents.unshift(current)
|
|
365
|
+
current = path.dirname(current)
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
for (const parentPath of parents) {
|
|
369
|
+
if (!resultsMap.has(parentPath)) {
|
|
370
|
+
const dir: DirectoryStream = {
|
|
371
|
+
type: EntryKind.DIRECTORY,
|
|
372
|
+
path: parentPath,
|
|
373
|
+
unpacked: false,
|
|
374
|
+
}
|
|
375
|
+
resultsMap.set(parentPath, dir)
|
|
376
|
+
streamOrdering.push(parentPath)
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
function markParentDirectoriesAsUnpacked(
|
|
382
|
+
destination: string,
|
|
383
|
+
resultsMap: Map<string, FileStreamEntry>,
|
|
384
|
+
isUnpacked: (destination: string, file?: string, stat?: FileStat) => boolean,
|
|
385
|
+
): void {
|
|
386
|
+
let current = path.dirname(path.normalize(destination))
|
|
387
|
+
while (current !== '.') {
|
|
388
|
+
const entry = resultsMap.get(current)
|
|
389
|
+
if (entry && isUnpacked(current)) {
|
|
390
|
+
entry.unpacked = true
|
|
391
|
+
}
|
|
392
|
+
current = path.dirname(current)
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
async function executeElectronAsar(outFile: string, streams: FileStreamEntry[]): Promise<void> {
|
|
397
|
+
await mkdirAsync(path.dirname(outFile), { recursive: true })
|
|
398
|
+
|
|
399
|
+
const header: HeaderDirNode = { files: Object.create(null) }
|
|
400
|
+
let dataOffset = BigInt(0)
|
|
401
|
+
const dataEntries: DataEntry[] = []
|
|
402
|
+
const pendingFileIntegrities: PendingFileIntegrity[] = []
|
|
403
|
+
const unpackedDir = `${outFile}.unpacked`
|
|
404
|
+
const unpackedStreams: { stream: FileStream; segments: string[] }[] = []
|
|
405
|
+
|
|
406
|
+
for (const stream of streams) {
|
|
407
|
+
const filename = path.normalize(stream.path)
|
|
408
|
+
const segments = filename.split(/[/\\]/).filter(Boolean)
|
|
409
|
+
|
|
410
|
+
if (stream.type === EntryKind.DIRECTORY) {
|
|
411
|
+
let node: HeaderDirNode = header
|
|
412
|
+
for (const seg of segments) {
|
|
413
|
+
const existing = node.files[seg]
|
|
414
|
+
if (!existing || !isDirNode(existing)) {
|
|
415
|
+
node.files[seg] = { files: Object.create(null) }
|
|
416
|
+
}
|
|
417
|
+
const next = node.files[seg]
|
|
418
|
+
if (isDirNode(next)) node = next
|
|
419
|
+
}
|
|
420
|
+
if (stream.unpacked) {
|
|
421
|
+
node.unpacked = true
|
|
422
|
+
}
|
|
423
|
+
continue
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
let node: HeaderDirNode = header
|
|
427
|
+
for (let i = 0; i < segments.length - 1; i++) {
|
|
428
|
+
const seg = segments[i]
|
|
429
|
+
const existing = node.files[seg]
|
|
430
|
+
if (!existing || !isDirNode(existing)) {
|
|
431
|
+
node.files[seg] = { files: Object.create(null) }
|
|
432
|
+
}
|
|
433
|
+
const next = node.files[seg]
|
|
434
|
+
if (isDirNode(next)) node = next
|
|
435
|
+
}
|
|
436
|
+
const name = segments[segments.length - 1]
|
|
437
|
+
|
|
438
|
+
if (stream.type === EntryKind.LINK) {
|
|
439
|
+
node.files[name] = { link: stream.symlink }
|
|
440
|
+
continue
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
const size = stream.stat.size
|
|
444
|
+
|
|
445
|
+
if (stream.unpacked) {
|
|
446
|
+
node.files[name] = { size, unpacked: true }
|
|
447
|
+
unpackedStreams.push({ stream, segments })
|
|
448
|
+
continue
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
const offsetStr = dataOffset.toString()
|
|
452
|
+
dataOffset += BigInt(size)
|
|
453
|
+
|
|
454
|
+
const fileEntry: HeaderFileNode = { size, offset: offsetStr }
|
|
455
|
+
|
|
456
|
+
if (stream.data) {
|
|
457
|
+
const integrity = computeIntegrityFromBuffer(stream.data)
|
|
458
|
+
fileEntry.integrity = integrity
|
|
459
|
+
dataEntries.push({ type: 'buffer', data: stream.data, size })
|
|
460
|
+
} else {
|
|
461
|
+
const realFile = stream.stat.realFilePath || stream.file || stream.path
|
|
462
|
+
const dataEntry: DataEntry = { type: 'file', path: realFile, size, pending: true }
|
|
463
|
+
pendingFileIntegrities.push({ fileEntry, dataEntry })
|
|
464
|
+
dataEntries.push(dataEntry)
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
node.files[name] = fileEntry
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
await assignFileIntegrities(pendingFileIntegrities)
|
|
471
|
+
|
|
472
|
+
const headerJson = JSON.stringify(header)
|
|
473
|
+
const headerBuf = createAsarHeader(headerJson)
|
|
474
|
+
|
|
475
|
+
const out = createWriteStream(outFile)
|
|
476
|
+
out.setMaxListeners(0)
|
|
477
|
+
await new Promise<void>((resolve, reject) => {
|
|
478
|
+
out.on('error', reject)
|
|
479
|
+
out.write(headerBuf, () => resolve())
|
|
480
|
+
})
|
|
481
|
+
|
|
482
|
+
for (const entry of dataEntries) {
|
|
483
|
+
if (entry.data) {
|
|
484
|
+
await new Promise<void>((resolve, reject) => {
|
|
485
|
+
out.on('error', reject)
|
|
486
|
+
out.write(entry.data, () => resolve())
|
|
487
|
+
})
|
|
488
|
+
} else if (entry.path) {
|
|
489
|
+
const readStream = createReadStream(entry.path)
|
|
490
|
+
await pipeline(readStream, out, { end: false })
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
await new Promise<void>((resolve, reject) => {
|
|
495
|
+
out.on('error', reject)
|
|
496
|
+
out.on('close', () => resolve())
|
|
497
|
+
out.end()
|
|
498
|
+
})
|
|
499
|
+
|
|
500
|
+
if (unpackedStreams.length > 0) {
|
|
501
|
+
await mkdirAsync(unpackedDir, { recursive: true })
|
|
502
|
+
await mapWithConcurrency(
|
|
503
|
+
unpackedStreams,
|
|
504
|
+
INTEGRITY_CONCURRENCY,
|
|
505
|
+
async ({ stream, segments }) => {
|
|
506
|
+
const destPath = path.join(unpackedDir, ...segments)
|
|
507
|
+
await mkdirAsync(path.dirname(destPath), { recursive: true })
|
|
508
|
+
if (stream.data) {
|
|
509
|
+
await writeFileAsync(destPath, stream.data)
|
|
510
|
+
return
|
|
511
|
+
}
|
|
512
|
+
const realFile = stream.stat.realFilePath || stream.file || stream.path
|
|
513
|
+
const srcStream = createReadStream(realFile)
|
|
514
|
+
const destStream = createWriteStream(destPath)
|
|
515
|
+
await pipeline(srcStream, destStream)
|
|
516
|
+
},
|
|
517
|
+
)
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
function isDirNode(node: HeaderNode): node is HeaderDirNode {
|
|
522
|
+
return 'files' in node && node.files !== undefined
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
async function assignFileIntegrities(
|
|
526
|
+
pendingFileIntegrities: PendingFileIntegrity[],
|
|
527
|
+
): Promise<void> {
|
|
528
|
+
if (pendingFileIntegrities.length === 0) {
|
|
529
|
+
return
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
const smallFiles = pendingFileIntegrities.filter(
|
|
533
|
+
(item) => item.dataEntry.size <= SMALL_FILE_THRESHOLD,
|
|
534
|
+
)
|
|
535
|
+
const largeFiles = pendingFileIntegrities.filter(
|
|
536
|
+
(item) => item.dataEntry.size > SMALL_FILE_THRESHOLD,
|
|
537
|
+
)
|
|
538
|
+
|
|
539
|
+
await mapWithConcurrency(smallFiles, INTEGRITY_CONCURRENCY, async (item) => {
|
|
540
|
+
const data = await readFileAsync(requireDataPath(item.dataEntry))
|
|
541
|
+
item.fileEntry.integrity = computeIntegrityFromBuffer(data)
|
|
542
|
+
item.dataEntry.data = data
|
|
543
|
+
item.dataEntry.pending = false
|
|
544
|
+
})
|
|
545
|
+
|
|
546
|
+
await mapWithConcurrency(largeFiles, INTEGRITY_CONCURRENCY, async (item) => {
|
|
547
|
+
item.fileEntry.integrity = await computeIntegrityFromFile(requireDataPath(item.dataEntry))
|
|
548
|
+
item.dataEntry.pending = false
|
|
549
|
+
})
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
/** Assert that a pending file data entry carries its source path. */
|
|
553
|
+
function requireDataPath(entry: DataEntry): string {
|
|
554
|
+
if (!entry.path) {
|
|
555
|
+
throw new Error(`Internal error: file data entry missing path (size=${entry.size})`)
|
|
556
|
+
}
|
|
557
|
+
return entry.path
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
function createAsarHeader(headerJson: string): Buffer {
|
|
561
|
+
const headerData = Buffer.from(headerJson, 'utf8')
|
|
562
|
+
const headerDataAligned = alignInt(headerData.length, 4)
|
|
563
|
+
const headerPayloadSize = 4 + headerDataAligned
|
|
564
|
+
const headerPickleSize = 4 + headerPayloadSize
|
|
565
|
+
const headerPickleBuf = Buffer.alloc(headerPickleSize)
|
|
566
|
+
headerPickleBuf.writeUInt32LE(headerPayloadSize, 0)
|
|
567
|
+
headerPickleBuf.writeUInt32LE(headerData.length, 4)
|
|
568
|
+
headerPickleBuf.fill(0, 8, 8 + headerDataAligned)
|
|
569
|
+
headerData.copy(headerPickleBuf, 8)
|
|
570
|
+
|
|
571
|
+
const sizePickleBuf = Buffer.alloc(8)
|
|
572
|
+
sizePickleBuf.writeUInt32LE(4, 0)
|
|
573
|
+
sizePickleBuf.writeUInt32LE(headerPickleBuf.length, 4)
|
|
574
|
+
|
|
575
|
+
return Buffer.concat([sizePickleBuf, headerPickleBuf])
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
function alignInt(i: number, alignment: number): number {
|
|
579
|
+
return i + ((alignment - (i % alignment)) % alignment)
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
function computeIntegrityFromBuffer(buf: Buffer): IntegrityInfo {
|
|
583
|
+
const blockSize = 4194304
|
|
584
|
+
const hash = crypto.createHash('sha256').update(buf).digest('hex')
|
|
585
|
+
const blocks: string[] = []
|
|
586
|
+
for (let i = 0; i < buf.length; i += blockSize) {
|
|
587
|
+
const chunk = buf.slice(i, Math.min(i + blockSize, buf.length))
|
|
588
|
+
blocks.push(crypto.createHash('sha256').update(chunk).digest('hex'))
|
|
589
|
+
}
|
|
590
|
+
return {
|
|
591
|
+
algorithm: 'SHA256',
|
|
592
|
+
hash,
|
|
593
|
+
blockSize,
|
|
594
|
+
blocks,
|
|
595
|
+
}
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
async function computeIntegrityFromFile(filePath: string): Promise<IntegrityInfo> {
|
|
599
|
+
const blockSize = 4194304
|
|
600
|
+
const blocks: string[] = []
|
|
601
|
+
return new Promise((resolve, reject) => {
|
|
602
|
+
const hash = crypto.createHash('sha256')
|
|
603
|
+
let blockHash = crypto.createHash('sha256')
|
|
604
|
+
let blockOffset = 0
|
|
605
|
+
const stream = createReadStream(filePath)
|
|
606
|
+
stream.on('data', (chunk) => {
|
|
607
|
+
const buf = typeof chunk === 'string' ? Buffer.from(chunk) : chunk
|
|
608
|
+
hash.update(buf)
|
|
609
|
+
let offset = 0
|
|
610
|
+
while (offset < buf.length) {
|
|
611
|
+
const remaining = blockSize - blockOffset
|
|
612
|
+
const end = Math.min(offset + remaining, buf.length)
|
|
613
|
+
blockHash.update(buf.slice(offset, end))
|
|
614
|
+
blockOffset += end - offset
|
|
615
|
+
offset = end
|
|
616
|
+
if (blockOffset >= blockSize) {
|
|
617
|
+
blocks.push(blockHash.digest('hex'))
|
|
618
|
+
blockHash = crypto.createHash('sha256')
|
|
619
|
+
blockOffset = 0
|
|
620
|
+
}
|
|
621
|
+
}
|
|
622
|
+
})
|
|
623
|
+
stream.on('end', () => {
|
|
624
|
+
if (blockOffset > 0) {
|
|
625
|
+
blocks.push(blockHash.digest('hex'))
|
|
626
|
+
}
|
|
627
|
+
resolve({
|
|
628
|
+
algorithm: 'SHA256',
|
|
629
|
+
hash: hash.digest('hex'),
|
|
630
|
+
blockSize,
|
|
631
|
+
blocks,
|
|
632
|
+
})
|
|
633
|
+
})
|
|
634
|
+
stream.on('error', reject)
|
|
635
|
+
})
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
async function protectSystemAndUnsafePaths(
|
|
639
|
+
file: string,
|
|
640
|
+
workspaceRoot: string | undefined,
|
|
641
|
+
stat: FileStat,
|
|
642
|
+
): Promise<void> {
|
|
643
|
+
if (stat.type !== EntryKind.LINK && isPathInsideWorkspace(file, workspaceRoot)) {
|
|
644
|
+
return
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
const resolved = await resolvePath(file)
|
|
648
|
+
if (resolved == null || isEmptyOrSpaces(resolved)) {
|
|
649
|
+
return
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
const workspace = await getResolvedWorkspaceRoot(workspaceRoot)
|
|
653
|
+
if (workspace && (resolved === workspace || resolved.startsWith(workspace + path.sep))) {
|
|
654
|
+
return
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
if (await checkAgainstRoots(file, await ALLOWLIST)) {
|
|
658
|
+
return
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
if (await checkAgainstRoots(file, await DENYLIST)) {
|
|
662
|
+
throw new Error(
|
|
663
|
+
`Cannot copy file [${file}] symlinked to file [${resolved}] outside the package to a system or unsafe path`,
|
|
664
|
+
)
|
|
665
|
+
}
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
function isPathInsideWorkspace(file: string, workspaceRoot: string | undefined): boolean {
|
|
669
|
+
if (!workspaceRoot) {
|
|
670
|
+
return false
|
|
671
|
+
}
|
|
672
|
+
const normalizedFile = path.normalize(file)
|
|
673
|
+
const normalizedWorkspace = path.normalize(workspaceRoot)
|
|
674
|
+
return (
|
|
675
|
+
normalizedFile === normalizedWorkspace ||
|
|
676
|
+
normalizedFile.startsWith(normalizedWorkspace + path.sep)
|
|
677
|
+
)
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
async function getResolvedWorkspaceRoot(
|
|
681
|
+
workspaceRoot: string | undefined,
|
|
682
|
+
): Promise<string | undefined> {
|
|
683
|
+
if (cachedWorkspaceRootPath !== workspaceRoot) {
|
|
684
|
+
cachedWorkspaceRootPath = workspaceRoot ?? null
|
|
685
|
+
cachedResolvedWorkspaceRoot = workspaceRoot ? await resolvePath(workspaceRoot) : undefined
|
|
686
|
+
}
|
|
687
|
+
return cachedResolvedWorkspaceRoot
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
async function resolvePath(target: string | undefined | null): Promise<string | undefined> {
|
|
691
|
+
if (!target) return undefined
|
|
692
|
+
try {
|
|
693
|
+
const exists = await access(target)
|
|
694
|
+
.then(() => true)
|
|
695
|
+
.catch(() => false)
|
|
696
|
+
if (!exists) return undefined
|
|
697
|
+
return await realpathAsync(target)
|
|
698
|
+
} catch {
|
|
699
|
+
return path.resolve(target)
|
|
700
|
+
}
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
async function checkAgainstRoots(target: string, roots: string[]): Promise<boolean> {
|
|
704
|
+
const resolved = await resolvePath(target)
|
|
705
|
+
if (resolved == null || isEmptyOrSpaces(resolved)) {
|
|
706
|
+
return false
|
|
707
|
+
}
|
|
708
|
+
for (const root of roots) {
|
|
709
|
+
if (resolved === root || resolved.startsWith(root + path.sep)) {
|
|
710
|
+
return true
|
|
711
|
+
}
|
|
712
|
+
}
|
|
713
|
+
return false
|
|
714
|
+
}
|
|
715
|
+
|
|
716
|
+
async function resolvePaths(filepaths: (string | undefined)[]): Promise<string[]> {
|
|
717
|
+
const results = await Promise.all(filepaths.map(resolvePath))
|
|
718
|
+
return results.filter((it): it is string => it != null)
|
|
719
|
+
}
|
|
720
|
+
|
|
721
|
+
function isEmptyOrSpaces(s: string | undefined | null): boolean {
|
|
722
|
+
return s == null || s.trim().length === 0
|
|
723
|
+
}
|