@@ -199,13 +199,27 @@ impl Config {
199199 & self ,
200200 location : Location ,
201201 source : Source ,
202+ ) -> IoResult < PathBuf > {
203+ self . resolve_installation_dir_or_install_with_fixup ( location, source, |_| Ok ( ( ) ) )
204+ }
205+
206+ /// Resolves the dependency directory, installing and fixing it up if needed.
207+ ///
208+ /// The `fixup` hook runs once after a fresh archive extraction. It receives
209+ /// the installed exocrate directory path and can rewrite files, adjust
210+ /// permissions, or perform other environment-local repairs.
211+ pub fn resolve_installation_dir_or_install_with_fixup (
212+ & self ,
213+ location : Location ,
214+ source : Source ,
215+ fixup : impl FnOnce ( & Path ) -> IoResult < ( ) > ,
202216 ) -> IoResult < PathBuf > {
203217 let dir_path = self . dir_path ( location) ;
204218 if ManagedDirName :: new ( & dir_path) . check_exists ( ) . is_ok ( ) {
205219 return Ok ( dir_path) ;
206220 }
207221 let ( reader, expected_sha) = self . open_source ( source) ?;
208- install ( reader, & dir_path, expected_sha) ?;
222+ install_with_fixup ( reader, & dir_path, expected_sha, fixup ) ?;
209223 Ok ( dir_path)
210224 }
211225
@@ -268,7 +282,17 @@ impl Config {
268282
269283/// Extracts the `.tar.zst` from `reader` and installs it at `dst`, optionally
270284/// validating its hash.
285+ #[ cfg( test) ]
271286fn install ( mut reader : impl Read , dst : & Path , expected_sha256 : Option < [ u8 ; 32 ] > ) -> IoResult < ( ) > {
287+ install_with_fixup ( & mut reader, dst, expected_sha256, |_| Ok ( ( ) ) )
288+ }
289+
290+ fn install_with_fixup (
291+ mut reader : impl Read ,
292+ dst : & Path ,
293+ expected_sha256 : Option < [ u8 ; 32 ] > ,
294+ fixup : impl FnOnce ( & Path ) -> IoResult < ( ) > ,
295+ ) -> IoResult < ( ) > {
272296 struct HashingReader < R > {
273297 reader : R ,
274298 hasher : sha2:: Sha256 ,
@@ -282,14 +306,20 @@ fn install(mut reader: impl Read, dst: &Path, expected_sha256: Option<[u8; 32]>)
282306 }
283307 }
284308
285- sync:: ManagedDirName :: new ( dst)
286- . check_exists_or_create ( |target_dir| {
309+ fn unpack_tar_zst ( reader : impl Read , target_dir : & Path ) -> IoResult < ( ) > {
310+ let decoder = zstd:: stream:: read:: Decoder :: new ( reader) ?;
311+ let mut archive = tar:: Archive :: new ( decoder) ;
312+ archive. set_mask ( 0 ) ;
313+ archive. set_preserve_permissions ( true ) ;
314+ archive. unpack ( target_dir)
315+ }
316+
317+ let ( _dir, created) =
318+ sync:: ManagedDirName :: new ( dst) . check_exists_or_create_with_status ( |target_dir| {
287319 if let Some ( expected) = expected_sha256 {
288320 let mut hash_reader = HashingReader { reader, hasher : sha2:: Sha256 :: new ( ) } ;
289321 {
290- let decoder = zstd:: stream:: read:: Decoder :: new ( & mut hash_reader) ?;
291- let mut archive = tar:: Archive :: new ( decoder) ;
292- archive. unpack ( target_dir) ?;
322+ unpack_tar_zst ( & mut hash_reader, target_dir) ?;
293323 }
294324
295325 // Ensure any remaining trailing bytes in the stream are read
@@ -308,13 +338,19 @@ fn install(mut reader: impl Read, dst: &Path, expected_sha256: Option<[u8; 32]>)
308338 ) ) ;
309339 }
310340 } else {
311- let decoder = zstd:: stream:: read:: Decoder :: new ( & mut reader) ?;
312- let mut archive = tar:: Archive :: new ( decoder) ;
313- archive. unpack ( target_dir) ?;
341+ unpack_tar_zst ( & mut reader, target_dir) ?;
314342 }
315343 Ok ( ( ) )
316- } )
317- . map ( |_| ( ) )
344+ } ) ?;
345+
346+ if created {
347+ if let Err ( e) = fixup ( dst) {
348+ let _ = std:: fs:: remove_dir_all ( dst) ;
349+ return Err ( e) ;
350+ }
351+ }
352+
353+ Ok ( ( ) )
318354}
319355
320356/// Parses a [`RemoteArchive`] from the `Cargo.toml` at `$cargo_toml_path`.
@@ -545,13 +581,19 @@ mod tests {
545581 use super :: * ;
546582
547583 fn create_dummy_tar_zst ( files : & [ ( & str , & [ u8 ] ) ] ) -> Vec < u8 > {
584+ let files =
585+ files. iter ( ) . map ( |( name, content) | ( * name, * content, 0o644 ) ) . collect :: < Vec < _ > > ( ) ;
586+ create_dummy_tar_zst_with_modes ( & files)
587+ }
588+
589+ fn create_dummy_tar_zst_with_modes ( files : & [ ( & str , & [ u8 ] , u32 ) ] ) -> Vec < u8 > {
548590 let mut zstd_enc = zstd:: stream:: write:: Encoder :: new ( Vec :: new ( ) , 0 ) . unwrap ( ) ;
549591 {
550592 let mut tar_builder = tar:: Builder :: new ( & mut zstd_enc) ;
551- for ( name, content) in files {
593+ for ( name, content, mode ) in files {
552594 let mut header = tar:: Header :: new_gnu ( ) ;
553595 header. set_size ( content. len ( ) as u64 ) ;
554- header. set_mode ( 0o644 ) ;
596+ header. set_mode ( * mode ) ;
555597 header. set_cksum ( ) ;
556598 tar_builder. append_data ( & mut header, name, * content) . unwrap ( ) ;
557599 }
@@ -595,6 +637,71 @@ mod tests {
595637 assert_eq ! ( fs:: read( dst. join( "nested/dir/data.bin" ) ) . unwrap( ) , b"\x01 \x02 \x03 " ) ;
596638 }
597639
640+ #[ test]
641+ fn test_install_with_fixup ( ) {
642+ let temp = tempfile:: tempdir ( ) . unwrap ( ) ;
643+ let dst = temp. path ( ) . join ( "install_target" ) ;
644+ let tar_zst = create_dummy_tar_zst ( & [ ( "hello.txt" , b"hello world" ) ] ) ;
645+
646+ install_with_fixup ( tar_zst. as_slice ( ) , & dst, None , |dir| {
647+ fs:: write ( dir. join ( "fixed.txt" ) , "fixed" )
648+ } )
649+ . unwrap ( ) ;
650+
651+ assert_eq ! ( fs:: read_to_string( dst. join( "hello.txt" ) ) . unwrap( ) , "hello world" ) ;
652+ assert_eq ! ( fs:: read_to_string( dst. join( "fixed.txt" ) ) . unwrap( ) , "fixed" ) ;
653+ }
654+
655+ #[ test]
656+ fn test_install_fixup_not_rerun_for_existing_dir ( ) {
657+ let temp = tempfile:: tempdir ( ) . unwrap ( ) ;
658+ let dst = temp. path ( ) . join ( "install_target" ) ;
659+ fs:: create_dir_all ( & dst) . unwrap ( ) ;
660+ fs:: write ( dst. join ( "existing.txt" ) , "existing content" ) . unwrap ( ) ;
661+
662+ let bad_data = b"invalid archive data" ;
663+ install_with_fixup ( & bad_data[ ..] , & dst, None , |_| {
664+ panic ! ( "fixup should not run for an existing installation" ) ;
665+ } )
666+ . unwrap ( ) ;
667+
668+ assert_eq ! ( fs:: read_to_string( dst. join( "existing.txt" ) ) . unwrap( ) , "existing content" ) ;
669+ }
670+
671+ #[ test]
672+ fn test_install_fixup_failure_removes_installation ( ) {
673+ let temp = tempfile:: tempdir ( ) . unwrap ( ) ;
674+ let dst = temp. path ( ) . join ( "install_target" ) ;
675+ let tar_zst = create_dummy_tar_zst ( & [ ( "hello.txt" , b"hello world" ) ] ) ;
676+
677+ let result = install_with_fixup ( tar_zst. as_slice ( ) , & dst, None , |_| {
678+ Err ( std:: io:: Error :: new ( std:: io:: ErrorKind :: Other , "fixup failed" ) )
679+ } ) ;
680+
681+ assert ! ( result. is_err( ) ) ;
682+ assert ! ( !dst. exists( ) ) ;
683+ }
684+
685+ #[ cfg( unix) ]
686+ #[ test]
687+ fn test_install_preserves_read_only_permissions ( ) {
688+ use std:: os:: unix:: fs:: PermissionsExt as _;
689+
690+ let temp = tempfile:: tempdir ( ) . unwrap ( ) ;
691+ let dst = temp. path ( ) . join ( "install_target" ) ;
692+ let tar_zst = create_dummy_tar_zst_with_modes ( & [
693+ ( "readonly.txt" , b"locked" , 0o444 ) ,
694+ ( "bin/tool" , b"tool" , 0o555 ) ,
695+ ] ) ;
696+
697+ install ( tar_zst. as_slice ( ) , & dst, None ) . unwrap ( ) ;
698+
699+ let readonly_mode = fs:: metadata ( dst. join ( "readonly.txt" ) ) . unwrap ( ) . permissions ( ) . mode ( ) ;
700+ let tool_mode = fs:: metadata ( dst. join ( "bin/tool" ) ) . unwrap ( ) . permissions ( ) . mode ( ) ;
701+ assert_eq ! ( readonly_mode & 0o777 , 0o444 ) ;
702+ assert_eq ! ( tool_mode & 0o777 , 0o555 ) ;
703+ }
704+
598705 #[ test]
599706 fn test_install_without_hash_validation ( ) {
600707 let temp = tempfile:: tempdir ( ) . unwrap ( ) ;
0 commit comments