@@ -4,7 +4,7 @@ use std::fs;
44use std:: io:: Write as _;
55#[ cfg( not( windows) ) ]
66use std:: os:: unix:: fs:: PermissionsExt as _;
7- use std:: path:: Path ;
7+ use std:: path:: { Path , PathBuf } ;
88
99use anyhow:: { anyhow, bail, Context as _, Result } ;
1010use clap:: { Arg , ArgAction , ArgMatches , Command } ;
@@ -13,6 +13,7 @@ use itertools::Itertools as _;
1313use log:: { debug, info, warn} ;
1414use sha1_smol:: Digest ;
1515use symbolic:: common:: ByteView ;
16+ use walkdir:: WalkDir ;
1617use zip:: write:: SimpleFileOptions ;
1718use zip:: { DateTime , ZipWriter } ;
1819
@@ -21,7 +22,6 @@ use crate::config::Config;
2122use crate :: utils:: args:: ArgExt as _;
2223use crate :: utils:: chunks:: { upload_chunks, Chunk , ASSEMBLE_POLL_INTERVAL } ;
2324use crate :: utils:: fs:: get_sha1_checksums;
24- #[ cfg( all( target_os = "macos" , target_arch = "aarch64" ) ) ]
2525use crate :: utils:: fs:: TempDir ;
2626use crate :: utils:: fs:: TempFile ;
2727#[ cfg( all( target_os = "macos" , target_arch = "aarch64" ) ) ]
@@ -95,9 +95,10 @@ pub fn execute(matches: &ArgMatches) -> Result<()> {
9595 let byteview = ByteView :: open ( path) ?;
9696 debug ! ( "Loaded file with {} bytes" , byteview. len( ) ) ;
9797
98+ let temp_dir = TempDir :: create ( ) ?;
9899 #[ cfg( all( target_os = "macos" , target_arch = "aarch64" ) ) ]
99100 if is_apple_app ( path) {
100- handle_asset_catalogs ( path) ;
101+ handle_asset_catalogs ( path, temp_dir . path ( ) ) ;
101102 }
102103
103104 validate_is_mobile_app ( path, & byteview) ?;
@@ -107,7 +108,7 @@ pub fn execute(matches: &ArgMatches) -> Result<()> {
107108 handle_file ( path, & byteview) ?
108109 } else if path. is_dir ( ) {
109110 debug ! ( "Normalizing directory: {}" , path. display( ) ) ;
110- normalize_directory ( path) . with_context ( || {
111+ normalize_directory ( path, temp_dir . path ( ) ) . with_context ( || {
111112 format ! (
112113 "Failed to generate uploadable bundle for directory {}" ,
113114 path. display( )
@@ -187,9 +188,10 @@ fn handle_file(path: &Path, byteview: &ByteView) -> Result<TempFile> {
187188 #[ cfg( all( target_os = "macos" , target_arch = "aarch64" ) ) ]
188189 if is_zip_file ( byteview) && is_ipa_file ( byteview) ? {
189190 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 ( ) ) )
193195 . with_context ( || format ! ( "Failed to process IPA file {}" , path. display( ) ) ) ;
194196 }
195197
@@ -276,38 +278,36 @@ fn normalize_file(path: &Path, bytes: &[u8]) -> Result<TempFile> {
276278 Ok ( temp_file)
277279}
278280
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+
279297// 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 > {
281299 debug ! ( "Creating normalized zip for directory: {}" , path. display( ) ) ;
282300
283301 let temp_file = TempFile :: create ( ) ?;
284302 let mut zip = ZipWriter :: new ( temp_file. open ( ) ?) ;
285303
286304 let mut file_count = 0 ;
305+ let directory_name = path. file_name ( ) . expect ( "Failed to get basename" ) ;
287306
288307 // Collect and sort entries for deterministic ordering
289308 // This is important to ensure stable sha1 checksums for the zip file as
290309 // 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) ?;
311311
312312 // Need to set the last modified time to a fixed value to ensure consistent checksums
313313 // 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> {
317317 . last_modified_time ( DateTime :: default ( ) ) ;
318318
319319 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) ;
321326
322327 #[ cfg( not( windows) ) ]
323328 // On Unix, we need to preserve the file permissions.
324329 let options = options. unix_permissions ( fs:: metadata ( & entry_path) ?. permissions ( ) . mode ( ) ) ;
325330
326- zip. start_file ( relative_path . to_string_lossy ( ) , options) ?;
331+ zip. start_file ( zip_path , options) ?;
327332 let file_byteview = ByteView :: open ( & entry_path) ?;
328333 zip. write_all ( file_byteview. as_slice ( ) ) ?;
329334 file_count += 1 ;
330335 }
331336
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+
332361 zip. finish ( ) ?;
333362 debug ! (
334363 "Successfully created normalized zip for directory with {} files" ,
@@ -470,7 +499,7 @@ mod tests {
470499 fs:: create_dir_all ( test_dir. join ( "Products" ) ) ?;
471500 fs:: write ( test_dir. join ( "Products" ) . join ( "app.txt" ) , "test content" ) ?;
472501
473- let result_zip = normalize_directory ( & test_dir) ?;
502+ let result_zip = normalize_directory ( & test_dir, temp_dir . path ( ) ) ?;
474503 let zip_file = fs:: File :: open ( result_zip. path ( ) ) ?;
475504 let mut archive = ZipArchive :: new ( zip_file) ?;
476505 let file = archive. by_index ( 0 ) ?;
0 commit comments