@@ -421,8 +421,8 @@ fn extract_file_inner<R: Read>(
421421 . ok_or_else ( || std:: io:: Error :: new ( ErrorKind :: Unsupported , "path not safe to extract" ) ) ?;
422422 let display_name = name. display ( ) . to_string ( ) ;
423423 let out_path = match output_directory {
424- Some ( output_directory) => output_directory. join ( name) ,
425- None => name,
424+ Some ( output_directory) => output_directory. join ( & * name) ,
425+ None => name. to_path_buf ( ) ,
426426 } ;
427427 progress_reporter. extraction_starting ( & display_name) ;
428428 log:: debug!(
@@ -437,55 +437,90 @@ fn extract_file_inner<R: Read>(
437437 if let Some ( parent) = out_path. parent ( ) {
438438 directory_creator. create_dir_all ( parent) ?;
439439 }
440- let out_file = File :: create ( & out_path) . map_err ( |e| RipunzipErrors :: IOErrorWithContext {
441- context : format ! ( "Failed to create file {}" , out_path. display( ) ) ,
442- source : e,
443- } ) ?;
444- // Progress bar strategy. The overall progress across the entire zip file must be
445- // denoted in terms of *compressed* bytes, since at the outset we don't know the uncompressed
446- // size of each file. Yet, within a given file, we update progress based on the bytes
447- // of uncompressed data written, once per 1MB, because that's the information that we happen
448- // to have available. So, calculate how many compressed bytes relate to 1MB of uncompressed
449- // data, and the remainder.
450- let uncompressed_size = file. size ( ) ;
451- let compressed_size = file. compressed_size ( ) ;
452- let mut progress_updater = ProgressUpdater :: new (
453- |external_progress| {
454- progress_reporter. bytes_extracted ( external_progress) ;
455- } ,
456- compressed_size,
457- uncompressed_size,
458- 1024 * 1024 ,
459- ) ;
460- let mut out_file = progress_streams:: ProgressWriter :: new ( out_file, |bytes_written| {
461- progress_updater. progress ( bytes_written as u64 )
462- } ) ;
463- // Using a BufWriter here doesn't improve performance even on a VM with
464- // spinny disks.
465- if let Err ( e) = std:: io:: copy ( & mut file, & mut out_file) {
466- return Err ( RipunzipErrors :: IOErrorWithContext {
467- context : format ! ( "Failed to write directory {:?}" , out_file. into_inner( ) ) ,
468- source : e,
440+ let is_symlink = {
441+ #[ cfg( unix) ]
442+ {
443+ file. is_symlink ( )
444+ }
445+ #[ cfg( not( unix) ) ]
446+ {
447+ false
448+ }
449+ } ;
450+ if is_symlink {
451+ #[ cfg( unix) ]
452+ {
453+ let mut target = String :: new ( ) ;
454+ file. read_to_string ( & mut target)
455+ . map_err ( |e| RipunzipErrors :: IOErrorWithContext {
456+ context : format ! ( "Failed to read symlink target for {}" , out_path. display( ) ) ,
457+ source : e,
458+ } ) ?;
459+ if let Err ( e) = std:: os:: unix:: fs:: symlink ( & target, & out_path) {
460+ return Err ( RipunzipErrors :: IOErrorWithContext {
461+ context : format ! (
462+ "Failed to create symlink {} -> {}" ,
463+ out_path. display( ) ,
464+ target
465+ ) ,
466+ source : e,
467+ } ) ;
468+ }
469+ }
470+ } else {
471+ let out_file =
472+ File :: create ( & out_path) . map_err ( |e| RipunzipErrors :: IOErrorWithContext {
473+ context : format ! ( "Failed to create file {}" , out_path. display( ) ) ,
474+ source : e,
475+ } ) ?;
476+ // Progress bar strategy. The overall progress across the entire zip file must be
477+ // denoted in terms of *compressed* bytes, since at the outset we don't know the uncompressed
478+ // size of each file. Yet, within a given file, we update progress based on the bytes
479+ // of uncompressed data written, once per 1MB, because that's the information that we happen
480+ // to have available. So, calculate how many compressed bytes relate to 1MB of uncompressed
481+ // data, and the remainder.
482+ let uncompressed_size = file. size ( ) ;
483+ let compressed_size = file. compressed_size ( ) ;
484+ let mut progress_updater = ProgressUpdater :: new (
485+ |external_progress| {
486+ progress_reporter. bytes_extracted ( external_progress) ;
487+ } ,
488+ compressed_size,
489+ uncompressed_size,
490+ 1024 * 1024 ,
491+ ) ;
492+ let mut out_file = progress_streams:: ProgressWriter :: new ( out_file, |bytes_written| {
493+ progress_updater. progress ( bytes_written as u64 )
469494 } ) ;
495+ // Using a BufWriter here doesn't improve performance even on a VM with
496+ // spinny disks.
497+ if let Err ( e) = std:: io:: copy ( & mut file, & mut out_file) {
498+ return Err ( RipunzipErrors :: IOErrorWithContext {
499+ context : format ! ( "Failed to write directory {:?}" , out_file. into_inner( ) ) ,
500+ source : e,
501+ } ) ;
502+ }
503+ progress_updater. finish ( ) ;
470504 }
471- progress_updater. finish ( ) ;
472505 }
473506
474507 #[ cfg( unix) ]
475508 {
476509 use std:: os:: unix:: fs:: PermissionsExt ;
477- if let Some ( mode) = file. unix_mode ( ) {
478- if let Err ( e) =
479- std:: fs:: set_permissions ( & out_path, std:: fs:: Permissions :: from_mode ( mode) )
480- {
481- return Err ( RipunzipErrors :: IOErrorWithContext {
482- context : format ! (
483- "Failed to set permissions {} for {}" ,
484- mode,
485- out_path. display( )
486- ) ,
487- source : e,
488- } ) ;
510+ if !file. is_symlink ( ) {
511+ if let Some ( mode) = file. unix_mode ( ) {
512+ if let Err ( e) =
513+ std:: fs:: set_permissions ( & out_path, std:: fs:: Permissions :: from_mode ( mode) )
514+ {
515+ return Err ( RipunzipErrors :: IOErrorWithContext {
516+ context : format ! (
517+ "Failed to set permissions {} for {}" ,
518+ mode,
519+ out_path. display( )
520+ ) ,
521+ source : e,
522+ } ) ;
523+ }
489524 }
490525 }
491526 }
@@ -613,6 +648,41 @@ mod tests {
613648 assert_eq ! ( read_to_string( c) . unwrap( ) , "Contents of C\n " ) ;
614649 }
615650
651+ #[ test]
652+ #[ cfg( unix) ]
653+ fn test_extract_symlink ( ) {
654+ let td = tempdir ( ) . unwrap ( ) ;
655+ let zf = td. path ( ) . join ( "z.zip" ) ;
656+ let file = File :: create ( & zf) . unwrap ( ) ;
657+ let mut zip = ZipWriter :: new ( file) ;
658+ let options: FileOptions < ExtendedFileOptions > = FileOptions :: default ( )
659+ . compression_method ( zip:: CompressionMethod :: Stored )
660+ . unix_permissions ( 0o755 ) ;
661+ zip. add_directory :: < _ , ExtendedFileOptions > ( "test/" , Default :: default ( ) )
662+ . unwrap ( ) ;
663+ zip. start_file ( "test/a.txt" , options. clone ( ) ) . unwrap ( ) ;
664+ zip. write_all ( b"Contents of A\n " ) . unwrap ( ) ;
665+ zip. add_symlink ( "test/b.txt" , "a.txt" , options. clone ( ) )
666+ . unwrap ( ) ;
667+ zip. finish ( ) . unwrap ( ) ;
668+
669+ let zf = File :: open ( zf) . unwrap ( ) ;
670+ let outdir = td. path ( ) . join ( "outdir" ) ;
671+ let options = UnzipOptions {
672+ output_directory : Some ( outdir. clone ( ) ) ,
673+ password : None ,
674+ single_threaded : false ,
675+ filename_filter : None ,
676+ progress_reporter : Box :: new ( NullProgressReporter ) ,
677+ } ;
678+ UnzipEngine :: for_file ( zf) . unwrap ( ) . unzip ( options) . unwrap ( ) ;
679+
680+ let a = outdir. join ( "test/a.txt" ) ;
681+ let b = outdir. join ( "test/b.txt" ) ;
682+ assert_eq ! ( read_to_string( a) . unwrap( ) , "Contents of A\n " ) ;
683+ assert_eq ! ( read_to_string( b) . unwrap( ) , "Contents of A\n " ) ;
684+ }
685+
616686 #[ test]
617687 #[ ignore] // because the chdir changes global state
618688 fn test_extract_no_path ( ) {
0 commit comments