1
1
import * as asar from 'asar' ;
2
+ import { execFileSync } from 'child_process' ;
2
3
import * as crypto from 'crypto' ;
3
4
import * as fs from 'fs-extra' ;
4
5
import * as path from 'path' ;
6
+ import * as minimatch from 'minimatch' ;
7
+ import * as os from 'os' ;
5
8
import { d } from './debug' ;
6
9
10
+ const LIPO = 'lipo' ;
11
+
7
12
export enum AsarMode {
8
13
NO_ASAR ,
9
14
HAS_ASAR ,
10
15
}
11
16
17
+ export type MergeASARsOptions = {
18
+ x64AsarPath : string ;
19
+ arm64AsarPath : string ;
20
+ outputAsarPath : string ;
21
+
22
+ singleArchFiles ?: string ;
23
+ } ;
24
+
25
+ // See: https://github.com/apple-opensource-mirror/llvmCore/blob/0c60489d96c87140db9a6a14c6e82b15f5e5d252/include/llvm/Object/MachOFormat.h#L108-L112
26
+ const MACHO_MAGIC = new Set ( [
27
+ // 32-bit Mach-O
28
+ 0xfeedface ,
29
+ 0xcefaedfe ,
30
+
31
+ // 64-bit Mach-O
32
+ 0xfeedfacf ,
33
+ 0xcffaedfe ,
34
+ ] ) ;
35
+
12
36
export const detectAsarMode = async ( appPath : string ) => {
13
37
d ( 'checking asar mode of' , appPath ) ;
14
38
const asarPath = path . resolve ( appPath , 'Contents' , 'Resources' , 'app.asar' ) ;
@@ -31,3 +55,152 @@ export const generateAsarIntegrity = (asarPath: string) => {
31
55
. digest ( 'hex' ) ,
32
56
} ;
33
57
} ;
58
+
59
+ function toRelativePath ( file : string ) : string {
60
+ return file . replace ( / ^ \/ / , '' ) ;
61
+ }
62
+
63
+ function isDirectory ( a : string , file : string ) : boolean {
64
+ return Boolean ( 'files' in asar . statFile ( a , file ) ) ;
65
+ }
66
+
67
+ function checkSingleArch ( archive : string , file : string , allowList ?: string ) : void {
68
+ if ( allowList === undefined || ! minimatch ( file , allowList , { matchBase : true } ) ) {
69
+ throw new Error (
70
+ `Detected unique file "${ file } " in "${ archive } " not covered by ` +
71
+ `allowList rule: "${ allowList } "` ,
72
+ ) ;
73
+ }
74
+ }
75
+
76
+ export const mergeASARs = async ( {
77
+ x64AsarPath,
78
+ arm64AsarPath,
79
+ outputAsarPath,
80
+ singleArchFiles,
81
+ } : MergeASARsOptions ) : Promise < void > => {
82
+ d ( `merging ${ x64AsarPath } and ${ arm64AsarPath } ` ) ;
83
+
84
+ const x64Files = new Set ( asar . listPackage ( x64AsarPath ) . map ( toRelativePath ) ) ;
85
+ const arm64Files = new Set ( asar . listPackage ( arm64AsarPath ) . map ( toRelativePath ) ) ;
86
+
87
+ //
88
+ // Build set of unpacked directories and files
89
+ //
90
+
91
+ const unpackedFiles = new Set < string > ( ) ;
92
+
93
+ function buildUnpacked ( a : string , fileList : Set < string > ) : void {
94
+ for ( const file of fileList ) {
95
+ const stat = asar . statFile ( a , file ) ;
96
+
97
+ if ( ! ( 'unpacked' in stat ) || ! stat . unpacked ) {
98
+ continue ;
99
+ }
100
+
101
+ if ( 'files' in stat ) {
102
+ continue ;
103
+ }
104
+ unpackedFiles . add ( file ) ;
105
+ }
106
+ }
107
+
108
+ buildUnpacked ( x64AsarPath , x64Files ) ;
109
+ buildUnpacked ( arm64AsarPath , arm64Files ) ;
110
+
111
+ //
112
+ // Build list of files/directories unique to each asar
113
+ //
114
+
115
+ for ( const file of x64Files ) {
116
+ if ( ! arm64Files . has ( file ) ) {
117
+ checkSingleArch ( x64AsarPath , file , singleArchFiles ) ;
118
+ }
119
+ }
120
+ const arm64Unique = [ ] ;
121
+ for ( const file of arm64Files ) {
122
+ if ( ! x64Files . has ( file ) ) {
123
+ checkSingleArch ( arm64AsarPath , file , singleArchFiles ) ;
124
+ arm64Unique . push ( file ) ;
125
+ }
126
+ }
127
+
128
+ //
129
+ // Find common bindings with different content
130
+ //
131
+
132
+ const commonBindings = [ ] ;
133
+ for ( const file of x64Files ) {
134
+ if ( ! arm64Files . has ( file ) ) {
135
+ continue ;
136
+ }
137
+
138
+ // Skip directories
139
+ if ( isDirectory ( x64AsarPath , file ) ) {
140
+ continue ;
141
+ }
142
+
143
+ const x64Content = asar . extractFile ( x64AsarPath , file ) ;
144
+ const arm64Content = asar . extractFile ( arm64AsarPath , file ) ;
145
+
146
+ if ( x64Content . compare ( arm64Content ) === 0 ) {
147
+ continue ;
148
+ }
149
+
150
+ if ( ! MACHO_MAGIC . has ( x64Content . readUInt32LE ( 0 ) ) ) {
151
+ throw new Error ( `Can't reconcile two non-macho files ${ file } ` ) ;
152
+ }
153
+
154
+ commonBindings . push ( file ) ;
155
+ }
156
+
157
+ //
158
+ // Extract both
159
+ //
160
+
161
+ const x64Dir = await fs . mkdtemp ( path . join ( os . tmpdir ( ) , 'x64-' ) ) ;
162
+ const arm64Dir = await fs . mkdtemp ( path . join ( os . tmpdir ( ) , 'arm64-' ) ) ;
163
+
164
+ try {
165
+ d ( `extracting ${ x64AsarPath } to ${ x64Dir } ` ) ;
166
+ asar . extractAll ( x64AsarPath , x64Dir ) ;
167
+
168
+ d ( `extracting ${ arm64AsarPath } to ${ arm64Dir } ` ) ;
169
+ asar . extractAll ( arm64AsarPath , arm64Dir ) ;
170
+
171
+ for ( const file of arm64Unique ) {
172
+ const source = path . resolve ( arm64Dir , file ) ;
173
+ const destination = path . resolve ( x64Dir , file ) ;
174
+
175
+ if ( isDirectory ( arm64AsarPath , file ) ) {
176
+ d ( `creating unique directory: ${ file } ` ) ;
177
+ await fs . mkdirp ( destination ) ;
178
+ continue ;
179
+ }
180
+
181
+ d ( `xopying unique file: ${ file } ` ) ;
182
+ await fs . mkdirp ( path . dirname ( destination ) ) ;
183
+ await fs . copy ( source , destination ) ;
184
+ }
185
+
186
+ for ( const binding of commonBindings ) {
187
+ const source = await fs . realpath ( path . resolve ( arm64Dir , binding ) ) ;
188
+ const destination = await fs . realpath ( path . resolve ( x64Dir , binding ) ) ;
189
+
190
+ d ( `merging binding: ${ binding } ` ) ;
191
+ execFileSync ( LIPO , [ source , destination , '-create' , '-output' , destination ] ) ;
192
+ }
193
+
194
+ d ( `creating archive at ${ outputAsarPath } ` ) ;
195
+
196
+ const resolvedUnpack = Array . from ( unpackedFiles ) . map ( ( file ) => path . join ( x64Dir , file ) ) ;
197
+
198
+ await asar . createPackageWithOptions ( x64Dir , outputAsarPath , {
199
+ unpack : `{${ resolvedUnpack . join ( ',' ) } }` ,
200
+ } ) ;
201
+
202
+ d ( 'done merging' ) ;
203
+ } finally {
204
+ await Promise . all ( [ fs . remove ( x64Dir ) , fs . remove ( arm64Dir ) ] ) ;
205
+ }
206
+ } ;
0 commit comments