@@ -4,7 +4,7 @@ use std::fs;
4
4
use std:: io:: Write as _;
5
5
#[ cfg( not( windows) ) ]
6
6
use std:: os:: unix:: fs:: PermissionsExt as _;
7
- use std:: path:: Path ;
7
+ use std:: path:: { Path , PathBuf } ;
8
8
9
9
use anyhow:: { anyhow, bail, Context as _, Result } ;
10
10
use clap:: { Arg , ArgAction , ArgMatches , Command } ;
@@ -13,6 +13,7 @@ use itertools::Itertools as _;
13
13
use log:: { debug, info, warn} ;
14
14
use sha1_smol:: Digest ;
15
15
use symbolic:: common:: ByteView ;
16
+ use walkdir:: WalkDir ;
16
17
use zip:: write:: SimpleFileOptions ;
17
18
use zip:: { DateTime , ZipWriter } ;
18
19
@@ -21,7 +22,6 @@ use crate::config::Config;
21
22
use crate :: utils:: args:: ArgExt as _;
22
23
use crate :: utils:: chunks:: { upload_chunks, Chunk , ASSEMBLE_POLL_INTERVAL } ;
23
24
use crate :: utils:: fs:: get_sha1_checksums;
24
- #[ cfg( all( target_os = "macos" , target_arch = "aarch64" ) ) ]
25
25
use crate :: utils:: fs:: TempDir ;
26
26
use crate :: utils:: fs:: TempFile ;
27
27
#[ cfg( all( target_os = "macos" , target_arch = "aarch64" ) ) ]
@@ -95,9 +95,10 @@ pub fn execute(matches: &ArgMatches) -> Result<()> {
95
95
let byteview = ByteView :: open ( path) ?;
96
96
debug ! ( "Loaded file with {} bytes" , byteview. len( ) ) ;
97
97
98
+ let temp_dir = TempDir :: create ( ) ?;
98
99
#[ cfg( all( target_os = "macos" , target_arch = "aarch64" ) ) ]
99
100
if is_apple_app ( path) {
100
- handle_asset_catalogs ( path) ;
101
+ handle_asset_catalogs ( path, temp_dir . path ( ) ) ;
101
102
}
102
103
103
104
validate_is_mobile_app ( path, & byteview) ?;
@@ -107,7 +108,7 @@ pub fn execute(matches: &ArgMatches) -> Result<()> {
107
108
handle_file ( path, & byteview) ?
108
109
} else if path. is_dir ( ) {
109
110
debug ! ( "Normalizing directory: {}" , path. display( ) ) ;
110
- normalize_directory ( path) . with_context ( || {
111
+ normalize_directory ( path, temp_dir . path ( ) ) . with_context ( || {
111
112
format ! (
112
113
"Failed to generate uploadable bundle for directory {}" ,
113
114
path. display( )
@@ -187,9 +188,10 @@ fn handle_file(path: &Path, byteview: &ByteView) -> Result<TempFile> {
187
188
#[ cfg( all( target_os = "macos" , target_arch = "aarch64" ) ) ]
188
189
if is_zip_file ( byteview) && is_ipa_file ( byteview) ? {
189
190
debug ! ( "Converting IPA file to XCArchive structure" ) ;
190
- let temp_dir = TempDir :: create ( ) ?;
191
- return ipa_to_xcarchive ( path, byteview, & temp_dir)
192
- . and_then ( |path| normalize_directory ( & path) )
191
+ let archive_temp_dir = TempDir :: create ( ) ?;
192
+ let assets_temp_dir = TempDir :: create ( ) ?;
193
+ return ipa_to_xcarchive ( path, byteview, & archive_temp_dir)
194
+ . and_then ( |path| normalize_directory ( & path, assets_temp_dir. path ( ) ) )
193
195
. with_context ( || format ! ( "Failed to process IPA file {}" , path. display( ) ) ) ;
194
196
}
195
197
@@ -276,38 +278,36 @@ fn normalize_file(path: &Path, bytes: &[u8]) -> Result<TempFile> {
276
278
Ok ( temp_file)
277
279
}
278
280
281
+ fn sort_entries ( path : & Path ) -> Result < std:: vec:: IntoIter < ( PathBuf , PathBuf ) > > {
282
+ Ok ( WalkDir :: new ( path)
283
+ . follow_links ( true )
284
+ . into_iter ( )
285
+ . filter_map ( Result :: ok)
286
+ . filter ( |entry| entry. path ( ) . is_file ( ) )
287
+ . map ( |entry| {
288
+ let entry_path = entry. into_path ( ) ;
289
+ let relative_path = entry_path. strip_prefix ( path) ?. to_owned ( ) ;
290
+ Ok ( ( entry_path, relative_path) )
291
+ } )
292
+ . collect :: < Result < Vec < _ > > > ( ) ?
293
+ . into_iter ( )
294
+ . sorted_by ( |( _, a) , ( _, b) | a. cmp ( b) ) )
295
+ }
296
+
279
297
// For XCArchive directories, we'll zip the entire directory
280
- fn normalize_directory ( path : & Path ) -> Result < TempFile > {
298
+ fn normalize_directory ( path : & Path , parsed_assets_path : & Path ) -> Result < TempFile > {
281
299
debug ! ( "Creating normalized zip for directory: {}" , path. display( ) ) ;
282
300
283
301
let temp_file = TempFile :: create ( ) ?;
284
302
let mut zip = ZipWriter :: new ( temp_file. open ( ) ?) ;
285
303
286
304
let mut file_count = 0 ;
305
+ let directory_name = path. file_name ( ) . expect ( "Failed to get basename" ) ;
287
306
288
307
// Collect and sort entries for deterministic ordering
289
308
// This is important to ensure stable sha1 checksums for the zip file as
290
309
// an optimization is used to avoid re-uploading the same chunks if they're already on the server.
291
- let entries = walkdir:: WalkDir :: new ( path)
292
- . follow_links ( true )
293
- . into_iter ( )
294
- . filter_map ( Result :: ok)
295
- . filter ( |entry| entry. path ( ) . is_file ( ) )
296
- . map ( |entry| {
297
- let entry_path = entry. into_path ( ) ;
298
- let relative_path = entry_path
299
- . strip_prefix ( path. parent ( ) . ok_or_else ( || {
300
- anyhow ! (
301
- "Cannot determine parent directory for path: {}" ,
302
- path. display( )
303
- )
304
- } ) ?) ?
305
- . to_owned ( ) ;
306
- Ok ( ( entry_path, relative_path) )
307
- } )
308
- . collect :: < Result < Vec < _ > > > ( ) ?
309
- . into_iter ( )
310
- . sorted_by ( |( _, a) , ( _, b) | a. cmp ( b) ) ;
310
+ let entries = sort_entries ( path) ?;
311
311
312
312
// Need to set the last modified time to a fixed value to ensure consistent checksums
313
313
// This is important as an optimization to avoid re-uploading the same chunks if they're already on the server
@@ -317,18 +317,47 @@ fn normalize_directory(path: &Path) -> Result<TempFile> {
317
317
. last_modified_time ( DateTime :: default ( ) ) ;
318
318
319
319
for ( entry_path, relative_path) in entries {
320
- debug ! ( "Adding file to zip: {}" , relative_path. display( ) ) ;
320
+ let zip_path = format ! (
321
+ "{}/{}" ,
322
+ directory_name. to_string_lossy( ) ,
323
+ relative_path. to_string_lossy( )
324
+ ) ;
325
+ debug ! ( "Adding file to zip: {}" , zip_path) ;
321
326
322
327
#[ cfg( not( windows) ) ]
323
328
// On Unix, we need to preserve the file permissions.
324
329
let options = options. unix_permissions ( fs:: metadata ( & entry_path) ?. permissions ( ) . mode ( ) ) ;
325
330
326
- zip. start_file ( relative_path . to_string_lossy ( ) , options) ?;
331
+ zip. start_file ( zip_path , options) ?;
327
332
let file_byteview = ByteView :: open ( & entry_path) ?;
328
333
zip. write_all ( file_byteview. as_slice ( ) ) ?;
329
334
file_count += 1 ;
330
335
}
331
336
337
+ // Add parsed assets to the zip in a "ParsedAssets" directory
338
+ if parsed_assets_path. exists ( ) {
339
+ debug ! (
340
+ "Adding parsed assets from: {}" ,
341
+ parsed_assets_path. display( )
342
+ ) ;
343
+
344
+ let parsed_assets_entries = sort_entries ( parsed_assets_path) ?;
345
+
346
+ for ( entry_path, relative_path) in parsed_assets_entries {
347
+ let zip_path = format ! (
348
+ "{}/ParsedAssets/{}" ,
349
+ directory_name. to_string_lossy( ) ,
350
+ relative_path. to_string_lossy( )
351
+ ) ;
352
+ debug ! ( "Adding parsed asset to zip: {}" , zip_path) ;
353
+
354
+ zip. start_file ( zip_path, options) ?;
355
+ let file_byteview = ByteView :: open ( & entry_path) ?;
356
+ zip. write_all ( file_byteview. as_slice ( ) ) ?;
357
+ file_count += 1 ;
358
+ }
359
+ }
360
+
332
361
zip. finish ( ) ?;
333
362
debug ! (
334
363
"Successfully created normalized zip for directory with {} files" ,
@@ -470,7 +499,7 @@ mod tests {
470
499
fs:: create_dir_all ( test_dir. join ( "Products" ) ) ?;
471
500
fs:: write ( test_dir. join ( "Products" ) . join ( "app.txt" ) , "test content" ) ?;
472
501
473
- let result_zip = normalize_directory ( & test_dir) ?;
502
+ let result_zip = normalize_directory ( & test_dir, temp_dir . path ( ) ) ?;
474
503
let zip_file = fs:: File :: open ( result_zip. path ( ) ) ?;
475
504
let mut archive = ZipArchive :: new ( zip_file) ?;
476
505
let file = archive. by_index ( 0 ) ?;
0 commit comments