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,41 @@
|
|
|
1
|
+
import { Buffer } from 'node:buffer'
|
|
2
|
+
|
|
3
|
+
export interface ArEntry {
|
|
4
|
+
name: string
|
|
5
|
+
content: Buffer
|
|
6
|
+
mode?: number
|
|
7
|
+
mtime?: number
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Minimal `ar` archive writer, sufficient for .deb assembly
|
|
12
|
+
* (debian-binary + control.tar.xz + data.tar.xz).
|
|
13
|
+
*/
|
|
14
|
+
export function createArArchive(files: ArEntry[]): Buffer {
|
|
15
|
+
const parts = [Buffer.from('!<arch>\n')]
|
|
16
|
+
|
|
17
|
+
for (const file of files) {
|
|
18
|
+
const mtime = file.mtime ?? Math.floor(Date.now() / 1000)
|
|
19
|
+
const mode = file.mode ?? 0o100644
|
|
20
|
+
const size = file.content.length
|
|
21
|
+
|
|
22
|
+
const header = Buffer.alloc(60)
|
|
23
|
+
header.write(file.name.padEnd(16), 0, 16, 'ascii')
|
|
24
|
+
header.write(String(mtime).padEnd(12), 16, 12, 'ascii')
|
|
25
|
+
header.write('0 ', 28, 6, 'ascii')
|
|
26
|
+
header.write('0 ', 34, 6, 'ascii')
|
|
27
|
+
header.write(mode.toString(8).padStart(6, '0').padEnd(8), 40, 8, 'ascii')
|
|
28
|
+
header.write(String(size).padEnd(10), 48, 10, 'ascii')
|
|
29
|
+
header.write('`\n', 58, 2, 'ascii')
|
|
30
|
+
|
|
31
|
+
parts.push(header)
|
|
32
|
+
parts.push(Buffer.from(file.content))
|
|
33
|
+
|
|
34
|
+
// ar members are 2-byte aligned
|
|
35
|
+
if (size % 2 !== 0) {
|
|
36
|
+
parts.push(Buffer.from('\n'))
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
return Buffer.concat(parts)
|
|
41
|
+
}
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 7za / xz compression wrapper.
|
|
3
|
+
* 7za is provided by the `7zip-bin` npm package, which is available on
|
|
4
|
+
* every host operating system. On Linux, xz compression uses the system
|
|
5
|
+
* `xz` executable when available.
|
|
6
|
+
*/
|
|
7
|
+
import { type SpawnSyncReturns, spawnSync } from 'node:child_process'
|
|
8
|
+
import { existsSync, renameSync } from 'node:fs'
|
|
9
|
+
import os from 'node:os'
|
|
10
|
+
import { path7za } from '7zip-bin'
|
|
11
|
+
|
|
12
|
+
const DEFAULT_7Z_LEVEL = 5
|
|
13
|
+
|
|
14
|
+
export interface CompressOptions {
|
|
15
|
+
src: string
|
|
16
|
+
dest: string
|
|
17
|
+
format?: '7z' | 'xz'
|
|
18
|
+
level?: number
|
|
19
|
+
threads?: number
|
|
20
|
+
solid?: boolean
|
|
21
|
+
sevenZipPath?: string
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export interface CompressResult {
|
|
25
|
+
level: number
|
|
26
|
+
threads: number
|
|
27
|
+
solid: boolean
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function get7zaPath(): string {
|
|
31
|
+
if (existsSync(path7za)) return path7za
|
|
32
|
+
throw new Error(`7za not found: ${path7za}`)
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Parses `7z l` output for the total uncompressed size, used for the
|
|
37
|
+
* NSIS ESTIMATED_SIZE define. Returns 0 when listing fails.
|
|
38
|
+
*/
|
|
39
|
+
export function computeEstimatedSize(sevenZipPath: string, archiveFile: string): number {
|
|
40
|
+
const result = spawnSync(sevenZipPath, ['l', archiveFile], {
|
|
41
|
+
encoding: 'utf8',
|
|
42
|
+
stdio: 'pipe',
|
|
43
|
+
windowsHide: true,
|
|
44
|
+
})
|
|
45
|
+
if (result.status !== 0) return 0
|
|
46
|
+
const output = (result.stdout || '').trim()
|
|
47
|
+
const match = /(\d+)\s+\d+\s+\d+\s+files/.exec(output)
|
|
48
|
+
return match ? Number.parseInt(match[1], 10) : 0
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function compress({
|
|
52
|
+
src,
|
|
53
|
+
dest,
|
|
54
|
+
format = '7z',
|
|
55
|
+
level = DEFAULT_7Z_LEVEL,
|
|
56
|
+
threads,
|
|
57
|
+
solid = false,
|
|
58
|
+
sevenZipPath: sevenZipPathOption,
|
|
59
|
+
}: CompressOptions): CompressResult {
|
|
60
|
+
const resolved = resolveCompressionOptions({ level, threads, solid })
|
|
61
|
+
|
|
62
|
+
if (format === 'xz' && process.platform === 'linux' && hasSystemXz()) {
|
|
63
|
+
compressWithSystemXz({ src, dest, level: resolved.level })
|
|
64
|
+
return resolved
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const sevenZipPath = sevenZipPathOption ?? get7zaPath()
|
|
68
|
+
const { args, cwd } = build7zaAddArgs({ src, dest, format, ...resolved })
|
|
69
|
+
|
|
70
|
+
const result = spawnSync(sevenZipPath, args, { cwd, stdio: 'pipe', windowsHide: true })
|
|
71
|
+
if (result.status !== 0) {
|
|
72
|
+
throwCompressionError(format, result)
|
|
73
|
+
}
|
|
74
|
+
return resolved
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function resolveCompressionOptions(options: {
|
|
78
|
+
level?: number
|
|
79
|
+
threads?: number
|
|
80
|
+
solid?: boolean
|
|
81
|
+
}): CompressResult {
|
|
82
|
+
const level = clampLevel(options.level ?? DEFAULT_7Z_LEVEL)
|
|
83
|
+
const threads = Math.max(1, options.threads ?? os.cpus().length)
|
|
84
|
+
const solid = options.solid ?? false
|
|
85
|
+
|
|
86
|
+
return { level, threads, solid }
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export function build7zaAddArgs({
|
|
90
|
+
src,
|
|
91
|
+
dest,
|
|
92
|
+
format = '7z',
|
|
93
|
+
level,
|
|
94
|
+
threads,
|
|
95
|
+
solid,
|
|
96
|
+
}: {
|
|
97
|
+
src: string
|
|
98
|
+
dest: string
|
|
99
|
+
format?: '7z' | 'xz'
|
|
100
|
+
level: number
|
|
101
|
+
threads: number
|
|
102
|
+
solid: boolean
|
|
103
|
+
}): { args: string[]; cwd: string | undefined } {
|
|
104
|
+
const formatArg = format === '7z' ? [] : [`-t${format}`]
|
|
105
|
+
const srcArg = format === '7z' ? '.' : src
|
|
106
|
+
const args = ['a', '-bd', `-mx=${level}`, ...formatArg, dest, srcArg]
|
|
107
|
+
|
|
108
|
+
if (format === '7z') {
|
|
109
|
+
args.splice(3, 0, `-mmt=${threads}`)
|
|
110
|
+
if (!solid) {
|
|
111
|
+
args.splice(3, 0, '-ms=off')
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
return { args, cwd: format === '7z' ? src : undefined }
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/** Clamp a compression level to the valid 0-9 range of 7z. */
|
|
119
|
+
function clampLevel(level: number): number {
|
|
120
|
+
if (!Number.isFinite(level)) {
|
|
121
|
+
return DEFAULT_7Z_LEVEL
|
|
122
|
+
}
|
|
123
|
+
return Math.min(9, Math.max(0, Math.trunc(level)))
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function hasSystemXz(): boolean {
|
|
127
|
+
const result = spawnSync('xz', ['--version'], { stdio: 'ignore', windowsHide: true })
|
|
128
|
+
return result.status === 0
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function compressWithSystemXz({
|
|
132
|
+
src,
|
|
133
|
+
dest,
|
|
134
|
+
level,
|
|
135
|
+
}: {
|
|
136
|
+
src: string
|
|
137
|
+
dest: string
|
|
138
|
+
level: number
|
|
139
|
+
}): void {
|
|
140
|
+
const result = spawnSync('xz', [`-${level}`, '-k', '-f', src], {
|
|
141
|
+
stdio: 'pipe',
|
|
142
|
+
windowsHide: true,
|
|
143
|
+
})
|
|
144
|
+
if (result.status !== 0) {
|
|
145
|
+
throwCompressionError('xz', result)
|
|
146
|
+
}
|
|
147
|
+
const defaultDest = `${src}.xz`
|
|
148
|
+
if (dest !== defaultDest) {
|
|
149
|
+
renameSync(defaultDest, dest)
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function throwCompressionError(format: string, result: SpawnSyncReturns<Buffer>): never {
|
|
154
|
+
if (result.error) {
|
|
155
|
+
throw new Error(`${format} compression failed to spawn: ${result.error.message}`)
|
|
156
|
+
}
|
|
157
|
+
const stderr = result.stderr?.toString().trim() || ''
|
|
158
|
+
const signal = result.signal ? `, signal ${result.signal}` : ''
|
|
159
|
+
throw new Error(`${format} compression failed (exit ${result.status}${signal}): ${stderr}`)
|
|
160
|
+
}
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tar entry helpers: materialize entries into a temporary directory, then
|
|
3
|
+
* pack it with `tar` while preserving per-entry modes. Mode preservation is
|
|
4
|
+
* required for setuid binaries such as chrome-sandbox.
|
|
5
|
+
*/
|
|
6
|
+
import { existsSync, type Stats } from 'node:fs'
|
|
7
|
+
import { mkdir, readdir, readFile, readlink, rm, stat, symlink, writeFile } from 'node:fs/promises'
|
|
8
|
+
import path from 'node:path'
|
|
9
|
+
import type { ReadEntry } from 'tar'
|
|
10
|
+
import * as tar from 'tar'
|
|
11
|
+
|
|
12
|
+
export type TarEntry =
|
|
13
|
+
| { type: 'dir'; path: string; mode?: number }
|
|
14
|
+
| {
|
|
15
|
+
type: 'file'
|
|
16
|
+
path: string
|
|
17
|
+
mode?: number
|
|
18
|
+
content?: Buffer
|
|
19
|
+
filePath?: string
|
|
20
|
+
getBuffer?: () => Promise<Buffer>
|
|
21
|
+
}
|
|
22
|
+
| {
|
|
23
|
+
type: 'link'
|
|
24
|
+
path: string
|
|
25
|
+
mode?: number
|
|
26
|
+
target?: string
|
|
27
|
+
getTarget?: () => Promise<string>
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** Packs entries into an uncompressed tar at destPath. */
|
|
31
|
+
export async function writeTarFromEntries(entries: TarEntry[], destPath: string): Promise<void> {
|
|
32
|
+
const tmpDir = `${destPath}-tmp`
|
|
33
|
+
await mkdir(tmpDir, { recursive: true })
|
|
34
|
+
|
|
35
|
+
try {
|
|
36
|
+
const modeMap = new Map<string, number | undefined>()
|
|
37
|
+
|
|
38
|
+
for (const entry of entries) {
|
|
39
|
+
const fullPath = path.join(tmpDir, entry.path)
|
|
40
|
+
const parentDir = path.dirname(fullPath)
|
|
41
|
+
|
|
42
|
+
if (entry.type === 'dir') {
|
|
43
|
+
await mkdir(fullPath, { recursive: true })
|
|
44
|
+
modeMap.set(entry.path, entry.mode)
|
|
45
|
+
} else if (entry.type === 'file') {
|
|
46
|
+
await mkdir(parentDir, { recursive: true })
|
|
47
|
+
await writeFile(fullPath, await resolveBuffer(entry))
|
|
48
|
+
modeMap.set(entry.path, entry.mode)
|
|
49
|
+
} else if (entry.type === 'link') {
|
|
50
|
+
await mkdir(parentDir, { recursive: true })
|
|
51
|
+
const target = entry.getTarget ? await entry.getTarget() : entry.target || ''
|
|
52
|
+
await symlink(target, fullPath)
|
|
53
|
+
modeMap.set(entry.path, entry.mode)
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
await tar.create(
|
|
58
|
+
{
|
|
59
|
+
gzip: false,
|
|
60
|
+
file: destPath,
|
|
61
|
+
cwd: tmpDir,
|
|
62
|
+
portable: true,
|
|
63
|
+
noPax: true,
|
|
64
|
+
filter: (entryPath: string, stat: ReadEntry | Stats) => {
|
|
65
|
+
const p = entryPath.replace(/^\.\//, '')
|
|
66
|
+
if (p === '.') return true
|
|
67
|
+
const mode = modeMap.get(p)
|
|
68
|
+
if (mode !== undefined) {
|
|
69
|
+
stat.mode = mode
|
|
70
|
+
}
|
|
71
|
+
return true
|
|
72
|
+
},
|
|
73
|
+
},
|
|
74
|
+
['.'],
|
|
75
|
+
)
|
|
76
|
+
} finally {
|
|
77
|
+
await rm(tmpDir, { recursive: true, force: true })
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export interface CollectDirectoryOptions {
|
|
82
|
+
includeSymlinks?: boolean
|
|
83
|
+
resolveFileMode?: (input: {
|
|
84
|
+
filePath: string
|
|
85
|
+
relativePath: string
|
|
86
|
+
sourceMode: number
|
|
87
|
+
}) => number | undefined | undefined
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/** Flattens a directory on disk into tar entries rooted at prefix. */
|
|
91
|
+
export async function collectDirectoryEntries(
|
|
92
|
+
dirPath: string,
|
|
93
|
+
prefix: string,
|
|
94
|
+
options: CollectDirectoryOptions = {},
|
|
95
|
+
): Promise<TarEntry[]> {
|
|
96
|
+
if (!existsSync(dirPath)) return []
|
|
97
|
+
|
|
98
|
+
const entries: TarEntry[] = []
|
|
99
|
+
await walkDir(dirPath, dirPath, prefix, entries, options)
|
|
100
|
+
return entries
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/** Resolve a file entry's content: inline Buffer, disk path, or lazy getter. */
|
|
104
|
+
async function resolveBuffer(entry: Extract<TarEntry, { type: 'file' }>): Promise<Buffer> {
|
|
105
|
+
if (entry.content) return entry.content
|
|
106
|
+
if (entry.filePath) return readFile(entry.filePath)
|
|
107
|
+
if (entry.getBuffer) return entry.getBuffer()
|
|
108
|
+
throw new Error(`TarEntry "${entry.path}" has no content source`)
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
async function walkDir(
|
|
112
|
+
rootDir: string,
|
|
113
|
+
dirPath: string,
|
|
114
|
+
prefix: string,
|
|
115
|
+
entries: TarEntry[],
|
|
116
|
+
options: CollectDirectoryOptions,
|
|
117
|
+
): Promise<void> {
|
|
118
|
+
const children = await readdir(dirPath, { withFileTypes: true })
|
|
119
|
+
for (const child of children) {
|
|
120
|
+
const fullPath = path.join(dirPath, child.name)
|
|
121
|
+
const entryPath = `${prefix}/${child.name}`
|
|
122
|
+
const relativePath = path.relative(rootDir, fullPath).split(path.sep).join('/')
|
|
123
|
+
|
|
124
|
+
if (child.isDirectory()) {
|
|
125
|
+
entries.push({ type: 'dir', path: entryPath, mode: 0o40755 })
|
|
126
|
+
await walkDir(rootDir, fullPath, entryPath, entries, options)
|
|
127
|
+
} else if (child.isFile()) {
|
|
128
|
+
const s = await stat(fullPath)
|
|
129
|
+
const mode =
|
|
130
|
+
options.resolveFileMode?.({ filePath: fullPath, relativePath, sourceMode: s.mode }) ??
|
|
131
|
+
0o100644
|
|
132
|
+
entries.push({ type: 'file', path: entryPath, mode, filePath: fullPath })
|
|
133
|
+
} else if (child.isSymbolicLink() && options.includeSymlinks) {
|
|
134
|
+
entries.push({
|
|
135
|
+
type: 'link',
|
|
136
|
+
path: entryPath,
|
|
137
|
+
mode: 0o120755,
|
|
138
|
+
target: await readlink(fullPath),
|
|
139
|
+
})
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Reads a zip archive (Electron distributions) preserving Unix permissions
|
|
3
|
+
* so symlinks and setuid bits survive repacking.
|
|
4
|
+
*/
|
|
5
|
+
import { readFile } from 'node:fs/promises'
|
|
6
|
+
import type { JSZipObject } from 'jszip'
|
|
7
|
+
import JSZip from 'jszip'
|
|
8
|
+
|
|
9
|
+
const S_IFMT = 0o170000
|
|
10
|
+
const S_IFLNK = 0o120000
|
|
11
|
+
|
|
12
|
+
export interface ZipEntry {
|
|
13
|
+
path: string
|
|
14
|
+
dir: boolean
|
|
15
|
+
symlink: boolean
|
|
16
|
+
mode: string | number
|
|
17
|
+
entry: JSZipObject
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export async function readZip(zipPath: string): Promise<{ entries: ZipEntry[]; zip: JSZip }> {
|
|
21
|
+
const data = await readFile(zipPath)
|
|
22
|
+
const zip = await JSZip.loadAsync(data)
|
|
23
|
+
|
|
24
|
+
const entries: ZipEntry[] = []
|
|
25
|
+
for (const [entryPath, entry] of Object.entries(zip.files)) {
|
|
26
|
+
const unixPerms = Number(entry.unixPermissions || 0)
|
|
27
|
+
const fileType = unixPerms & S_IFMT
|
|
28
|
+
|
|
29
|
+
entries.push({
|
|
30
|
+
path: entryPath,
|
|
31
|
+
dir: entry.dir,
|
|
32
|
+
symlink: fileType === S_IFLNK,
|
|
33
|
+
mode: unixPerms,
|
|
34
|
+
entry,
|
|
35
|
+
})
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
return { entries, zip }
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export async function getEntryBuffer(entry: JSZipObject): Promise<Buffer> {
|
|
42
|
+
return entry.async('nodebuffer')
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export async function getSymlinkTarget(entry: JSZipObject): Promise<string> {
|
|
46
|
+
return entry.async('text')
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function isSymlink(zipEntry: ZipEntry): boolean {
|
|
50
|
+
return (Number(zipEntry.mode) & S_IFMT) === S_IFLNK
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function isDefaultAppEntry(entryPath: string): boolean {
|
|
54
|
+
return (
|
|
55
|
+
entryPath === 'resources/app' ||
|
|
56
|
+
entryPath.startsWith('resources/app/') ||
|
|
57
|
+
entryPath === 'resources/default_app.asar' ||
|
|
58
|
+
entryPath === 'Electron.app/Contents/Resources/app' ||
|
|
59
|
+
entryPath.startsWith('Electron.app/Contents/Resources/app/')
|
|
60
|
+
)
|
|
61
|
+
}
|