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.
Files changed (126) hide show
  1. package/LICENSE +661 -0
  2. package/README.md +151 -0
  3. package/README.zh-CN.md +151 -0
  4. package/dist/archive/ar.d.ts +12 -0
  5. package/dist/archive/seven-za.d.ts +32 -0
  6. package/dist/archive/tar.d.ts +30 -0
  7. package/dist/archive/zip-reader.d.ts +17 -0
  8. package/dist/asar/dependency/collect.d.ts +2 -0
  9. package/dist/asar/dependency/detector.d.ts +8 -0
  10. package/dist/asar/dependency/hoister.d.ts +27 -0
  11. package/dist/asar/dependency/utils.d.ts +59 -0
  12. package/dist/asar/fileset/matcher.d.ts +12 -0
  13. package/dist/asar/fileset/platform-fileset.d.ts +36 -0
  14. package/dist/asar/fileset/transformer.d.ts +2 -0
  15. package/dist/asar/fileset/unpack-detector.d.ts +10 -0
  16. package/dist/asar/fileset/walker.d.ts +14 -0
  17. package/dist/asar/pack.d.ts +23 -0
  18. package/dist/asar/packer/app-collector.d.ts +2 -0
  19. package/dist/asar/packer/collector.d.ts +18 -0
  20. package/dist/asar/packer/concurrency.d.ts +1 -0
  21. package/dist/asar/packer/integrity.d.ts +8 -0
  22. package/dist/asar/packer/nm-collector.d.ts +7 -0
  23. package/dist/asar/packer/stream-builder.d.ts +18 -0
  24. package/dist/cli.d.ts +2 -0
  25. package/dist/config/index.d.ts +97 -0
  26. package/dist/electron/dist-zip.d.ts +18 -0
  27. package/dist/electron/download.d.ts +21 -0
  28. package/dist/index.d.ts +11 -0
  29. package/dist/packer/common/app-update-yml.d.ts +5 -0
  30. package/dist/packer/common/extra-resources.d.ts +39 -0
  31. package/dist/packer/common/staging.d.ts +3 -0
  32. package/dist/packer/common/template.d.ts +2 -0
  33. package/dist/packer/linux/linux.d.ts +2 -0
  34. package/dist/packer/mac/mac.d.ts +23 -0
  35. package/dist/packer/win/nsis.d.ts +24 -0
  36. package/dist/packer/win/resedit.d.ts +5 -0
  37. package/dist/packer/win/toolchain.d.ts +8 -0
  38. package/dist/packer/win/win.d.ts +12 -0
  39. package/dist/pipeline/pipeline.d.ts +41 -0
  40. package/dist/pipeline/targets.d.ts +28 -0
  41. package/dist/shared/hash.d.ts +2 -0
  42. package/dist/shared/logger.d.ts +39 -0
  43. package/dist/shared/platform.d.ts +26 -0
  44. package/dist/shared/types.d.ts +68 -0
  45. package/dist/update/blockmap.d.ts +13 -0
  46. package/dist/update/metadata.d.ts +20 -0
  47. package/dist/update/version.d.ts +7 -0
  48. package/package.json +71 -0
  49. package/src/archive/ar.ts +41 -0
  50. package/src/archive/seven-za.ts +160 -0
  51. package/src/archive/tar.ts +142 -0
  52. package/src/archive/zip-reader.ts +61 -0
  53. package/src/asar/dependency/collect.ts +437 -0
  54. package/src/asar/dependency/detector.ts +73 -0
  55. package/src/asar/dependency/hoister.ts +714 -0
  56. package/src/asar/dependency/utils.ts +225 -0
  57. package/src/asar/fileset/matcher.ts +101 -0
  58. package/src/asar/fileset/platform-fileset.ts +416 -0
  59. package/src/asar/fileset/transformer.ts +154 -0
  60. package/src/asar/fileset/unpack-detector.ts +38 -0
  61. package/src/asar/fileset/walker.ts +115 -0
  62. package/src/asar/pack.ts +186 -0
  63. package/src/asar/packer/app-collector.ts +74 -0
  64. package/src/asar/packer/collector.ts +134 -0
  65. package/src/asar/packer/concurrency.ts +24 -0
  66. package/src/asar/packer/integrity.ts +53 -0
  67. package/src/asar/packer/nm-collector.ts +194 -0
  68. package/src/asar/packer/stream-builder.ts +723 -0
  69. package/src/cli.ts +182 -0
  70. package/src/config/index.ts +365 -0
  71. package/src/electron/dist-zip.ts +65 -0
  72. package/src/electron/download.ts +121 -0
  73. package/src/index.ts +14 -0
  74. package/src/packer/common/app-update-yml.ts +21 -0
  75. package/src/packer/common/extra-resources.ts +83 -0
  76. package/src/packer/common/staging.ts +33 -0
  77. package/src/packer/common/template.ts +24 -0
  78. package/src/packer/linux/linux.ts +329 -0
  79. package/src/packer/mac/mac.ts +495 -0
  80. package/src/packer/win/nsis.ts +244 -0
  81. package/src/packer/win/resedit.ts +60 -0
  82. package/src/packer/win/toolchain.ts +63 -0
  83. package/src/packer/win/win.ts +421 -0
  84. package/src/pipeline/pipeline.ts +149 -0
  85. package/src/pipeline/targets.ts +67 -0
  86. package/src/shared/hash.ts +15 -0
  87. package/src/shared/logger.ts +91 -0
  88. package/src/shared/platform.ts +41 -0
  89. package/src/shared/types.ts +82 -0
  90. package/src/update/blockmap.ts +247 -0
  91. package/src/update/metadata.ts +97 -0
  92. package/src/update/version.ts +59 -0
  93. package/templates/linux/app.desktop +11 -0
  94. package/templates/linux/control +9 -0
  95. package/templates/linux/postinst +10 -0
  96. package/templates/linux/postrm +8 -0
  97. package/templates/linux/preinst +49 -0
  98. package/templates/linux/prerm +43 -0
  99. package/templates/mac/Info.plist +18 -0
  100. package/templates/mac/app-update.yml +4 -0
  101. package/templates/win/nsis/README.md +51 -0
  102. package/templates/win/nsis/THIRD-PARTY-NOTICES.md +20 -0
  103. package/templates/win/nsis/assistedInstaller.nsh +154 -0
  104. package/templates/win/nsis/assistedMessages.yml +368 -0
  105. package/templates/win/nsis/common.nsh +147 -0
  106. package/templates/win/nsis/empty-license.txt +1 -0
  107. package/templates/win/nsis/include/FileAssociation.nsh +123 -0
  108. package/templates/win/nsis/include/StdUtils.nsh +496 -0
  109. package/templates/win/nsis/include/StrContains.nsh +48 -0
  110. package/templates/win/nsis/include/UAC.nsh +299 -0
  111. package/templates/win/nsis/include/allowOnlyOneInstallerInstance.nsh +164 -0
  112. package/templates/win/nsis/include/extractAppPackage.nsh +138 -0
  113. package/templates/win/nsis/include/getProcessInfo.nsh +157 -0
  114. package/templates/win/nsis/include/installUtil.nsh +248 -0
  115. package/templates/win/nsis/include/installer.nsh +247 -0
  116. package/templates/win/nsis/include/nsProcess.nsh +28 -0
  117. package/templates/win/nsis/include/webPackage.nsh +64 -0
  118. package/templates/win/nsis/installSection.nsh +110 -0
  119. package/templates/win/nsis/installer.nsi +132 -0
  120. package/templates/win/nsis/messages.yml +240 -0
  121. package/templates/win/nsis/multiUser.nsh +134 -0
  122. package/templates/win/nsis/multiUserUi.nsh +230 -0
  123. package/templates/win/nsis/oneClick.nsh +19 -0
  124. package/templates/win/nsis/portable.nsi +91 -0
  125. package/templates/win/nsis/uninstaller.nsh +263 -0
  126. 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
+ }