@@ -4,6 +4,8 @@ mod compress;
44mod decompress;
55mod list;
66
7+ use std:: path:: PathBuf ;
8+
79use bstr:: ByteSlice ;
810use decompress:: { DecompressOptions , PreparedTarget , prepare_decompress_target} ;
911use rayon:: prelude:: { IndexedParallelIterator , IntoParallelRefIterator , ParallelIterator } ;
@@ -23,6 +25,7 @@ use crate::{
2325 self , BytesFmt , FileVisibilityPolicy , NoQuotePathFmt , PathFmt , QuestionAction , canonicalize, colors:: * ,
2426 file_size, is_path_stdin,
2527 } ,
28+ warning,
2629} ;
2730
2831/// Warn the user that (de)compressing this .zip archive might freeze their system.
@@ -132,7 +135,10 @@ pub fn run(args: CliArgs, question_policy: QuestionPolicy, file_visibility_polic
132135 } else if compress_result. is_err ( ) {
133136 // Sandbox blocks unlinking the partial file; tell the user to delete it.
134137 eprintln ! ( "{red}FATAL ERROR:\n " , red = * colors:: RED ) ;
135- eprintln ! ( " Compression did not finish; the partial file at {} may be corrupted." , PathFmt ( & output_path) ) ;
138+ eprintln ! (
139+ " Compression did not finish; the partial file at {} may be corrupted." ,
140+ PathFmt ( & output_path)
141+ ) ;
136142 eprintln ! ( " Please delete it manually." ) ;
137143 eprintln ! ( " Compression failed for reasons below." ) ;
138144 }
@@ -232,21 +238,29 @@ pub fn run(args: CliArgs, question_policy: QuestionPolicy, file_visibility_polic
232238 for p in & prepared {
233239 match p {
234240 PreparedTarget :: Archive { dir } => {
235- policy. allow_read_write ( sandbox:: canonicalize_for_sandbox ( dir) ) ;
241+ let canon = sandbox:: canonicalize_for_sandbox ( dir) ;
242+ // The sandbox cannot confine writes when the target is inside $HOME.
243+ if ( output_dir_was_explicit || here) && sandbox:: is_home_or_ancestor ( & canon) {
244+ warning ! (
245+ "Sandbox: extraction target {} is $HOME or an ancestor of it; the sandbox cannot meaningfully confine writes" ,
246+ PathFmt ( & canon)
247+ ) ;
248+ }
249+ policy. allow_read_write ( canon) ;
236250 }
237251 // Non-archive: held FD covers writes, no path grant needed.
238252 PreparedTarget :: NonArchive { .. } | PreparedTarget :: Cancelled => { }
239253 }
240254 }
241- // TODO: warn when that parent is $HOME or an ancestor.
242255 if remove {
243256 for f in & files {
244257 if is_path_stdin ( f) {
245258 continue ;
246259 }
247260 let canon = sandbox:: canonicalize_for_sandbox ( f) ;
248261 if let Some ( parent) = canon. parent ( ) {
249- policy. allow_read_write ( parent. to_path_buf ( ) ) ;
262+ // The decompressor can delete the input archive but not write nearby.
263+ policy. allow_remove_in ( parent. to_path_buf ( ) ) ;
250264 }
251265 }
252266 }
@@ -256,14 +270,12 @@ pub fn run(args: CliArgs, question_policy: QuestionPolicy, file_visibility_polic
256270 files
257271 . par_iter ( )
258272 . zip ( files_extensions)
259- . zip ( output_file_paths)
260273 . zip ( prepared)
261- . try_for_each ( |( ( ( input_path, formats) , output_file_path ) , prepared) | {
274+ . try_for_each ( |( ( input_path, formats) , prepared) | {
262275 decompress_file ( DecompressOptions {
263276 input_file_path : input_path,
264277 formats,
265278 output_dir : & output_dir,
266- output_file_path,
267279 output_dir_was_explicit,
268280 here,
269281 question_policy,
@@ -312,14 +324,27 @@ pub fn run(args: CliArgs, question_policy: QuestionPolicy, file_visibility_polic
312324 // Ensure we were not told to list the content of a non-archive compressed file
313325 check:: check_for_non_archive_formats ( & files, & formats) ?;
314326
327+ // Flatten the per-file formats up front so the next step can inspect them.
328+ let flat_formats: Vec < Vec < extension:: CompressionFormat > > = formats
329+ . iter ( )
330+ . map ( |f| extension:: flatten_compression_formats ( f) )
331+ . collect ( ) ;
332+ // Open RAR spill tempfiles up front so unrar can read from them under the sandbox.
333+ let mut rar_spill_tempfiles: Vec < Option < tempfile:: NamedTempFile > > = flat_formats
334+ . iter ( )
335+ . map ( |fs| {
336+ if fs. len ( ) > 1 && matches ! ( fs. last( ) , Some ( extension:: CompressionFormat :: Rar ) ) {
337+ tempfile:: NamedTempFile :: new ( ) . ok ( )
338+ } else {
339+ None
340+ }
341+ } )
342+ . collect ( ) ;
343+
315344 {
316345 let mut policy = SandboxPolicy :: new ( ) ;
317346 for f in & files {
318- let canon = sandbox:: canonicalize_for_sandbox ( f) ;
319- policy. allow_read ( canon. clone ( ) ) ;
320- if let Some ( parent) = canon. parent ( ) {
321- policy. allow_read ( parent. to_path_buf ( ) ) ;
322- }
347+ policy. allow_read ( sandbox:: canonicalize_for_sandbox ( f) ) ;
323348 }
324349 policy. set_disabled ( args. no_sandbox ) . apply ( ) ;
325350 }
@@ -329,11 +354,15 @@ pub fn run(args: CliArgs, question_policy: QuestionPolicy, file_visibility_polic
329354 quiet : args. quiet ,
330355 } ;
331356
332- for ( i, ( archive_path, formats) ) in files. iter ( ) . zip ( formats) . enumerate ( ) {
357+ for ( i, ( ( archive_path, formats) , spill) ) in files
358+ . iter ( )
359+ . zip ( flat_formats)
360+ . zip ( rar_spill_tempfiles. iter_mut ( ) )
361+ . enumerate ( )
362+ {
333363 if i > 0 && !args. quiet {
334364 println ! ( ) ;
335365 }
336- let formats = extension:: flatten_compression_formats ( & formats) ;
337366 list_archive_contents (
338367 archive_path,
339368 formats,
@@ -342,6 +371,7 @@ pub fn run(args: CliArgs, question_policy: QuestionPolicy, file_visibility_polic
342371 args. password
343372 . as_deref ( )
344373 . map ( |str| <[ u8 ] as ByteSlice >:: from_os_str ( str) . expect ( "convert password to bytes failed" ) ) ,
374+ spill. take ( ) ,
345375 ) ?;
346376 }
347377
0 commit comments