this repo has no description
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

fix(pipeline): preserve per-stage config via Compressor::clone_boxed

Pipeline previously rebuilt each stage from its name (e.g. "gzip") using the
default-level constructor, so user-supplied settings like --level 9 were
silently dropped inside a compound archive. Replace the name round-trip with a
trait-level clone_boxed so owned, fully-configured stages reach worker threads.

+118 -18
+5
src/backends/brotli.rs
··· 52 52 pub progress_args: ProgressArgs, 53 53 } 54 54 55 + #[derive(Clone)] 55 56 pub struct Brotli { 56 57 pub compression_level: i32, 57 58 pub progress_args: ProgressArgs, ··· 85 86 /// Full name for brotli. 86 87 fn name(&self) -> &str { 87 88 "brotli" 89 + } 90 + 91 + fn clone_boxed(&self) -> Box<dyn Compressor> { 92 + Box::new(self.clone()) 88 93 } 89 94 90 95 /// Compress an input file or pipe to a brotli archive
+5
src/backends/bzip2.rs
··· 46 46 pub level_args: LevelArgs, 47 47 } 48 48 49 + #[derive(Clone)] 49 50 pub struct Bzip2 { 50 51 pub level: i32, // 1-9 51 52 pub progress_args: ProgressArgs, ··· 79 80 /// Name of this compressor 80 81 fn name(&self) -> &str { 81 82 "bzip2" 83 + } 84 + 85 + fn clone_boxed(&self) -> Box<dyn Compressor> { 86 + Box::new(self.clone()) 82 87 } 83 88 84 89 /// Compress an input file or pipe to a bz2 archive
+5
src/backends/gzip.rs
··· 20 20 pub progress_args: ProgressArgs, 21 21 } 22 22 23 + #[derive(Clone)] 23 24 pub struct Gzip { 24 25 pub compression_level: i32, 25 26 pub progress_args: ProgressArgs, ··· 53 54 /// Full name for gzip. 54 55 fn name(&self) -> &str { 55 56 "gzip" 57 + } 58 + 59 + fn clone_boxed(&self) -> Box<dyn Compressor> { 60 + Box::new(self.clone()) 56 61 } 57 62 58 63 /// Compress an input file or pipe to a gzip archive
+5 -1
src/backends/lz4.rs
··· 13 13 pub progress_args: ProgressArgs, 14 14 } 15 15 16 - #[derive(Default)] 16 + #[derive(Default, Clone)] 17 17 pub struct Lz4 { 18 18 pub progress_args: ProgressArgs, 19 19 } ··· 35 35 /// Full name for lz4. 36 36 fn name(&self) -> &str { 37 37 "lz4" 38 + } 39 + 40 + fn clone_boxed(&self) -> Box<dyn Compressor> { 41 + Box::new(self.clone()) 38 42 } 39 43 40 44 /// Compress an input file or pipe to a lz4 archive
+5
src/backends/lzma.rs
··· 45 45 pub level_args: LevelArgs, 46 46 } 47 47 48 + #[derive(Clone)] 48 49 pub struct Lzma { 49 50 pub level: i32, 50 51 pub progress_args: ProgressArgs, ··· 89 90 /// Full name for lzma. 90 91 fn name(&self) -> &str { 91 92 "lzma" 93 + } 94 + 95 + fn clone_boxed(&self) -> Box<dyn Compressor> { 96 + Box::new(self.clone()) 92 97 } 93 98 94 99 fn compress(&self, input: CmprssInput, output: CmprssOutput) -> Result {
+52 -14
src/backends/pipeline.rs
··· 27 27 .collect::<Vec<&str>>() 28 28 .join(".") 29 29 } 30 - 31 - fn create_compressor(name: &str) -> Result<Box<dyn Compressor>> { 32 - crate::backends::compressor_from_str(name) 33 - .ok_or_else(|| anyhow!("Unknown compressor type: {}", name)) 34 - } 35 30 } 36 31 37 32 /// A reader that reads from a receiver channel ··· 146 141 .name() 147 142 } 148 143 144 + fn clone_boxed(&self) -> Box<dyn Compressor> { 145 + Box::new(Pipeline { 146 + compressors: self.compressors.iter().map(|c| c.clone_boxed()).collect(), 147 + }) 148 + } 149 + 149 150 fn extension(&self) -> &str { 150 151 self.compressors 151 152 .last() ··· 205 206 return self.compressors[0].compress(input, output); 206 207 } 207 208 208 - let mut op_compressors: Vec<Box<dyn Compressor>> = self 209 - .compressors 210 - .iter() 211 - .map(|c| Self::create_compressor(c.name())) 212 - .collect::<Result<Vec<_>>>()?; 209 + let mut op_compressors: Vec<Box<dyn Compressor>> = 210 + self.compressors.iter().map(|c| c.clone_boxed()).collect(); 213 211 214 212 let mut handles = Vec::new(); 215 213 let mut current_thread_input = input; // Consumed by the first (innermost) compressor ··· 259 257 .compressors 260 258 .iter() 261 259 .rev() 262 - .map(|c| Self::create_compressor(c.name())) 263 - .collect::<Result<Vec<_>>>()?; 260 + .map(|c| c.clone_boxed()) 261 + .collect(); 264 262 265 263 let mut handles = Vec::new(); 266 264 let mut current_thread_input = input; // Consumed by the first (outermost) extractor ··· 331 329 .compressors 332 330 .iter() 333 331 .rev() 334 - .map(|c| Self::create_compressor(c.name())) 335 - .collect::<Result<Vec<_>>>()?; 332 + .map(|c| c.clone_boxed()) 333 + .collect(); 336 334 337 335 let mut handles = Vec::new(); 338 336 let mut current_thread_input = input; ··· 404 402 405 403 let extracted_content = fs::read_to_string(extracted_file)?; 406 404 assert_eq!(extracted_content, test_content); 405 + 406 + Ok(()) 407 + } 408 + 409 + /// Regression test: per-stage configuration (e.g. `--level 1` vs 410 + /// `--level 9` on the outer gzip of a `.tar.gz`) must survive the 411 + /// thread-dispatch in `Pipeline::compress`. Previously the pipeline 412 + /// reconstructed each stage from its *name* alone, producing a default 413 + /// Gzip regardless of the level the user requested. 414 + #[test] 415 + fn test_pipeline_preserves_stage_config() -> Result { 416 + use crate::progress::ProgressArgs; 417 + 418 + let temp_dir = tempdir()?; 419 + let input = temp_dir.path().join("input.txt"); 420 + // Repetitive content amplifies the level difference in output size. 421 + fs::write(&input, "0123456789abcdef".repeat(1024))?; 422 + 423 + let run = |level: i32, suffix: &str| -> Result<u64> { 424 + let fast = Pipeline::new(vec![ 425 + Box::new(crate::backends::Tar::default()), 426 + Box::new(crate::backends::Gzip { 427 + compression_level: level, 428 + progress_args: ProgressArgs::default(), 429 + }), 430 + ]); 431 + let out = temp_dir.path().join(format!("out.{suffix}.tar.gz")); 432 + fast.compress( 433 + CmprssInput::Path(vec![input.clone()]), 434 + CmprssOutput::Path(out.clone()), 435 + )?; 436 + Ok(fs::metadata(&out)?.len()) 437 + }; 438 + 439 + let fast_size = run(1, "fast")?; 440 + let best_size = run(9, "best")?; 441 + assert!( 442 + best_size < fast_size, 443 + "expected best (level 9) to be smaller than fast (level 1), got {best_size} >= {fast_size}", 444 + ); 407 445 408 446 Ok(()) 409 447 }
+5 -1
src/backends/snappy.rs
··· 15 15 pub progress_args: ProgressArgs, 16 16 } 17 17 18 - #[derive(Default)] 18 + #[derive(Default, Clone)] 19 19 pub struct Snappy { 20 20 pub progress_args: ProgressArgs, 21 21 } ··· 38 38 /// Full name for snappy. 39 39 fn name(&self) -> &str { 40 40 "snappy" 41 + } 42 + 43 + fn clone_boxed(&self) -> Box<dyn Compressor> { 44 + Box::new(self.clone()) 41 45 } 42 46 43 47 /// Compress an input file or pipe to a snappy frame-format archive
+5 -1
src/backends/tar.rs
··· 15 15 pub common_args: CommonArgs, 16 16 } 17 17 18 - #[derive(Default)] 18 + #[derive(Default, Clone)] 19 19 pub struct Tar {} 20 20 21 21 impl Tar { ··· 28 28 /// Full name for tar, also used for extension 29 29 fn name(&self) -> &str { 30 30 "tar" 31 + } 32 + 33 + fn clone_boxed(&self) -> Box<dyn Compressor> { 34 + Box::new(self.clone()) 31 35 } 32 36 33 37 /// Tar extracts to a directory by default
+5
src/backends/xz.rs
··· 22 22 pub level_args: LevelArgs, 23 23 } 24 24 25 + #[derive(Clone)] 25 26 pub struct Xz { 26 27 pub level: i32, 27 28 pub progress_args: ProgressArgs, ··· 55 56 /// Full name for xz. 56 57 fn name(&self) -> &str { 57 58 "xz" 59 + } 60 + 61 + fn clone_boxed(&self) -> Box<dyn Compressor> { 62 + Box::new(self.clone()) 58 63 } 59 64 60 65 fn compress(&self, input: CmprssInput, output: CmprssOutput) -> Result {
+5 -1
src/backends/zip.rs
··· 15 15 pub common_args: CommonArgs, 16 16 } 17 17 18 - #[derive(Default)] 18 + #[derive(Default, Clone)] 19 19 pub struct Zip {} 20 20 21 21 impl Zip { ··· 62 62 impl Compressor for Zip { 63 63 fn name(&self) -> &str { 64 64 "zip" 65 + } 66 + 67 + fn clone_boxed(&self) -> Box<dyn Compressor> { 68 + Box::new(self.clone()) 65 69 } 66 70 67 71 /// Zip extracts to a directory by default
+5
src/backends/zstd.rs
··· 43 43 pub progress_args: ProgressArgs, 44 44 } 45 45 46 + #[derive(Clone)] 46 47 pub struct Zstd { 47 48 pub compression_level: i32, 48 49 pub progress_args: ProgressArgs, ··· 76 77 /// Full name for zstd. 77 78 fn name(&self) -> &str { 78 79 "zstd" 80 + } 81 + 82 + fn clone_boxed(&self) -> Box<dyn Compressor> { 83 + Box::new(self.clone()) 79 84 } 80 85 81 86 /// Compress an input file or pipe to a zstd archive
+16
src/utils.rs
··· 178 178 /// Name of this Compressor 179 179 fn name(&self) -> &str; 180 180 181 + /// Produce an owned copy of this compressor, preserving all configuration 182 + /// (compression level, progress args, pipeline chain, etc). `Pipeline` 183 + /// uses this to hand owned instances to worker threads without losing 184 + /// user-supplied settings. 185 + fn clone_boxed(&self) -> Box<dyn Compressor>; 186 + 181 187 /// Default extension for this Compressor 182 188 fn extension(&self) -> &str { 183 189 self.name() ··· 317 323 use std::path::Path; 318 324 319 325 /// A simple implementation of the Compressor trait for testing 326 + #[derive(Clone)] 320 327 struct TestCompressor; 321 328 322 329 impl Compressor for TestCompressor { 323 330 fn name(&self) -> &str { 324 331 "test" 332 + } 333 + 334 + fn clone_boxed(&self) -> Box<dyn Compressor> { 335 + Box::new(self.clone()) 325 336 } 326 337 327 338 // We'll use the default implementation for extension() and other methods ··· 336 347 } 337 348 338 349 /// A compressor that overrides the default extension 350 + #[derive(Clone)] 339 351 struct CustomExtensionCompressor; 340 352 341 353 impl Compressor for CustomExtensionCompressor { ··· 345 357 346 358 fn extension(&self) -> &str { 347 359 "cst" 360 + } 361 + 362 + fn clone_boxed(&self) -> Box<dyn Compressor> { 363 + Box::new(self.clone()) 348 364 } 349 365 350 366 fn compress(&self, _: CmprssInput, _: CmprssOutput) -> Result {