11import { createPackageFromStreams , AsarStreamType , AsarDirectory } from "@electron/asar"
2- import { log } from "builder-util"
3- import { exists , Filter } from "builder-util/out/fs"
2+ import { isEmptyOrSpaces , log } from "builder-util"
3+ import { exists , Filter , FilterStats } from "builder-util/out/fs"
44import * as fs from "fs-extra"
55import { readlink } from "fs-extra"
66import * as path from "path"
@@ -91,26 +91,39 @@ export class AsarPackager {
9191 }
9292 }
9393
94- const results : AsarStreamType [ ] = [ ]
95- const resultsPaths = new Set < string > ( )
94+ const resultsMap = new Map < string , AsarStreamType > ( )
95+ const streamOrdering : string [ ] = [ ]
96+ const normalizedUnpackedPaths = Array . from ( unpackedPaths ) . map ( p => path . normalize ( p ) )
97+
98+ // Check whether a file or directory should be unpacked, using pre-normalized unpacked paths and early returns
99+ const isUnpacked = ( dir : string , file ?: string , stat ?: FilterStats ) : boolean => {
100+ const normalizedDir = path . normalize ( dir )
101+
102+ // Check file pattern first (most specific)
103+ if ( ! isEmptyOrSpaces ( file ) && stat && this . config . unpackPattern ?.( file , stat ) ) {
104+ return true
105+ }
106+
107+ // Check if path is within any unpacked directory
108+ for ( const unpackedPath of normalizedUnpackedPaths ) {
109+ if ( normalizedDir === unpackedPath || normalizedDir . startsWith ( unpackedPath + path . sep ) ) {
110+ return true
111+ }
112+ }
113+
114+ return false
115+ }
116+
117+ // First pass: process all files in order, ensuring parent directories exist
96118 for ( const fileSet of fileSets ) {
97119 // Don't use Promise.all, we need to retain order of execution/iteration through the already-ordered fileset
98120 for ( const [ index , file ] of fileSet . files . entries ( ) ) {
99121 const transformedData = fileSet . transformedFiles ?. get ( index )
100122 const stat = fileSet . metadata . get ( file ) !
101123 const destination = path . relative ( this . config . defaultDestination , getDestinationPath ( file , fileSet ) )
102124
103- const paths = Array . from ( unpackedPaths ) . map ( p => path . normalize ( p ) )
104-
105- const isChildDirectory = ( fileOrDirPath : string ) =>
106- paths . includes ( path . normalize ( fileOrDirPath ) ) || paths . some ( unpackedPath => path . normalize ( fileOrDirPath ) . startsWith ( unpackedPath + path . sep ) )
107- const isUnpacked = ( dir : string ) => {
108- const isChild = isChildDirectory ( dir )
109- const isFileUnpacked = this . config . unpackPattern ?.( file , stat ) ?? false
110- return isChild || isFileUnpacked
111- }
112-
113- this . processParentDirectories ( isUnpacked , destination , results , resultsPaths )
125+ // Ensure parent directories exist before processing file
126+ this . ensureParentDirectories ( destination , resultsMap , streamOrdering )
114127
115128 const result = await this . processFileOrSymlink ( {
116129 file,
@@ -120,31 +133,64 @@ export class AsarPackager {
120133 stat,
121134 isUnpacked,
122135 } )
123- if ( result != null ) {
124- results . push ( result )
125- resultsPaths . add ( result . path )
136+
137+ if ( result && ! resultsMap . has ( result . path ) ) {
138+ resultsMap . set ( result . path , result )
139+ streamOrdering . push ( result . path )
126140 }
127141 }
128142 }
129- return results
130- }
131143
132- private processParentDirectories ( isUnpacked : ( path : string ) => boolean , destination : string , results : AsarStreamType [ ] , resultsPaths : Set < string > ) {
133- // process parent directories
134- let superDir = path . dirname ( path . normalize ( destination ) )
135- while ( superDir !== "." ) {
136- const dir : AsarDirectory = {
137- type : "directory" ,
138- path : superDir ,
139- unpacked : isUnpacked ( superDir ) ,
144+ // Second pass: propagate unpacked flag to parent directories
145+ for ( const entry of resultsMap . values ( ) ) {
146+ if ( entry . unpacked ) {
147+ this . markParentDirectoriesAsUnpacked ( entry . path , resultsMap , isUnpacked )
140148 }
141- // add to results if not already present
142- if ( ! resultsPaths . has ( dir . path ) ) {
143- results . push ( dir )
144- resultsPaths . add ( dir . path )
149+ }
150+
151+ // Build final results array maintaining processing order
152+ return streamOrdering . reduce < AsarStreamType [ ] > ( ( streams , path ) => {
153+ const stream = resultsMap . has ( path ) ? resultsMap . get ( path ) : null
154+ if ( stream != null ) {
155+ streams . push ( stream )
145156 }
157+ return streams
158+ } , [ ] )
159+ }
160+
161+ private ensureParentDirectories ( destination : string , resultsMap : Map < string , AsarStreamType > , streamOrdering : string [ ] ) : void {
162+ const parents : string [ ] = [ ]
163+ let current = path . dirname ( path . normalize ( destination ) )
146164
147- superDir = path . dirname ( superDir )
165+ // Collect all parent directories from deepest to root
166+ while ( current !== "." ) {
167+ parents . unshift ( current )
168+ current = path . dirname ( current )
169+ }
170+
171+ // Add parent directories in order (root to deepest)
172+ for ( const parentPath of parents ) {
173+ if ( ! resultsMap . has ( parentPath ) ) {
174+ const dir : AsarDirectory = {
175+ type : "directory" ,
176+ path : parentPath ,
177+ unpacked : false , // Updated in second pass if needed
178+ }
179+ resultsMap . set ( parentPath , dir )
180+ streamOrdering . push ( parentPath )
181+ }
182+ }
183+ }
184+
185+ private markParentDirectoriesAsUnpacked ( destination : string , resultsMap : Map < string , AsarStreamType > , isUnpacked : ( path : string ) => boolean ) : void {
186+ let current = path . dirname ( path . normalize ( destination ) )
187+
188+ while ( current !== "." ) {
189+ const entry = resultsMap . get ( current )
190+ if ( entry && isUnpacked ( current ) ) {
191+ entry . unpacked = true
192+ }
193+ current = path . dirname ( current )
148194 }
149195 }
150196
@@ -154,54 +200,57 @@ export class AsarPackager {
154200 stat : fs . Stats
155201 fileSet : ResolvedFileSet
156202 transformedData : string | Buffer | undefined
157- isUnpacked : ( path : string ) => boolean
203+ isUnpacked : ( dir : string , file ?: string , stat ?: FilterStats ) => boolean
158204 } ) : Promise < AsarStreamType > {
159205 const { isUnpacked, transformedData, file, destination, stat } = options
160- const unpacked = isUnpacked ( destination )
206+ const unpacked = isUnpacked ( destination , file , stat )
161207
208+ // Handle directories
162209 if ( ! stat . isFile ( ) && ! stat . isSymbolicLink ( ) ) {
163210 return { path : destination , unpacked, type : "directory" }
164211 }
165212
166- // write any data if provided, skip symlink check
213+ // Handle transformed data (pre-processed content)
167214 if ( transformedData != null ) {
168- const streamGenerator = ( ) => {
169- return new Readable ( {
170- read ( ) {
171- this . push ( transformedData )
172- this . push ( null )
173- } ,
174- } )
175- }
176215 const size = Buffer . byteLength ( transformedData )
177- return { path : destination , streamGenerator, unpacked, type : "file" , stat : { mode : stat . mode , size } }
216+ return {
217+ path : destination ,
218+ streamGenerator : ( ) =>
219+ new Readable ( {
220+ read ( ) {
221+ this . push ( transformedData )
222+ this . push ( null )
223+ } ,
224+ } ) ,
225+ unpacked,
226+ type : "file" ,
227+ stat : { mode : stat . mode , size } ,
228+ }
178229 }
179230
180231 // verify that the file is not a direct link or symlinked to access/copy a system file
181232 await this . protectSystemAndUnsafePaths ( file , await this . packager . info . getWorkspaceRoot ( ) )
182233
183- const config = {
234+ const baseConfig = {
184235 path : destination ,
185236 streamGenerator : ( ) => fs . createReadStream ( file ) ,
186237 unpacked,
187238 stat,
188239 }
189240
190- // file, stream directly
241+ // Handle regular files
191242 if ( ! stat . isSymbolicLink ( ) ) {
192- return {
193- ...config ,
194- type : "file" ,
195- }
243+ return { ...baseConfig , type : "file" }
196244 }
197245
198- // okay, it must be a symlink. evaluate link to be relative to source file in asar
246+ // Handle symlinks - make relative to source location
199247 let link = await readlink ( file )
200248 if ( path . isAbsolute ( link ) ) {
201249 link = path . relative ( path . dirname ( file ) , link )
202250 }
251+
203252 return {
204- ...config ,
253+ ...baseConfig ,
205254 type : "link" ,
206255 symlink : link ,
207256 }
@@ -241,31 +290,29 @@ export class AsarPackager {
241290 for ( const [ oldIndex , value ] of fileSet . transformedFiles ) {
242291 const newIndex = indexMap . get ( oldIndex )
243292 if ( newIndex === undefined ) {
244- const file = fileSet . files [ oldIndex ]
245- throw new Error ( `Internal error: ${ file } was lost while ordering asar` )
293+ throw new Error ( `Internal error: ${ fileSet . files [ oldIndex ] } was lost while ordering asar` )
246294 }
247-
248295 transformedFiles . set ( newIndex , value )
249296 }
250297 }
251298
252- const { src, destination, metadata } = fileSet
253-
254299 return {
255- src,
256- destination,
257- metadata,
300+ src : fileSet . src ,
301+ destination : fileSet . destination ,
302+ metadata : fileSet . metadata ,
258303 files : sortedFileEntries . map ( ( [ , file ] ) => file ) ,
259304 transformedFiles,
260305 }
261306 }
262307
263308 private async checkAgainstRoots ( target : string , allowRoots : string [ ] ) : Promise < boolean > {
264309 const resolved = await resolvePath ( target )
310+ if ( resolved == null || isEmptyOrSpaces ( resolved ) ) {
311+ return false
312+ }
265313
266314 for ( const root of allowRoots ) {
267- const resolvedRoot = root
268- if ( resolved === resolvedRoot || resolved ?. startsWith ( resolvedRoot + path . sep ) ) {
315+ if ( resolved === root || resolved . startsWith ( root + path . sep ) ) {
269316 return true
270317 }
271318 }
@@ -276,33 +323,25 @@ export class AsarPackager {
276323 const resolved = await resolvePath ( file )
277324 const logFields = { source : file , realPath : resolved }
278325
279- const isUnsafe = async ( ) => {
280- const workspace = await resolvePath ( workspaceRoot )
281-
282- if ( workspace && resolved ?. startsWith ( workspace ) ) {
283- // if in workspace, always safe
284- return false
285- }
286-
287- const allowed = await this . checkAgainstRoots ( file , await ALLOWLIST )
288- if ( allowed ) {
289- return false // allowlist is priority
290- }
326+ const workspace = await resolvePath ( workspaceRoot )
291327
292- const denied = await this . checkAgainstRoots ( file , await DENYLIST )
293- if ( denied ) {
294- log . error ( logFields , `denied access to system or unsafe path` )
295- return true
296- }
297- // default
298- log . debug ( logFields , `path is outside of explicit safe paths, defaulting to safe` )
299- return false
328+ // If in workspace, always safe
329+ if ( workspace && resolved ?. startsWith ( workspace ) ) {
330+ return
300331 }
301332
302- const unsafe = await isUnsafe ( )
333+ // Check allowlist (priority)
334+ if ( await this . checkAgainstRoots ( file , await ALLOWLIST ) ) {
335+ return
336+ }
303337
304- if ( unsafe ) {
338+ // Check denylist
339+ if ( await this . checkAgainstRoots ( file , await DENYLIST ) ) {
340+ log . error ( logFields , `denied access to system or unsafe path` )
305341 throw new Error ( `Cannot copy file [${ file } ] symlinked to file [${ resolved } ] outside the package to a system or unsafe path` )
306342 }
343+
344+ // Default: outside explicit paths but not explicitly denied
345+ log . debug ( logFields , `path is outside of explicit safe paths, defaulting to safe` )
307346 }
308347}
0 commit comments