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
7
10
export enum AsarMode {
8
11
NO_ASAR ,
9
12
HAS_ASAR ,
10
13
}
11
14
15
+ export type FuseASARsOptions = {
16
+ x64 : string ;
17
+ arm64 : string ;
18
+ output : string ;
19
+
20
+ lipo ?: string ;
21
+ uniqueAllowList ?: string ;
22
+ } ;
23
+
24
+ const MACHO_MAGIC = new Set ( [ 0xfeedface , 0xcefaedfe , 0xfeedfacf , 0xcffaedfe ] ) ;
25
+
12
26
export const detectAsarMode = async ( appPath : string ) => {
13
27
d ( 'checking asar mode of' , appPath ) ;
14
28
const asarPath = path . resolve ( appPath , 'Contents' , 'Resources' , 'app.asar' ) ;
@@ -31,3 +45,153 @@ export const generateAsarIntegrity = (asarPath: string) => {
31
45
. digest ( 'hex' ) ,
32
46
} ;
33
47
} ;
48
+
49
+ function toRelativePath ( file : string ) : string {
50
+ return file . replace ( / ^ \/ / , '' ) ;
51
+ }
52
+
53
+ function isDirectory ( a : string , file : string ) : boolean {
54
+ return Boolean ( 'files' in asar . statFile ( a , file ) ) ;
55
+ }
56
+
57
+ function checkUnique ( archive : string , file : string , allowList ?: string ) : void {
58
+ if ( allowList === undefined || ! minimatch ( file , allowList , { matchBase : true } ) ) {
59
+ throw new Error (
60
+ `Detected unique file "${ file } " in "${ archive } " not covered by ` +
61
+ `allowList rule: "${ allowList } "` ,
62
+ ) ;
63
+ }
64
+ }
65
+
66
+ export const fuseASARs = async ( {
67
+ x64,
68
+ arm64,
69
+ output,
70
+ lipo = 'lipo' ,
71
+ uniqueAllowList,
72
+ } : FuseASARsOptions ) : Promise < void > => {
73
+ d ( `Fusing ${ x64 } and ${ arm64 } ` ) ;
74
+
75
+ const x64Files = new Set ( asar . listPackage ( x64 ) . map ( toRelativePath ) ) ;
76
+ const arm64Files = new Set ( asar . listPackage ( arm64 ) . map ( toRelativePath ) ) ;
77
+
78
+ //
79
+ // Build set of unpacked directories and files
80
+ //
81
+
82
+ const unpackedFiles = new Set < string > ( ) ;
83
+
84
+ function buildUnpacked ( a : string , fileList : Set < string > ) : void {
85
+ for ( const file of fileList ) {
86
+ const stat = asar . statFile ( a , file ) ;
87
+
88
+ if ( ! ( 'unpacked' in stat ) || ! stat . unpacked ) {
89
+ continue ;
90
+ }
91
+
92
+ if ( 'files' in stat ) {
93
+ continue ;
94
+ }
95
+ unpackedFiles . add ( file ) ;
96
+ }
97
+ }
98
+
99
+ buildUnpacked ( x64 , x64Files ) ;
100
+ buildUnpacked ( arm64 , arm64Files ) ;
101
+
102
+ //
103
+ // Build list of files/directories unique to each asar
104
+ //
105
+
106
+ for ( const file of x64Files ) {
107
+ if ( ! arm64Files . has ( file ) ) {
108
+ checkUnique ( x64 , file , uniqueAllowList ) ;
109
+ }
110
+ }
111
+ const arm64Unique = [ ] ;
112
+ for ( const file of arm64Files ) {
113
+ if ( ! x64Files . has ( file ) ) {
114
+ checkUnique ( arm64 , file , uniqueAllowList ) ;
115
+ arm64Unique . push ( file ) ;
116
+ }
117
+ }
118
+
119
+ //
120
+ // Find common bindings with different content
121
+ //
122
+
123
+ const commonBindings = [ ] ;
124
+ for ( const file of x64Files ) {
125
+ if ( ! arm64Files . has ( file ) ) {
126
+ continue ;
127
+ }
128
+
129
+ // Skip directories
130
+ if ( isDirectory ( x64 , file ) ) {
131
+ continue ;
132
+ }
133
+
134
+ const x64Content = asar . extractFile ( x64 , file ) ;
135
+ const arm64Content = asar . extractFile ( arm64 , file ) ;
136
+
137
+ if ( x64Content . compare ( arm64Content ) === 0 ) {
138
+ continue ;
139
+ }
140
+
141
+ if ( ! MACHO_MAGIC . has ( x64Content . readUInt32LE ( 0 ) ) ) {
142
+ throw new Error ( `Can't reconcile two non-macho files ${ file } ` ) ;
143
+ }
144
+
145
+ commonBindings . push ( file ) ;
146
+ }
147
+
148
+ //
149
+ // Extract both
150
+ //
151
+
152
+ const x64Dir = await fs . mkdtemp ( path . join ( os . tmpdir ( ) , 'x64-' ) ) ;
153
+ const arm64Dir = await fs . mkdtemp ( path . join ( os . tmpdir ( ) , 'arm64-' ) ) ;
154
+
155
+ try {
156
+ d ( `Extracting ${ x64 } to ${ x64Dir } ` ) ;
157
+ asar . extractAll ( x64 , x64Dir ) ;
158
+
159
+ d ( `Extracting ${ arm64 } to ${ arm64Dir } ` ) ;
160
+ asar . extractAll ( arm64 , arm64Dir ) ;
161
+
162
+ for ( const file of arm64Unique ) {
163
+ const source = path . resolve ( arm64Dir , file ) ;
164
+ const destination = path . resolve ( x64Dir , file ) ;
165
+
166
+ if ( isDirectory ( arm64 , file ) ) {
167
+ d ( `Creating unique directory: ${ file } ` ) ;
168
+ await fs . mkdirp ( destination ) ;
169
+ continue ;
170
+ }
171
+
172
+ d ( `Copying unique file: ${ file } ` ) ;
173
+ await fs . mkdirp ( path . dirname ( destination ) ) ;
174
+ await fs . copy ( source , destination ) ;
175
+ }
176
+
177
+ for ( const binding of commonBindings ) {
178
+ const source = await fs . realpath ( path . resolve ( arm64Dir , binding ) ) ;
179
+ const destination = await fs . realpath ( path . resolve ( x64Dir , binding ) ) ;
180
+
181
+ d ( `Fusing binding: ${ binding } ` ) ;
182
+ execFileSync ( lipo , [ source , destination , '-create' , '-output' , destination ] ) ;
183
+ }
184
+
185
+ d ( `Creating archive at ${ output } ` ) ;
186
+
187
+ const resolvedUnpack = Array . from ( unpackedFiles ) . map ( ( file ) => path . join ( x64Dir , file ) ) ;
188
+
189
+ await asar . createPackageWithOptions ( x64Dir , output , {
190
+ unpack : `{${ resolvedUnpack . join ( ',' ) } }` ,
191
+ } ) ;
192
+
193
+ d ( 'Done fusing' ) ;
194
+ } finally {
195
+ await Promise . all ( [ fs . remove ( x64Dir ) , fs . remove ( arm64Dir ) ] ) ;
196
+ }
197
+ } ;
0 commit comments