this repo has no description
0
fork

Configure Feed

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

fix: replace panics with proper Writer handling in all backends

+392 -393
+9 -14
README.md
··· 75 75 76 76 ### Multi-level Compression 77 77 78 - `cmprss` supports multi-level archives like `.tar.gz` directly: 78 + `cmprss` supports multi-level archives like `.tar.gz`, `.tar.xz`, or `.zstd.bz2` directly: 79 79 80 80 ```bash 81 - # Compress a directory to a tar.gz file in one step 82 - cmprss uncompressed_dir out.tar.gz 81 + # Compress a directory to a tar.gz file 82 + cmprss directory out.tar.gz 83 83 84 - # Extract a tar.gz file to the current directory in one step 85 - cmprss out.tar.gz 86 - ``` 84 + # Extract a tar.xz file to a directory 85 + cmprss --extract archive.tar.xz output_dir 87 86 88 - Any combination of supported formats can be used together: 87 + # Gzip an existing tar archive 88 + cmprss archive.tar archive.tar.gz 89 89 90 - ```bash 91 - # Create a zip.tar.xz archive (zip -> tar -> xz) 92 - cmprss directory archive.zip.tar.xz 93 - 94 - # Extract a zip.tar.xz archive (xz -> tar -> zip) 95 - cmprss archive.zip.tar.xz output_dir 90 + # Extract just the xz layer 91 + cmprss archive.tar.xz archive.tar 96 92 ``` 97 93 98 94 Pipes can still be used if preferred: 99 95 100 96 ```bash 101 - # A full roundtrip in one line using pipes 102 97 cmprss tar dir | cmprss gz | cmprss gz -e | cmprss tar -e 103 98 ``` 104 99
+39 -33
src/backends/bzip2.rs
··· 1 1 use crate::{ 2 2 progress::{copy_with_progress, ProgressArgs}, 3 3 utils::{ 4 - cmprss_error, CmprssInput, CmprssOutput, CommonArgs, CompressionLevelValidator, Compressor, 4 + CmprssInput, CmprssOutput, CommonArgs, CompressionLevelValidator, Compressor, 5 5 ExtractedTarget, LevelArgs, 6 6 }, 7 7 }; ··· 110 110 CmprssInput::Pipe(pipe) => Box::new(pipe) as Box<dyn Read + Send>, 111 111 CmprssInput::Reader(reader) => reader.0, 112 112 }; 113 - let output_stream: Box<dyn Write + Send> = match &output { 114 - CmprssOutput::Path(path) => Box::new(BufWriter::new(File::create(path)?)), 115 - CmprssOutput::Pipe(pipe) => Box::new(pipe), 116 - CmprssOutput::Writer(writer) => panic!("Writer output not supported in this context"), 117 - }; 118 - let mut encoder = BzEncoder::new(output_stream, Compression::new(self.level as u32)); 119 - 120 - // Use the custom output function to handle progress bar updates 121 - copy_with_progress( 122 - &mut input_stream, 123 - &mut encoder, 124 - self.progress_args.chunk_size.size_in_bytes, 125 - file_size, 126 - self.progress_args.progress, 127 - &output, 128 - )?; 113 + if let CmprssOutput::Writer(writer) = output { 114 + let mut encoder = BzEncoder::new(writer, Compression::new(self.level as u32)); 115 + io::copy(&mut input_stream, &mut encoder)?; 116 + } else { 117 + let output_stream: Box<dyn Write + Send> = match &output { 118 + CmprssOutput::Path(path) => Box::new(BufWriter::new(File::create(path)?)), 119 + CmprssOutput::Pipe(pipe) => Box::new(pipe), 120 + CmprssOutput::Writer(_) => unreachable!(), 121 + }; 122 + let mut encoder = BzEncoder::new(output_stream, Compression::new(self.level as u32)); 123 + copy_with_progress( 124 + &mut input_stream, 125 + &mut encoder, 126 + self.progress_args.chunk_size.size_in_bytes, 127 + file_size, 128 + self.progress_args.progress, 129 + &output, 130 + )?; 131 + } 129 132 130 133 Ok(()) 131 134 } ··· 148 151 CmprssInput::Pipe(pipe) => Box::new(pipe) as Box<dyn Read + Send>, 149 152 CmprssInput::Reader(reader) => reader.0, 150 153 }; 151 - let output_stream: Box<dyn Write + Send> = match &output { 152 - CmprssOutput::Path(path) => Box::new(BufWriter::new(File::create(path)?)), 153 - CmprssOutput::Pipe(pipe) => Box::new(pipe), 154 - CmprssOutput::Writer(_) => panic!("Writer output not supported in this context"), 155 - }; 156 - let mut decoder = BzDecoder::new(output_stream); 157 - 158 - // Use the custom output function to handle progress bar updates 159 - copy_with_progress( 160 - &mut input_stream, 161 - &mut decoder, 162 - self.progress_args.chunk_size.size_in_bytes, 163 - file_size, 164 - self.progress_args.progress, 165 - &output, 166 - )?; 154 + if let CmprssOutput::Writer(writer) = output { 155 + let mut decoder = BzDecoder::new(writer); 156 + io::copy(&mut input_stream, &mut decoder)?; 157 + } else { 158 + let output_stream: Box<dyn Write + Send> = match &output { 159 + CmprssOutput::Path(path) => Box::new(BufWriter::new(File::create(path)?)), 160 + CmprssOutput::Pipe(pipe) => Box::new(pipe), 161 + CmprssOutput::Writer(_) => unreachable!(), 162 + }; 163 + let mut decoder = BzDecoder::new(output_stream); 164 + copy_with_progress( 165 + &mut input_stream, 166 + &mut decoder, 167 + self.progress_args.chunk_size.size_in_bytes, 168 + file_size, 169 + self.progress_args.progress, 170 + &output, 171 + )?; 172 + } 167 173 168 174 Ok(()) 169 175 }
+42 -38
src/backends/gzip.rs
··· 97 97 CmprssInput::Reader(reader) => reader.0, 98 98 }; 99 99 100 - let output_stream: Box<dyn Write + Send> = match &output { 101 - CmprssOutput::Path(path) => Box::new(BufWriter::new(File::create(path)?)), 102 - CmprssOutput::Pipe(stdout) => Box::new(BufWriter::new(stdout)), 103 - CmprssOutput::Writer(_) => panic!("Writer output not supported in this context"), 104 - }; 105 - 106 - // Create a gzip encoder with the specified compression level 107 - let mut encoder = GzEncoder::new( 108 - output_stream, 109 - Compression::new(self.compression_level as u32), 110 - ); 111 - 112 - // Use the custom output function to handle progress bar updates with CountingWriter 113 - copy_with_progress( 114 - &mut input_stream, 115 - &mut encoder, 116 - self.progress_args.chunk_size.size_in_bytes, 117 - file_size, 118 - self.progress_args.progress, 119 - &output, 120 - )?; 121 - 122 - encoder.finish()?; 100 + if let CmprssOutput::Writer(writer) = output { 101 + let mut encoder = 102 + GzEncoder::new(writer, Compression::new(self.compression_level as u32)); 103 + io::copy(&mut input_stream, &mut encoder)?; 104 + encoder.finish()?; 105 + } else { 106 + let output_stream: Box<dyn Write + Send> = match &output { 107 + CmprssOutput::Path(path) => Box::new(BufWriter::new(File::create(path)?)), 108 + CmprssOutput::Pipe(stdout) => Box::new(BufWriter::new(stdout)), 109 + CmprssOutput::Writer(_) => unreachable!(), 110 + }; 111 + let mut encoder = GzEncoder::new( 112 + output_stream, 113 + Compression::new(self.compression_level as u32), 114 + ); 115 + copy_with_progress( 116 + &mut input_stream, 117 + &mut encoder, 118 + self.progress_args.chunk_size.size_in_bytes, 119 + file_size, 120 + self.progress_args.progress, 121 + &output, 122 + )?; 123 + encoder.finish()?; 124 + } 123 125 Ok(()) 124 126 } 125 127 ··· 142 144 CmprssInput::Reader(reader) => reader.0, 143 145 }; 144 146 145 - let mut output_stream: Box<dyn Write + Send> = match &output { 146 - CmprssOutput::Path(path) => Box::new(BufWriter::new(File::create(path)?)), 147 - CmprssOutput::Pipe(stdout) => Box::new(BufWriter::new(stdout)), 148 - CmprssOutput::Writer(_) => panic!("Writer output not supported in this context"), 149 - }; 150 - 151 147 let mut decoder = GzDecoder::new(input_stream); 152 148 153 - // Use the utility function to handle progress bar updates 154 - copy_with_progress( 155 - &mut decoder, 156 - &mut output_stream, 157 - self.progress_args.chunk_size.size_in_bytes, 158 - file_size, 159 - self.progress_args.progress, 160 - &output, 161 - )?; 149 + if let CmprssOutput::Writer(mut writer) = output { 150 + io::copy(&mut decoder, &mut writer)?; 151 + } else { 152 + let mut output_stream: Box<dyn Write + Send> = match &output { 153 + CmprssOutput::Path(path) => Box::new(BufWriter::new(File::create(path)?)), 154 + CmprssOutput::Pipe(stdout) => Box::new(BufWriter::new(stdout)), 155 + CmprssOutput::Writer(_) => unreachable!(), 156 + }; 157 + copy_with_progress( 158 + &mut decoder, 159 + &mut output_stream, 160 + self.progress_args.chunk_size.size_in_bytes, 161 + file_size, 162 + self.progress_args.progress, 163 + &output, 164 + )?; 165 + } 162 166 163 167 Ok(()) 164 168 }
+38 -36
src/backends/lz4.rs
··· 71 71 CmprssInput::Reader(reader) => reader.0, 72 72 }; 73 73 74 - let output_stream: Box<dyn Write + Send> = match &output { 75 - CmprssOutput::Path(path) => Box::new(BufWriter::new(File::create(path)?)), 76 - CmprssOutput::Pipe(stdout) => Box::new(BufWriter::new(stdout)), 77 - CmprssOutput::Writer(_) => panic!("Writer output not supported in this context"), 78 - }; 79 - 80 - // Create a lz4 encoder 81 - let mut encoder = FrameEncoder::new(output_stream); 82 - 83 - // Copy the input to the encoder with progress reporting 84 - copy_with_progress( 85 - &mut input_stream, 86 - &mut encoder, 87 - self.progress_args.chunk_size.size_in_bytes, 88 - file_size, 89 - self.progress_args.progress, 90 - &output, 91 - )?; 92 - 93 - // Finish the encoder to ensure all data is written 94 - encoder.finish()?; 74 + if let CmprssOutput::Writer(writer) = output { 75 + let mut encoder = FrameEncoder::new(writer); 76 + io::copy(&mut input_stream, &mut encoder)?; 77 + encoder.finish()?; 78 + } else { 79 + let output_stream: Box<dyn Write + Send> = match &output { 80 + CmprssOutput::Path(path) => Box::new(BufWriter::new(File::create(path)?)), 81 + CmprssOutput::Pipe(stdout) => Box::new(BufWriter::new(stdout)), 82 + CmprssOutput::Writer(_) => unreachable!(), 83 + }; 84 + let mut encoder = FrameEncoder::new(output_stream); 85 + copy_with_progress( 86 + &mut input_stream, 87 + &mut encoder, 88 + self.progress_args.chunk_size.size_in_bytes, 89 + file_size, 90 + self.progress_args.progress, 91 + &output, 92 + )?; 93 + encoder.finish()?; 94 + } 95 95 96 96 Ok(()) 97 97 } ··· 124 124 // Create a lz4 decoder 125 125 let mut decoder = FrameDecoder::new(input_stream); 126 126 127 - let mut output_stream: Box<dyn Write + Send> = match &output { 128 - CmprssOutput::Path(path) => Box::new(BufWriter::new(File::create(path)?)), 129 - CmprssOutput::Pipe(stdout) => Box::new(BufWriter::new(stdout)), 130 - CmprssOutput::Writer(_) => panic!("Writer output not supported in this context"), 131 - }; 132 - 133 - // Copy the decoded data to the output with progress reporting 134 - copy_with_progress( 135 - &mut decoder, 136 - &mut output_stream, 137 - self.progress_args.chunk_size.size_in_bytes, 138 - file_size, 139 - self.progress_args.progress, 140 - &output, 141 - )?; 127 + if let CmprssOutput::Writer(mut writer) = output { 128 + io::copy(&mut decoder, &mut writer)?; 129 + } else { 130 + let mut output_stream: Box<dyn Write + Send> = match &output { 131 + CmprssOutput::Path(path) => Box::new(BufWriter::new(File::create(path)?)), 132 + CmprssOutput::Pipe(stdout) => Box::new(BufWriter::new(stdout)), 133 + CmprssOutput::Writer(_) => unreachable!(), 134 + }; 135 + copy_with_progress( 136 + &mut decoder, 137 + &mut output_stream, 138 + self.progress_args.chunk_size.size_in_bytes, 139 + file_size, 140 + self.progress_args.progress, 141 + &output, 142 + )?; 143 + } 142 144 143 145 Ok(()) 144 146 }
+57 -50
src/backends/multi_level.rs
··· 19 19 20 20 /// Create a new MultiLevelCompressor from compressor type names 21 21 pub fn from_names(compressor_names: &[String]) -> io::Result<Self> { 22 - let mut compressors: Vec<Box<dyn Compressor>> = Vec::new(); 23 - 24 - for name in compressor_names { 25 - let compressor: Box<dyn Compressor> = match name.as_str() { 26 - "tar" => Box::new(crate::backends::Tar::default()), 27 - "gzip" | "gz" => Box::new(crate::backends::Gzip::default()), 28 - "xz" => Box::new(crate::backends::Xz::default()), 29 - "bzip2" | "bz2" => Box::new(crate::backends::Bzip2::default()), 30 - "zip" => Box::new(crate::backends::Zip::default()), 31 - "zstd" | "zst" => Box::new(crate::backends::Zstd::default()), 32 - "lz4" => Box::new(crate::backends::Lz4::default()), 33 - _ => { 34 - return Err(io::Error::new( 35 - io::ErrorKind::InvalidInput, 36 - format!("Unknown compressor type: {}", name), 37 - )) 38 - } 39 - }; 40 - compressors.push(compressor); 41 - } 42 - 22 + let compressors = compressor_names 23 + .iter() 24 + .map(|name| Self::create_compressor(name)) 25 + .collect::<io::Result<Vec<_>>>()?; 43 26 Ok(Self { compressors }) 44 27 } 45 28 46 29 /// Get a string representation of the chained format (e.g., "tar.gz") 47 30 fn format_chain(&self) -> String { 48 - // Create a format string like "tar.gz" from the chain of compressors 49 31 self.compressors 50 32 .iter() 51 33 .map(|c| c.extension()) 52 - .rev() // Reverse to get innermost first 53 34 .collect::<Vec<&str>>() 54 35 .join(".") 55 36 } ··· 65 46 "zstd" | "zst" => Ok(Box::new(crate::backends::Zstd::default())), 66 47 "lz4" => Ok(Box::new(crate::backends::Lz4::default())), 67 48 _ => Err(io::Error::new( 68 - io::ErrorKind::Other, 49 + io::ErrorKind::InvalidInput, 69 50 format!("Unknown compressor type: {}", name), 70 51 )), 71 52 } ··· 178 159 179 160 impl Compressor for MultiLevelCompressor { 180 161 fn name(&self) -> &str { 181 - // Return the name of the first (outermost) compressor 182 - if let Some(comp) = self.compressors.first() { 162 + if let Some(comp) = self.compressors.last() { 183 163 comp.name() 184 164 } else { 185 165 "multi" ··· 187 167 } 188 168 189 169 fn extension(&self) -> &str { 190 - // This is a bit of a hack since we can't return an owned String from this method 191 - // We'll just return the extension of the outermost compressor 192 - if let Some(comp) = self.compressors.first() { 170 + if let Some(comp) = self.compressors.last() { 193 171 comp.extension() 194 172 } else { 195 173 "multi" ··· 197 175 } 198 176 199 177 fn default_extracted_target(&self) -> ExtractedTarget { 200 - // The extracted target depends on the innermost compressor 201 - if let Some(comp) = self.compressors.last() { 202 - // The innermost compressor's target is what matters 178 + // After full extraction, the result is what the innermost compressor produces 179 + if let Some(comp) = self.compressors.first() { 203 180 comp.default_extracted_target() 204 181 } else { 205 - // If there are no compressors (shouldn't happen), default to FILE 206 182 ExtractedTarget::FILE 207 183 } 208 184 } 209 185 186 + fn default_compressed_filename(&self, in_path: &Path) -> String { 187 + // Add all extensions: input.txt → input.txt.tar.gz 188 + let base = in_path 189 + .file_name() 190 + .unwrap_or_else(|| std::ffi::OsStr::new("archive")) 191 + .to_str() 192 + .unwrap(); 193 + format!("{}.{}", base, self.format_chain()) 194 + } 195 + 196 + fn default_extracted_filename(&self, in_path: &Path) -> String { 197 + if self.default_extracted_target() == ExtractedTarget::DIRECTORY { 198 + return ".".to_string(); 199 + } 200 + // Strip all known extensions: input.tar.gz → input 201 + let mut name = in_path 202 + .file_name() 203 + .unwrap_or_else(|| std::ffi::OsStr::new("archive")) 204 + .to_str() 205 + .unwrap() 206 + .to_string(); 207 + for comp in self.compressors.iter().rev() { 208 + let ext = format!(".{}", comp.extension()); 209 + if let Some(stripped) = name.strip_suffix(&ext) { 210 + name = stripped.to_string(); 211 + } 212 + } 213 + name 214 + } 215 + 210 216 fn is_archive(&self, in_path: &Path) -> bool { 211 - // Check if the path matches our format chain 212 - if let Some(filename) = in_path.to_str() { 213 - let format_chain = self.format_chain(); 214 - filename.ends_with(&format_chain) 215 - } else { 216 - false 217 - } 217 + let file_name = match in_path.file_name().and_then(|f| f.to_str()) { 218 + Some(f) => f, 219 + None => return false, 220 + }; 221 + file_name.ends_with(&format!(".{}", self.format_chain())) 218 222 } 219 223 220 224 fn compress(&self, input: CmprssInput, output: CmprssOutput) -> Result<(), io::Error> { ··· 232 236 let mut op_compressors: Vec<Box<dyn Compressor>> = self 233 237 .compressors 234 238 .iter() 235 - .map(|c| Self::create_compressor(c.name()).unwrap()) // TODO: Handle error properly 236 - .collect(); 239 + .map(|c| Self::create_compressor(c.name())) 240 + .collect::<io::Result<Vec<_>>>()?; 237 241 238 242 let mut handles = Vec::new(); 239 243 let mut current_thread_input = input; // Consumed by the first (innermost) compressor 240 244 let buffer_size = 64 * 1024; 241 245 242 246 // Process all but the last (outermost) compressor in separate threads 243 - for i in 0..op_compressors.len() - 1 { 247 + for _ in 0..op_compressors.len() - 1 { 244 248 let compressor_for_this_stage = op_compressors.remove(0); 245 249 let (sender, receiver) = channel::<Vec<u8>>(); 246 250 let pipe_writer = PipeWriter::new(sender, buffer_size); ··· 265 269 last_compressor.compress(current_thread_input, output)?; 266 270 267 271 for handle in handles { 268 - handle.join().unwrap()?; // TODO: Handle thread errors properly 272 + handle.join().map_err(|_| { 273 + io::Error::new(io::ErrorKind::Other, "Compression thread panicked") 274 + })??; 269 275 } 270 276 Ok(()) 271 277 } ··· 285 291 let mut op_extractors: Vec<Box<dyn Compressor>> = self 286 292 .compressors 287 293 .iter() 288 - .rev() // Iterate from Outermost to Innermost 289 - .map(|c| Self::create_compressor(c.name()).unwrap()) // TODO: Handle error properly 290 - .collect(); 294 + .rev() 295 + .map(|c| Self::create_compressor(c.name())) 296 + .collect::<io::Result<Vec<_>>>()?; 291 297 292 298 let mut handles = Vec::new(); 293 299 let mut current_thread_input = input; // Consumed by the first (outermost) extractor 294 300 let buffer_size = 64 * 1024; 295 301 296 302 // Process all but the last (innermost) extractor in separate threads. 297 - for i in 0..op_extractors.len() - 1 { 303 + for _ in 0..op_extractors.len() - 1 { 298 304 let extractor_for_this_stage = op_extractors.remove(0); 299 305 let (sender, receiver) = channel::<Vec<u8>>(); 300 306 let pipe_writer = PipeWriter::new(sender, buffer_size); ··· 336 342 last_extractor.extract(current_thread_input, final_output)?; 337 343 338 344 for handle in handles { 339 - handle.join().unwrap()?; // TODO: Handle thread errors properly 345 + handle.join().map_err(|_| { 346 + io::Error::new(io::ErrorKind::Other, "Extraction thread panicked") 347 + })??; 340 348 } 341 349 Ok(()) 342 350 } ··· 346 354 mod tests { 347 355 use super::*; 348 356 use std::fs; 349 - use std::io::{Read, Write}; 350 357 use tempfile::tempdir; 351 358 352 359 #[test]
+26 -3
src/backends/tar.rs
··· 52 52 io::copy(&mut temp_file, &mut pipe)?; 53 53 Ok(()) 54 54 } 55 - CmprssOutput::Writer(_) => panic!("Writer output not supported in tar compress"), 55 + CmprssOutput::Writer(mut writer) => { 56 + let mut temp_file = tempfile()?; 57 + self.compress_internal(input, Builder::new(&mut temp_file))?; 58 + temp_file.seek(SeekFrom::Start(0))?; 59 + io::copy(&mut temp_file, &mut writer)?; 60 + Ok(()) 61 + } 56 62 } 57 63 } 58 64 ··· 97 103 } 98 104 } 99 105 CmprssOutput::Pipe(_) => cmprss_error("tar extraction to stdout is not supported"), 100 - CmprssOutput::Writer(_) => panic!("Writer output not supported in tar extract"), 106 + CmprssOutput::Writer(mut writer) => match input { 107 + CmprssInput::Path(paths) => { 108 + if paths.len() != 1 { 109 + return cmprss_error("tar extraction expects a single archive file"); 110 + } 111 + let mut file = File::open(&paths[0])?; 112 + io::copy(&mut file, &mut writer)?; 113 + Ok(()) 114 + } 115 + CmprssInput::Pipe(mut pipe) => { 116 + io::copy(&mut pipe, &mut writer)?; 117 + Ok(()) 118 + } 119 + CmprssInput::Reader(mut reader) => { 120 + io::copy(&mut reader, &mut writer)?; 121 + Ok(()) 122 + } 123 + }, 101 124 } 102 125 } 103 126 } ··· 131 154 temp_file.seek(SeekFrom::Start(0))?; 132 155 archive.append_file("archive", &mut temp_file)?; 133 156 } 134 - CmprssInput::Reader(reader) => { 157 + CmprssInput::Reader(_) => { 135 158 return cmprss_error("Cannot tar a reader input directly"); 136 159 } 137 160 }
+37 -32
src/backends/xz.rs
··· 77 77 CmprssInput::Pipe(pipe) => Box::new(pipe) as Box<dyn Read + Send>, 78 78 CmprssInput::Reader(reader) => reader.0, 79 79 }; 80 - let output_stream: Box<dyn Write + Send> = match &output { 81 - CmprssOutput::Path(path) => Box::new(BufWriter::new(File::create(path)?)), 82 - CmprssOutput::Pipe(pipe) => Box::new(pipe) as Box<dyn Write + Send>, 83 - CmprssOutput::Writer(_) => panic!("Writer output not supported in this context"), 84 - }; 85 - let mut encoder = XzEncoder::new(output_stream, self.level as u32); 86 - 87 - // Use the custom output function to handle progress bar updates 88 - copy_with_progress( 89 - &mut input_stream, 90 - &mut encoder, 91 - self.progress_args.chunk_size.size_in_bytes, 92 - file_size, 93 - self.progress_args.progress, 94 - &output, 95 - )?; 80 + if let CmprssOutput::Writer(writer) = output { 81 + let mut encoder = XzEncoder::new(writer, self.level as u32); 82 + io::copy(&mut input_stream, &mut encoder)?; 83 + encoder.finish()?; 84 + } else { 85 + let output_stream: Box<dyn Write + Send> = match &output { 86 + CmprssOutput::Path(path) => Box::new(BufWriter::new(File::create(path)?)), 87 + CmprssOutput::Pipe(pipe) => Box::new(pipe) as Box<dyn Write + Send>, 88 + CmprssOutput::Writer(_) => unreachable!(), 89 + }; 90 + let mut encoder = XzEncoder::new(output_stream, self.level as u32); 91 + copy_with_progress( 92 + &mut input_stream, 93 + &mut encoder, 94 + self.progress_args.chunk_size.size_in_bytes, 95 + file_size, 96 + self.progress_args.progress, 97 + &output, 98 + )?; 99 + } 96 100 97 101 Ok(()) 98 102 } ··· 114 118 CmprssInput::Pipe(pipe) => Box::new(pipe) as Box<dyn Read + Send>, 115 119 CmprssInput::Reader(reader) => reader.0, 116 120 }; 117 - let mut output_stream: Box<dyn Write + Send> = match &output { 118 - CmprssOutput::Path(path) => Box::new(BufWriter::new(File::create(path)?)), 119 - CmprssOutput::Pipe(pipe) => Box::new(pipe) as Box<dyn Write + Send>, 120 - CmprssOutput::Writer(_) => panic!("Writer output not supported in this context"), 121 - }; 122 - 123 - // Create an XZ decoder to decompress the input 124 121 let mut decoder = XzDecoder::new(input_stream); 125 122 126 - // Use the custom output function to handle progress bar updates 127 - copy_with_progress( 128 - &mut decoder, 129 - &mut *output_stream, 130 - self.progress_args.chunk_size.size_in_bytes, 131 - file_size, 132 - self.progress_args.progress, 133 - &output, 134 - )?; 123 + if let CmprssOutput::Writer(mut writer) = output { 124 + io::copy(&mut decoder, &mut writer)?; 125 + } else { 126 + let mut output_stream: Box<dyn Write + Send> = match &output { 127 + CmprssOutput::Path(path) => Box::new(BufWriter::new(File::create(path)?)), 128 + CmprssOutput::Pipe(pipe) => Box::new(pipe) as Box<dyn Write + Send>, 129 + CmprssOutput::Writer(_) => unreachable!(), 130 + }; 131 + copy_with_progress( 132 + &mut decoder, 133 + &mut *output_stream, 134 + self.progress_args.chunk_size.size_in_bytes, 135 + file_size, 136 + self.progress_args.progress, 137 + &output, 138 + )?; 139 + } 135 140 136 141 Ok(()) 137 142 }
+24 -3
src/backends/zip.rs
··· 90 90 io::copy(&mut temp_file, &mut pipe)?; 91 91 Ok(()) 92 92 } 93 - CmprssOutput::Writer(_) => { 94 - panic!("Writer output not supported in zip compress"); 93 + CmprssOutput::Writer(mut writer) => { 94 + let mut temp_file = tempfile()?; 95 + self.compress_to_file(input, &mut temp_file)?; 96 + temp_file.seek(SeekFrom::Start(0))?; 97 + io::copy(&mut temp_file, &mut writer)?; 98 + Ok(()) 95 99 } 96 100 } 97 101 } ··· 141 145 } 142 146 } 143 147 CmprssOutput::Pipe(_) => cmprss_error("zip extraction to stdout is not supported"), 144 - CmprssOutput::Writer(_) => panic!("Writer output not supported in zip extract"), 148 + CmprssOutput::Writer(mut writer) => match input { 149 + CmprssInput::Path(paths) => { 150 + if paths.len() != 1 { 151 + return cmprss_error("zip extraction expects a single archive file"); 152 + } 153 + let mut file = File::open(&paths[0])?; 154 + io::copy(&mut file, &mut writer)?; 155 + Ok(()) 156 + } 157 + CmprssInput::Pipe(mut pipe) => { 158 + io::copy(&mut pipe, &mut writer)?; 159 + Ok(()) 160 + } 161 + CmprssInput::Reader(mut reader) => { 162 + io::copy(&mut reader, &mut writer)?; 163 + Ok(()) 164 + } 165 + }, 145 166 } 146 167 } 147 168 }
+38 -37
src/backends/zstd.rs
··· 119 119 CmprssInput::Reader(reader) => reader.0, 120 120 }; 121 121 122 - let output_stream: Box<dyn Write + Send> = match &output { 123 - CmprssOutput::Path(path) => Box::new(BufWriter::new(File::create(path)?)), 124 - CmprssOutput::Pipe(stdout) => Box::new(BufWriter::new(stdout)), 125 - CmprssOutput::Writer(_) => panic!("Writer output not supported in this context"), 126 - }; 127 - 128 - // Create a zstd encoder with the specified compression level 129 - let mut encoder = Encoder::new(output_stream, self.compression_level)?; 130 - 131 - // Copy the input to the encoder with progress reporting 132 - copy_with_progress( 133 - &mut input_stream, 134 - &mut encoder, 135 - self.progress_args.chunk_size.size_in_bytes, 136 - file_size, 137 - self.progress_args.progress, 138 - &output, 139 - )?; 140 - 141 - // Finish the encoder to ensure all data is written 142 - encoder.finish()?; 122 + if let CmprssOutput::Writer(writer) = output { 123 + let mut encoder = Encoder::new(writer, self.compression_level)?; 124 + io::copy(&mut input_stream, &mut encoder)?; 125 + encoder.finish()?; 126 + } else { 127 + let output_stream: Box<dyn Write + Send> = match &output { 128 + CmprssOutput::Path(path) => Box::new(BufWriter::new(File::create(path)?)), 129 + CmprssOutput::Pipe(stdout) => Box::new(BufWriter::new(stdout)), 130 + CmprssOutput::Writer(_) => unreachable!(), 131 + }; 132 + let mut encoder = Encoder::new(output_stream, self.compression_level)?; 133 + copy_with_progress( 134 + &mut input_stream, 135 + &mut encoder, 136 + self.progress_args.chunk_size.size_in_bytes, 137 + file_size, 138 + self.progress_args.progress, 139 + &output, 140 + )?; 141 + encoder.finish()?; 142 + } 143 143 144 144 Ok(()) 145 145 } ··· 169 169 CmprssInput::Reader(reader) => reader.0, 170 170 }; 171 171 172 - let mut output_stream: Box<dyn Write + Send> = match &output { 173 - CmprssOutput::Path(path) => Box::new(BufWriter::new(File::create(path)?)), 174 - CmprssOutput::Pipe(stdout) => Box::new(BufWriter::new(stdout)), 175 - CmprssOutput::Writer(_) => panic!("Writer output not supported in this context"), 176 - }; 177 - 178 - // Create a zstd decoder 179 172 let mut decoder = Decoder::new(input_stream)?; 180 173 181 - // Copy the decoded data to the output with progress reporting 182 - copy_with_progress( 183 - &mut decoder, 184 - &mut output_stream, 185 - self.progress_args.chunk_size.size_in_bytes, 186 - file_size, 187 - self.progress_args.progress, 188 - &output, 189 - )?; 174 + if let CmprssOutput::Writer(mut writer) = output { 175 + io::copy(&mut decoder, &mut writer)?; 176 + } else { 177 + let mut output_stream: Box<dyn Write + Send> = match &output { 178 + CmprssOutput::Path(path) => Box::new(BufWriter::new(File::create(path)?)), 179 + CmprssOutput::Pipe(stdout) => Box::new(BufWriter::new(stdout)), 180 + CmprssOutput::Writer(_) => unreachable!(), 181 + }; 182 + copy_with_progress( 183 + &mut decoder, 184 + &mut output_stream, 185 + self.progress_args.chunk_size.size_in_bytes, 186 + file_size, 187 + self.progress_args.progress, 188 + &output, 189 + )?; 190 + } 190 191 191 192 Ok(()) 192 193 }
+46 -145
src/main.rs
··· 84 84 85 85 /// Get a compressor from a filename 86 86 fn get_compressor_from_filename(filename: &Path) -> Option<Box<dyn Compressor>> { 87 + // Use just the filename component to avoid dots in directory names 88 + let file_name = filename.file_name()?.to_str()?; 89 + 87 90 // Prioritize checking for multi-level formats first 88 - if let Some(filename_str) = filename.to_str() { 89 - let parts: Vec<&str> = filename_str.split('.').collect(); 91 + { 92 + let parts: Vec<&str> = file_name.split('.').collect(); 90 93 // A potential multi-level format like "archive.tar.gz" will have at least 3 parts 91 94 if parts.len() >= 3 { 92 95 // Get all available single compressors for matching extensions ··· 140 143 compressor_types.reverse(); // e.g., ["tar", "gzip"] 141 144 return Some(create_multi_level_compressor(&compressor_types)); 142 145 } 143 - // If compressor_types is empty here, it means the multi-level parse failed (e.g. "file.foo.bar" with unknown foo/bar) 144 - // or an unknown extension was found in the chain. We'll fall through to single extension check. 145 146 } 146 147 } 147 148 148 - // Fallback: If not a recognized multi-level format, or if fewer than 3 parts (e.g. "file.gz"), 149 - // try matching a single known compressor extension. 150 - let single_compressors: Vec<Box<dyn Compressor>> = vec![ 151 - Box::<Tar>::default(), 149 + // Fallback: try matching a single known compressor extension 150 + for sc in [ 151 + Box::<Tar>::default() as Box<dyn Compressor>, 152 152 Box::<Gzip>::default(), 153 153 Box::<Xz>::default(), 154 154 Box::<Bzip2>::default(), 155 155 Box::<Zip>::default(), 156 156 Box::<Zstd>::default(), 157 157 Box::<Lz4>::default(), 158 - ]; 159 - 160 - // Check if file extension matches any known format 161 - // This is now a fallback. 162 - // Ensure this doesn't misinterpret "foo.tar.gz" as just "gz" if multi-level check failed for some reason 163 - // A more robust check here might be to see if filename *only* ends with .ext and not .something_else.ext 164 - // For now, the standard check is: 165 - if let Some(filename_str) = filename.to_str() { 166 - for sc in single_compressors { 167 - // A simple "ends_with" can be problematic for "file.tar.gz" vs "file.gz" 168 - // We need to be more specific. The extension should be exactly the compressor's extension. 169 - let expected_extension = format!(".{}", sc.extension()); 170 - if filename_str.ends_with(&expected_extension) { 171 - // Further check: ensure it's not something like ".tar.gz" being matched by ".gz" 172 - // if we want to be super sure, but the multi-level check should catch .tar.gz first. 173 - // A simple way: if it ends with ".tar.gz", Gzip (gz) should not match here IF Tar (tar) also exists. 174 - // The current structure relies on multi-level being caught first. 175 - // If multi-level parsing failed, then we check single extensions. 176 - // Example: "archive.gz" -> Gzip 177 - // Example: "archive.tar" -> Tar 178 - // Example: "archive.unknown.gz" -> Multi-level fails, then Gzip matches. 179 - return Some(sc); 180 - } 158 + ] { 159 + let expected_extension = format!(".{}", sc.extension()); 160 + if file_name.ends_with(&expected_extension) { 161 + return Some(sc); 181 162 } 182 163 } 183 164 None 184 165 } 185 166 167 + /// Get a single compressor from an extension string (no multi-level detection) 168 + fn get_compressor_from_extension(ext: &str) -> Option<Box<dyn Compressor>> { 169 + match ext { 170 + "tar" => Some(Box::<Tar>::default()), 171 + "gz" => Some(Box::<Gzip>::default()), 172 + "xz" => Some(Box::<Xz>::default()), 173 + "bz2" => Some(Box::<Bzip2>::default()), 174 + "zip" => Some(Box::<Zip>::default()), 175 + "zst" => Some(Box::<Zstd>::default()), 176 + "lz4" => Some(Box::<Lz4>::default()), 177 + _ => None, 178 + } 179 + } 180 + 186 181 /// Create a MultiLevelCompressor from a list of compressor types 187 182 fn create_multi_level_compressor(compressor_types: &[String]) -> Box<dyn Compressor> { 188 183 // Create a MultiLevelCompressor from the list of compressor types ··· 270 265 (Some(c), None) => (Some(c), Action::Compress), 271 266 (None, Some(e)) => (Some(e), Action::Extract), 272 267 (Some(c), Some(e)) => { 273 - if c.name() == e.name() { 274 - // Same format for input and output, can't decide 275 - if output.is_dir() { 276 - // If output is a directory, we're probably extracting 277 - return (Some(e), Action::Extract); 278 - } 279 - return (Some(c), Action::Unknown); 280 - } 281 - 282 268 // Compare the input and output extensions to see if one has an extra extension 283 269 let input_file = input.file_name().unwrap().to_str().unwrap(); 284 270 let input_ext = input.extension().unwrap_or_default(); ··· 288 274 let guessed_input = output_file.to_string() + "." + input_ext.to_str().unwrap(); 289 275 290 276 if guessed_output == output_file { 291 - (Some(c), Action::Compress) 277 + // Input is "archive.tar", output is "archive.tar.gz" — only add the outer layer 278 + let single_compressor = get_compressor_from_extension(output_ext.to_str().unwrap_or("")); 279 + (single_compressor.or(Some(c)), Action::Compress) 292 280 } else if guessed_input == input_file { 293 - (Some(e), Action::Extract) 281 + // Output is "archive.tar", input is "archive.tar.gz" — only strip the outer layer 282 + let single_compressor = get_compressor_from_extension(input_ext.to_str().unwrap_or("")); 283 + (single_compressor.or(Some(e)), Action::Extract) 284 + } else if c.name() == e.name() { 285 + // Same format for input and output, can't decide 286 + if output.is_dir() { 287 + (Some(e), Action::Extract) 288 + } else { 289 + (Some(c), Action::Unknown) 290 + } 294 291 } else if output.is_dir() { 295 - // If output is a directory, we're probably extracting 296 292 (Some(e), Action::Extract) 297 293 } else { 298 294 (None, Action::Unknown) ··· 512 508 } 513 509 } 514 510 Action::Extract => { 515 - // Look at the input name 516 511 if let CmprssInput::Path(paths) = &cmprss_input { 517 512 if paths.len() != 1 { 518 - // When extracting, we expect a single input file 519 513 return Err(io::Error::new( 520 514 io::ErrorKind::Other, 521 515 "Expected a single archive to extract", 522 516 )); 523 517 } 524 518 compressor = get_compressor_from_filename(paths.first().unwrap()); 525 - 526 - // If we still couldn't guess the compressor, try harder with multi-level extraction 527 - if compressor.is_none() && paths.len() == 1 { 528 - if let Some(filename_str) = paths.first().unwrap().to_str() { 529 - // Try to parse multi-level formats (e.g., tar.gz) 530 - let parts: Vec<&str> = filename_str.split('.').collect(); 531 - if parts.len() >= 3 { 532 - // Get all available compressors 533 - let compressors: Vec<Box<dyn Compressor>> = vec![ 534 - Box::<Tar>::default(), 535 - Box::<Gzip>::default(), 536 - Box::<Xz>::default(), 537 - Box::<Bzip2>::default(), 538 - Box::<Zip>::default(), 539 - Box::<Zstd>::default(), 540 - Box::<Lz4>::default(), 541 - ]; 542 - 543 - // Get extensions in reverse order (from right to left) 544 - let mut extensions: Vec<String> = Vec::new(); 545 - for i in 1..parts.len() { 546 - extensions.push(parts[parts.len() - i].to_string()); 547 - } 548 - 549 - // Try to find a compressor for each extension 550 - let mut compressor_types: Vec<String> = Vec::new(); 551 - for ext in &extensions { 552 - for compressor in &compressors { 553 - if compressor.extension() == ext || compressor.name() == ext 554 - { 555 - compressor_types.push(compressor.name().to_string()); 556 - break; 557 - } 558 - } 559 - } 560 - 561 - // If we found compressor types, create a MultiLevelCompressor 562 - if !compressor_types.is_empty() { 563 - compressor = 564 - Some(create_multi_level_compressor(&compressor_types)); 565 - } 566 - } 567 - } 568 - } 569 519 } 570 520 } 571 521 Action::Unknown => match (&cmprss_input, &cmprss_output) { 572 522 (CmprssInput::Path(paths), CmprssOutput::Path(path)) => { 573 - // Special case: if output is a directory, assume we're extracting 574 523 if path.is_dir() && paths.len() == 1 { 575 - // For extraction to directory, try to determine compressor from input file 576 524 compressor = get_compressor_from_filename(paths.first().unwrap()); 577 525 action = Action::Extract; 578 526 579 - // If no compressor was found, try harder with multi-level detection 580 527 if compressor.is_none() { 581 - if let Some(filename_str) = paths.first().unwrap().to_str() { 582 - // Try to parse multi-level formats (e.g., tar.gz) 583 - let parts: Vec<&str> = filename_str.split('.').collect(); 584 - if parts.len() >= 3 { 585 - // Get all available compressors 586 - let compressors: Vec<Box<dyn Compressor>> = vec![ 587 - Box::<Tar>::default(), 588 - Box::<Gzip>::default(), 589 - Box::<Xz>::default(), 590 - Box::<Bzip2>::default(), 591 - Box::<Zip>::default(), 592 - Box::<Zstd>::default(), 593 - Box::<Lz4>::default(), 594 - ]; 595 - 596 - // Get extensions in reverse order (from right to left) 597 - let mut extensions: Vec<String> = Vec::new(); 598 - for i in 1..parts.len() { 599 - extensions.push(parts[parts.len() - i].to_string()); 600 - } 601 - 602 - // Try to find a compressor for each extension 603 - let mut compressor_types: Vec<String> = Vec::new(); 604 - for ext in &extensions { 605 - for compressor in &compressors { 606 - if compressor.extension() == ext 607 - || compressor.name() == ext 608 - { 609 - compressor_types 610 - .push(compressor.name().to_string()); 611 - break; 612 - } 613 - } 614 - } 615 - 616 - // If we found compressor types, create a MultiLevelCompressor 617 - if !compressor_types.is_empty() { 618 - compressor = 619 - Some(create_multi_level_compressor(&compressor_types)); 620 - } 621 - } 622 - } 623 - 624 - // If we still couldn't determine compressor, fail with a clear message 625 - if compressor.is_none() { 626 - return Err(io::Error::new( 627 - io::ErrorKind::Other, 628 - format!( 629 - "Couldn't determine how to extract {:?}", 630 - paths.first().unwrap() 631 - ), 632 - )); 633 - } 528 + return Err(io::Error::new( 529 + io::ErrorKind::Other, 530 + format!( 531 + "Couldn't determine how to extract {:?}", 532 + paths.first().unwrap() 533 + ), 534 + )); 634 535 } 635 536 } else { 636 537 let (guessed_compressor, guessed_action) =
+36 -2
tests/multi_level.rs
··· 135 135 .assert() 136 136 .success(); 137 137 138 - // Verify the file was extracted correctly 139 - let extracted_file = extract_dir.child("test_file.txt"); 138 + // Verify the file was extracted correctly (tar preserves the directory structure) 139 + let extracted_file = extract_dir.child("source").child("test_file.txt"); 140 140 extracted_file.assert(predicate::path::exists()); 141 141 extracted_file.assert(predicate::str::contains( 142 142 "test content for tar.gz extraction", 143 143 )); 144 + 145 + Ok(()) 146 + } 147 + 148 + /// Full roundtrip: directory -> tar.xz -> directory 149 + #[test] 150 + fn test_tar_xz_roundtrip() -> Result<(), Box<dyn std::error::Error>> { 151 + let temp_dir = TempDir::new()?; 152 + 153 + let source_dir = temp_dir.child("source"); 154 + source_dir.create_dir_all()?; 155 + let test_file = source_dir.child("data.txt"); 156 + test_file.write_str("tar.xz roundtrip content")?; 157 + 158 + let archive = temp_dir.child("archive.tar.xz"); 159 + Command::cargo_bin("cmprss")? 160 + .arg("--compress") 161 + .arg(source_dir.path()) 162 + .arg(archive.path()) 163 + .assert() 164 + .success(); 165 + 166 + let extract_dir = temp_dir.child("extract"); 167 + extract_dir.create_dir_all()?; 168 + Command::cargo_bin("cmprss")? 169 + .arg("--extract") 170 + .arg(archive.path()) 171 + .arg(extract_dir.path()) 172 + .assert() 173 + .success(); 174 + 175 + let extracted_file = extract_dir.child("source").child("data.txt"); 176 + extracted_file.assert(predicate::path::exists()); 177 + extracted_file.assert(predicate::str::contains("tar.xz roundtrip content")); 144 178 145 179 Ok(()) 146 180 }