this repo has no description
0
fork

Configure Feed

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

refactor: migrate error handling from io::Error to anyhow

+142 -201
+1
Cargo.lock
··· 255 255 name = "cmprss" 256 256 version = "0.2.0" 257 257 dependencies = [ 258 + "anyhow", 258 259 "assert_cmd", 259 260 "assert_fs", 260 261 "bzip2",
+1
Cargo.toml
··· 11 11 categories = ["command-line-utilities", "compression"] 12 12 13 13 [dependencies] 14 + anyhow = "1" 14 15 bzip2 = { version = "0.6", features = ["static"] } 15 16 clap = { version = "4", features = ["derive"] } 16 17 flate2 = "1"
+9 -14
src/backends/bzip2.rs
··· 2 2 progress::{ProgressArgs, copy_with_progress}, 3 3 utils::{ 4 4 CmprssInput, CmprssOutput, CommonArgs, CompressionLevelValidator, Compressor, 5 - ExtractedTarget, LevelArgs, 5 + ExtractedTarget, LevelArgs, Result, 6 6 }, 7 7 }; 8 + use anyhow::bail; 8 9 use bzip2::Compression; 9 10 use bzip2::write::{BzDecoder, BzEncoder}; 10 11 use clap::Args; ··· 93 94 } 94 95 95 96 /// Compress an input file or pipe to a bz2 archive 96 - fn compress(&self, input: CmprssInput, output: CmprssOutput) -> Result<(), io::Error> { 97 + fn compress(&self, input: CmprssInput, output: CmprssOutput) -> Result { 97 98 let mut file_size = None; 98 99 let mut input_stream = match input { 99 100 CmprssInput::Path(paths) => { 100 101 if paths.len() > 1 { 101 - return Err(io::Error::new( 102 - io::ErrorKind::InvalidInput, 103 - "Multiple input files not supported for bzip2", 104 - )); 102 + bail!("Multiple input files not supported for bzip2"); 105 103 } 106 104 let path = &paths[0]; 107 105 file_size = Some(std::fs::metadata(path)?.len()); ··· 134 132 } 135 133 136 134 /// Extract a bz2 archive to a file or pipe 137 - fn extract(&self, input: CmprssInput, output: CmprssOutput) -> Result<(), io::Error> { 135 + fn extract(&self, input: CmprssInput, output: CmprssOutput) -> Result { 138 136 let mut file_size = None; 139 137 let mut input_stream = match input { 140 138 CmprssInput::Path(paths) => { 141 139 if paths.len() > 1 { 142 - return Err(io::Error::new( 143 - io::ErrorKind::InvalidInput, 144 - "Multiple input files not supported for bzip2 extraction", 145 - )); 140 + bail!("Multiple input files not supported for bzip2 extraction"); 146 141 } 147 142 let path = &paths[0]; 148 143 file_size = Some(std::fs::metadata(path)?.len()); ··· 203 198 204 199 /// Test the default compression level 205 200 #[test] 206 - fn test_bzip2_default_compression() -> Result<(), io::Error> { 201 + fn test_bzip2_default_compression() -> Result { 207 202 let compressor = Bzip2::default(); 208 203 test_compression(&compressor) 209 204 } 210 205 211 206 /// Test fast compression level 212 207 #[test] 213 - fn test_bzip2_fast_compression() -> Result<(), io::Error> { 208 + fn test_bzip2_fast_compression() -> Result { 214 209 let fast_compressor = Bzip2 { 215 210 level: 1, 216 211 progress_args: ProgressArgs::default(), ··· 220 215 221 216 /// Test best compression level 222 217 #[test] 223 - fn test_bzip2_best_compression() -> Result<(), io::Error> { 218 + fn test_bzip2_best_compression() -> Result { 224 219 let best_compressor = Bzip2 { 225 220 level: 9, 226 221 progress_args: ProgressArgs::default(),
+13 -18
src/backends/gzip.rs
··· 1 1 use crate::progress::{ProgressArgs, copy_with_progress}; 2 2 use crate::utils::*; 3 + use anyhow::bail; 3 4 use clap::Args; 4 5 use flate2::write::GzEncoder; 5 6 use flate2::{Compression, read::GzDecoder}; ··· 65 66 } 66 67 67 68 /// Compress an input file or pipe to a gzip archive 68 - fn compress(&self, input: CmprssInput, output: CmprssOutput) -> Result<(), io::Error> { 69 + fn compress(&self, input: CmprssInput, output: CmprssOutput) -> Result { 69 70 if let CmprssOutput::Path(out_path) = &output { 70 71 if out_path.is_dir() { 71 - return cmprss_error( 72 - "Gzip does not support compressing to a directory. Please specify an output file.", 72 + bail!( 73 + "Gzip does not support compressing to a directory. Please specify an output file." 73 74 ); 74 75 } 75 76 } 76 77 if let CmprssInput::Path(input_paths) = &input { 77 78 for x in input_paths { 78 79 if x.is_dir() { 79 - return cmprss_error( 80 - "Gzip does not support compressing a directory. Please specify only files.", 80 + bail!( 81 + "Gzip does not support compressing a directory. Please specify only files." 81 82 ); 82 83 } 83 84 } ··· 86 87 let mut input_stream: Box<dyn Read + Send> = match input { 87 88 CmprssInput::Path(paths) => { 88 89 if paths.len() > 1 { 89 - return Err(io::Error::new( 90 - io::ErrorKind::InvalidInput, 91 - "Multiple input files not supported for gzip", 92 - )); 90 + bail!("Multiple input files not supported for gzip"); 93 91 } 94 92 let path = &paths[0]; 95 93 file_size = Some(std::fs::metadata(path)?.len()); ··· 128 126 } 129 127 130 128 /// Extract a gzip archive 131 - fn extract(&self, input: CmprssInput, output: CmprssOutput) -> Result<(), io::Error> { 129 + fn extract(&self, input: CmprssInput, output: CmprssOutput) -> Result { 132 130 let mut file_size = None; 133 131 let input_stream: Box<dyn Read + Send> = match input { 134 132 CmprssInput::Path(paths) => { 135 133 if paths.len() > 1 { 136 - return Err(io::Error::new( 137 - io::ErrorKind::InvalidInput, 138 - "Multiple input files not supported for gzip extraction", 139 - )); 134 + bail!("Multiple input files not supported for gzip extraction"); 140 135 } 141 136 let path = &paths[0]; 142 137 file_size = Some(std::fs::metadata(path)?.len()); ··· 187 182 188 183 /// Test the default compression level 189 184 #[test] 190 - fn test_gzip_default_compression() -> Result<(), io::Error> { 185 + fn test_gzip_default_compression() -> Result { 191 186 let compressor = Gzip::default(); 192 187 test_compression(&compressor) 193 188 } 194 189 195 190 /// Test fast compression level 196 191 #[test] 197 - fn test_gzip_fast_compression() -> Result<(), io::Error> { 192 + fn test_gzip_fast_compression() -> Result { 198 193 let fast_compressor = Gzip { 199 194 compression_level: 1, 200 195 progress_args: ProgressArgs::default(), ··· 204 199 205 200 /// Test best compression level 206 201 #[test] 207 - fn test_gzip_best_compression() -> Result<(), io::Error> { 202 + fn test_gzip_best_compression() -> Result { 208 203 let best_compressor = Gzip { 209 204 compression_level: 9, 210 205 progress_args: ProgressArgs::default(), ··· 214 209 215 210 /// Test for gzip-specific behavior: handling of concatenated gzip archives 216 211 #[test] 217 - fn test_concatenated_gzip() -> Result<(), io::Error> { 212 + fn test_concatenated_gzip() -> Result { 218 213 let compressor = Gzip::default(); 219 214 let temp_dir = tempdir().expect("Failed to create temp dir"); 220 215
+13 -18
src/backends/lz4.rs
··· 1 1 use crate::progress::{ProgressArgs, copy_with_progress}; 2 - use crate::utils::{CmprssInput, CmprssOutput, CommonArgs, Compressor, cmprss_error}; 2 + use crate::utils::*; 3 + use anyhow::bail; 3 4 use clap::Args; 4 5 use lz4_flex::frame::{FrameDecoder, FrameEncoder}; 5 6 use std::fs::File; ··· 39 40 } 40 41 41 42 /// Compress an input file or pipe to a lz4 archive 42 - fn compress(&self, input: CmprssInput, output: CmprssOutput) -> Result<(), io::Error> { 43 + fn compress(&self, input: CmprssInput, output: CmprssOutput) -> Result { 43 44 if let CmprssOutput::Path(out_path) = &output { 44 45 if out_path.is_dir() { 45 - return cmprss_error( 46 - "LZ4 does not support compressing to a directory. Please specify an output file.", 46 + bail!( 47 + "LZ4 does not support compressing to a directory. Please specify an output file." 47 48 ); 48 49 } 49 50 } 50 51 if let CmprssInput::Path(input_paths) = &input { 51 52 for x in input_paths { 52 53 if x.is_dir() { 53 - return cmprss_error( 54 - "LZ4 does not support compressing a directory. Please specify only files.", 54 + bail!( 55 + "LZ4 does not support compressing a directory. Please specify only files." 55 56 ); 56 57 } 57 58 } ··· 60 61 let mut input_stream: Box<dyn Read + Send> = match input { 61 62 CmprssInput::Path(paths) => { 62 63 if paths.len() > 1 { 63 - return Err(io::Error::new( 64 - io::ErrorKind::InvalidInput, 65 - "Multiple input files not supported for lz4", 66 - )); 64 + bail!("Multiple input files not supported for lz4"); 67 65 } 68 66 let path = &paths[0]; 69 67 file_size = Some(std::fs::metadata(path)?.len()); ··· 99 97 } 100 98 101 99 /// Extract a lz4 archive to an output file or pipe 102 - fn extract(&self, input: CmprssInput, output: CmprssOutput) -> Result<(), io::Error> { 100 + fn extract(&self, input: CmprssInput, output: CmprssOutput) -> Result { 103 101 if let CmprssOutput::Path(out_path) = &output { 104 102 if out_path.is_dir() { 105 - return cmprss_error( 106 - "LZ4 does not support extracting to a directory. Please specify an output file.", 103 + bail!( 104 + "LZ4 does not support extracting to a directory. Please specify an output file." 107 105 ); 108 106 } 109 107 } ··· 112 110 let input_stream: Box<dyn Read + Send> = match input { 113 111 CmprssInput::Path(paths) => { 114 112 if paths.len() > 1 { 115 - return Err(io::Error::new( 116 - io::ErrorKind::InvalidInput, 117 - "Multiple input files not supported for lz4 extraction", 118 - )); 113 + bail!("Multiple input files not supported for lz4 extraction"); 119 114 } 120 115 let path = &paths[0]; 121 116 file_size = Some(std::fs::metadata(path)?.len()); ··· 164 159 165 160 /// Test the default compression level 166 161 #[test] 167 - fn test_lz4_default_compression() -> Result<(), io::Error> { 162 + fn test_lz4_default_compression() -> Result { 168 163 let compressor = Lz4::default(); 169 164 test_compression(&compressor) 170 165 }
+13 -16
src/backends/pipeline.rs
··· 1 1 use crate::utils::*; 2 + use anyhow::anyhow; 2 3 use std::io::{self, Read, Write}; 3 4 use std::path::Path; 4 5 use std::sync::mpsc::{Receiver, Sender, channel}; ··· 17 18 } 18 19 19 20 /// Create a new Pipeline from compressor type names 20 - pub fn from_names(compressor_names: &[String]) -> io::Result<Self> { 21 + pub fn from_names(compressor_names: &[String]) -> Result<Self> { 21 22 let compressors = compressor_names 22 23 .iter() 23 24 .map(|name| Self::create_compressor(name)) 24 - .collect::<io::Result<Vec<_>>>()?; 25 + .collect::<Result<Vec<_>>>()?; 25 26 Ok(Self { compressors }) 26 27 } 27 28 ··· 34 35 .join(".") 35 36 } 36 37 37 - fn create_compressor(name: &str) -> io::Result<Box<dyn Compressor>> { 38 - crate::backends::compressor_from_str(name).ok_or_else(|| { 39 - io::Error::new( 40 - io::ErrorKind::InvalidInput, 41 - format!("Unknown compressor type: {}", name), 42 - ) 43 - }) 38 + fn create_compressor(name: &str) -> Result<Box<dyn Compressor>> { 39 + crate::backends::compressor_from_str(name) 40 + .ok_or_else(|| anyhow!("Unknown compressor type: {}", name)) 44 41 } 45 42 } 46 43 ··· 208 205 file_name.ends_with(&format!(".{}", self.format_chain())) 209 206 } 210 207 211 - fn compress(&self, input: CmprssInput, output: CmprssOutput) -> Result<(), io::Error> { 208 + fn compress(&self, input: CmprssInput, output: CmprssOutput) -> Result { 212 209 debug_assert!(!self.compressors.is_empty(), "pipeline is never empty"); 213 210 214 211 if self.compressors.len() == 1 { ··· 219 216 .compressors 220 217 .iter() 221 218 .map(|c| Self::create_compressor(c.name())) 222 - .collect::<io::Result<Vec<_>>>()?; 219 + .collect::<Result<Vec<_>>>()?; 223 220 224 221 let mut handles = Vec::new(); 225 222 let mut current_thread_input = input; // Consumed by the first (innermost) compressor ··· 253 250 for handle in handles { 254 251 handle 255 252 .join() 256 - .map_err(|_| io::Error::other("Compression thread panicked"))??; 253 + .map_err(|_| anyhow!("Compression thread panicked"))??; 257 254 } 258 255 Ok(()) 259 256 } 260 257 261 - fn extract(&self, input: CmprssInput, output: CmprssOutput) -> Result<(), io::Error> { 258 + fn extract(&self, input: CmprssInput, output: CmprssOutput) -> Result { 262 259 debug_assert!(!self.compressors.is_empty(), "pipeline is never empty"); 263 260 264 261 if self.compressors.len() == 1 { ··· 270 267 .iter() 271 268 .rev() 272 269 .map(|c| Self::create_compressor(c.name())) 273 - .collect::<io::Result<Vec<_>>>()?; 270 + .collect::<Result<Vec<_>>>()?; 274 271 275 272 let mut handles = Vec::new(); 276 273 let mut current_thread_input = input; // Consumed by the first (outermost) extractor ··· 321 318 for handle in handles { 322 319 handle 323 320 .join() 324 - .map_err(|_| io::Error::other("Extraction thread panicked"))??; 321 + .map_err(|_| anyhow!("Extraction thread panicked"))??; 325 322 } 326 323 Ok(()) 327 324 } ··· 334 331 use tempfile::tempdir; 335 332 336 333 #[test] 337 - fn test_pipeline_compression() -> Result<(), io::Error> { 334 + fn test_pipeline_compression() -> Result { 338 335 let temp_dir = tempdir()?; 339 336 340 337 let test_content = "This is a test file for pipeline compression";
+15 -18
src/backends/tar.rs
··· 1 1 extern crate tar; 2 2 3 + use anyhow::bail; 3 4 use clap::Args; 4 5 use std::fs::File; 5 6 use std::io::{self, Seek, SeekFrom, Write}; ··· 34 35 ExtractedTarget::DIRECTORY 35 36 } 36 37 37 - fn compress(&self, input: CmprssInput, output: CmprssOutput) -> Result<(), io::Error> { 38 + fn compress(&self, input: CmprssInput, output: CmprssOutput) -> Result { 38 39 match output { 39 40 CmprssOutput::Path(path) => { 40 41 let file = File::create(path)?; ··· 62 63 } 63 64 } 64 65 65 - fn extract(&self, input: CmprssInput, output: CmprssOutput) -> Result<(), io::Error> { 66 + fn extract(&self, input: CmprssInput, output: CmprssOutput) -> Result { 66 67 match output { 67 68 CmprssOutput::Path(ref out_dir) => { 68 69 // Create the output directory if it doesn't exist 69 70 if !out_dir.exists() { 70 71 std::fs::create_dir_all(out_dir)?; 71 72 } else if !out_dir.is_dir() { 72 - return cmprss_error("tar extraction output must be a directory"); 73 + bail!("tar extraction output must be a directory"); 73 74 } 74 75 75 76 match input { 76 77 CmprssInput::Path(paths) => { 77 78 if paths.len() != 1 { 78 - return cmprss_error("tar extraction expects a single archive file"); 79 + bail!("tar extraction expects a single archive file"); 79 80 } 80 81 let file = File::open(&paths[0])?; 81 82 let mut archive = Archive::new(file); 82 - archive.unpack(out_dir) 83 + Ok(archive.unpack(out_dir)?) 83 84 } 84 85 CmprssInput::Pipe(mut pipe) => { 85 86 // Create a temporary file to store the tar content ··· 93 94 94 95 // Extract from the temporary file 95 96 let mut archive = Archive::new(temp_file); 96 - archive.unpack(out_dir) 97 + Ok(archive.unpack(out_dir)?) 97 98 } 98 99 CmprssInput::Reader(reader) => { 99 100 let mut archive = Archive::new(reader.0); ··· 102 103 } 103 104 } 104 105 } 105 - CmprssOutput::Pipe(_) => cmprss_error("tar extraction to stdout is not supported"), 106 + CmprssOutput::Pipe(_) => bail!("tar extraction to stdout is not supported"), 106 107 CmprssOutput::Writer(mut writer) => match input { 107 108 CmprssInput::Path(paths) => { 108 109 if paths.len() != 1 { 109 - return cmprss_error("tar extraction expects a single archive file"); 110 + bail!("tar extraction expects a single archive file"); 110 111 } 111 112 let mut file = File::open(&paths[0])?; 112 113 io::copy(&mut file, &mut writer)?; ··· 127 128 128 129 impl Tar { 129 130 /// Internal compress helper 130 - fn compress_internal<W: Write>( 131 - &self, 132 - input: CmprssInput, 133 - mut archive: Builder<W>, 134 - ) -> Result<(), io::Error> { 131 + fn compress_internal<W: Write>(&self, input: CmprssInput, mut archive: Builder<W>) -> Result { 135 132 match input { 136 133 CmprssInput::Path(paths) => { 137 134 for path in paths { ··· 143 140 } else if path.is_dir() { 144 141 archive.append_dir_all(path.file_name().unwrap(), path.as_path())?; 145 142 } else { 146 - return cmprss_error("unsupported file type for tar compression"); 143 + bail!("unsupported file type for tar compression"); 147 144 } 148 145 } 149 146 } ··· 155 152 archive.append_file("archive", &mut temp_file)?; 156 153 } 157 154 CmprssInput::Reader(_) => { 158 - return cmprss_error("Cannot tar a reader input directly"); 155 + bail!("Cannot tar a reader input directly"); 159 156 } 160 157 } 161 - archive.finish() 158 + Ok(archive.finish()?) 162 159 } 163 160 } 164 161 ··· 179 176 180 177 /// Test the default compression level 181 178 #[test] 182 - fn test_tar_default_compression() -> Result<(), io::Error> { 179 + fn test_tar_default_compression() -> Result { 183 180 let compressor = Tar::default(); 184 181 test_compression(&compressor) 185 182 } 186 183 187 184 /// Test tar-specific functionality: directory handling 188 185 #[test] 189 - fn test_directory_handling() -> Result<(), Box<dyn std::error::Error>> { 186 + fn test_directory_handling() -> Result { 190 187 let compressor = Tar::default(); 191 188 let dir = assert_fs::TempDir::new()?; 192 189 let file_path = dir.child("file.txt");
+8 -13
src/backends/xz.rs
··· 2 2 progress::{ProgressArgs, copy_with_progress}, 3 3 utils::*, 4 4 }; 5 + use anyhow::bail; 5 6 use clap::Args; 6 7 use std::{ 7 8 fs::File, ··· 60 61 "xz" 61 62 } 62 63 63 - fn compress(&self, input: CmprssInput, output: CmprssOutput) -> Result<(), io::Error> { 64 + fn compress(&self, input: CmprssInput, output: CmprssOutput) -> Result { 64 65 let mut file_size = None; 65 66 let mut input_stream = match input { 66 67 CmprssInput::Path(paths) => { 67 68 if paths.len() > 1 { 68 - return Err(io::Error::new( 69 - io::ErrorKind::InvalidInput, 70 - "Multiple input files not supported for xz", 71 - )); 69 + bail!("Multiple input files not supported for xz"); 72 70 } 73 71 let path = &paths[0]; 74 72 file_size = Some(std::fs::metadata(path)?.len()); ··· 101 99 Ok(()) 102 100 } 103 101 104 - fn extract(&self, input: CmprssInput, output: CmprssOutput) -> Result<(), io::Error> { 102 + fn extract(&self, input: CmprssInput, output: CmprssOutput) -> Result { 105 103 let mut file_size = None; 106 104 let input_stream: Box<dyn Read + Send> = match input { 107 105 CmprssInput::Path(paths) => { 108 106 if paths.len() > 1 { 109 - return Err(io::Error::new( 110 - io::ErrorKind::InvalidInput, 111 - "Multiple input files not supported for xz extraction", 112 - )); 107 + bail!("Multiple input files not supported for xz extraction"); 113 108 } 114 109 let path = &paths[0]; 115 110 file_size = Some(std::fs::metadata(path)?.len()); ··· 156 151 157 152 /// Test the default compression level 158 153 #[test] 159 - fn test_xz_default_compression() -> Result<(), io::Error> { 154 + fn test_xz_default_compression() -> Result { 160 155 let compressor = Xz::default(); 161 156 test_compression(&compressor) 162 157 } 163 158 164 159 /// Test fast compression level 165 160 #[test] 166 - fn test_xz_fast_compression() -> Result<(), io::Error> { 161 + fn test_xz_fast_compression() -> Result { 167 162 let fast_compressor = Xz { 168 163 level: 1, 169 164 progress_args: ProgressArgs::default(), ··· 173 168 174 169 /// Test best compression level 175 170 #[test] 176 - fn test_xz_best_compression() -> Result<(), io::Error> { 171 + fn test_xz_best_compression() -> Result { 177 172 let best_compressor = Xz { 178 173 level: 9, 179 174 progress_args: ProgressArgs::default(),
+20 -25
src/backends/zip.rs
··· 1 1 use crate::utils::*; 2 + use anyhow::bail; 2 3 use clap::Args; 3 4 use std::fs::File; 4 5 use std::io::{self, Seek, SeekFrom, Write}; ··· 22 23 Zip {} 23 24 } 24 25 25 - fn compress_to_file<W: Write + Seek>( 26 - &self, 27 - input: CmprssInput, 28 - writer: W, 29 - ) -> Result<(), io::Error> { 26 + fn compress_to_file<W: Write + Seek>(&self, input: CmprssInput, writer: W) -> Result { 30 27 let mut zip_writer = ZipWriter::new(writer); 31 28 let options = FileOptions::<()>::default().compression_method(CompressionMethod::Deflated); 32 29 ··· 43 40 let base = path.parent().unwrap_or(&path); 44 41 add_directory(&mut zip_writer, base, &path)?; 45 42 } else { 46 - return cmprss_error("unsupported file type for zip compression"); 43 + bail!("unsupported file type for zip compression"); 47 44 } 48 45 } 49 46 } ··· 53 50 io::copy(&mut pipe, &mut zip_writer)?; 54 51 } 55 52 CmprssInput::Reader(_) => { 56 - return cmprss_error("Cannot zip a reader input"); 53 + bail!("Cannot zip a reader input"); 57 54 } 58 55 } 59 56 ··· 72 69 ExtractedTarget::DIRECTORY 73 70 } 74 71 75 - fn compress(&self, input: CmprssInput, output: CmprssOutput) -> Result<(), io::Error> { 72 + fn compress(&self, input: CmprssInput, output: CmprssOutput) -> Result { 76 73 match output { 77 74 CmprssOutput::Path(ref path) => { 78 75 let file = File::create(path)?; ··· 100 97 } 101 98 } 102 99 103 - fn extract(&self, input: CmprssInput, output: CmprssOutput) -> Result<(), io::Error> { 100 + fn extract(&self, input: CmprssInput, output: CmprssOutput) -> Result { 104 101 match output { 105 102 CmprssOutput::Path(ref out_dir) => { 106 103 // Create the output directory if it doesn't exist 107 104 if !out_dir.exists() { 108 105 std::fs::create_dir_all(out_dir)?; 109 106 } else if !out_dir.is_dir() { 110 - return cmprss_error("zip extraction output must be a directory"); 107 + bail!("zip extraction output must be a directory"); 111 108 } 112 109 113 110 match input { 114 111 CmprssInput::Path(paths) => { 115 112 if paths.len() != 1 { 116 - return cmprss_error("zip extraction expects a single archive file"); 113 + bail!("zip extraction expects a single archive file"); 117 114 } 118 115 let file = File::open(&paths[0])?; 119 116 let mut archive = ZipArchive::new(file)?; 120 - archive.extract(out_dir).map_err(io::Error::other) 117 + Ok(archive.extract(out_dir)?) 121 118 } 122 119 CmprssInput::Pipe(mut pipe) => { 123 120 // Create a temporary file to store the zip content ··· 131 128 132 129 // Extract from the temporary file 133 130 let mut archive = ZipArchive::new(temp_file)?; 134 - archive.extract(out_dir).map_err(io::Error::other) 131 + Ok(archive.extract(out_dir)?) 132 + } 133 + CmprssInput::Reader(_) => { 134 + bail!( 135 + "Cannot extract from a reader input for zip (requires seekable input)" 136 + ) 135 137 } 136 - CmprssInput::Reader(_) => cmprss_error( 137 - "Cannot extract from a reader input for zip (requires seekable input)", 138 - ), 139 138 } 140 139 } 141 - CmprssOutput::Pipe(_) => cmprss_error("zip extraction to stdout is not supported"), 140 + CmprssOutput::Pipe(_) => bail!("zip extraction to stdout is not supported"), 142 141 CmprssOutput::Writer(mut writer) => match input { 143 142 CmprssInput::Path(paths) => { 144 143 if paths.len() != 1 { 145 - return cmprss_error("zip extraction expects a single archive file"); 144 + bail!("zip extraction expects a single archive file"); 146 145 } 147 146 let mut file = File::open(&paths[0])?; 148 147 io::copy(&mut file, &mut writer)?; ··· 161 160 } 162 161 } 163 162 164 - fn add_directory<W: Write + Seek>( 165 - zip: &mut ZipWriter<W>, 166 - base: &Path, 167 - path: &Path, 168 - ) -> Result<(), io::Error> { 163 + fn add_directory<W: Write + Seek>(zip: &mut ZipWriter<W>, base: &Path, path: &Path) -> Result { 169 164 for entry in std::fs::read_dir(path)? { 170 165 let entry = entry?; 171 166 let entry_path = entry.path(); ··· 211 206 212 207 /// Test the default compression level 213 208 #[test] 214 - fn test_zip_default_compression() -> Result<(), io::Error> { 209 + fn test_zip_default_compression() -> Result { 215 210 let compressor = Zip::default(); 216 211 test_compression(&compressor) 217 212 } 218 213 219 214 /// Test zip-specific functionality: directory handling 220 215 #[test] 221 - fn test_directory_handling() -> Result<(), Box<dyn std::error::Error>> { 216 + fn test_directory_handling() -> Result { 222 217 let compressor = Zip::default(); 223 218 let dir = assert_fs::TempDir::new()?; 224 219 let file_path = dir.child("file.txt");
+15 -23
src/backends/zstd.rs
··· 1 1 use crate::progress::{ProgressArgs, copy_with_progress}; 2 - use crate::utils::{ 3 - CmprssInput, CmprssOutput, CommonArgs, CompressionLevelValidator, Compressor, LevelArgs, 4 - cmprss_error, 5 - }; 2 + use crate::utils::*; 3 + use anyhow::bail; 6 4 use clap::Args; 7 5 use std::fs::File; 8 6 use std::io::{self, BufReader, BufWriter, Read, Write}; ··· 87 85 } 88 86 89 87 /// Compress an input file or pipe to a zstd archive 90 - fn compress(&self, input: CmprssInput, output: CmprssOutput) -> Result<(), io::Error> { 88 + fn compress(&self, input: CmprssInput, output: CmprssOutput) -> Result { 91 89 if let CmprssOutput::Path(out_path) = &output { 92 90 if out_path.is_dir() { 93 - return cmprss_error( 94 - "Zstd does not support compressing to a directory. Please specify an output file.", 91 + bail!( 92 + "Zstd does not support compressing to a directory. Please specify an output file." 95 93 ); 96 94 } 97 95 } 98 96 if let CmprssInput::Path(input_paths) = &input { 99 97 for x in input_paths { 100 98 if x.is_dir() { 101 - return cmprss_error( 102 - "Zstd does not support compressing a directory. Please specify only files.", 99 + bail!( 100 + "Zstd does not support compressing a directory. Please specify only files." 103 101 ); 104 102 } 105 103 } ··· 108 106 let mut input_stream: Box<dyn Read + Send> = match input { 109 107 CmprssInput::Path(paths) => { 110 108 if paths.len() > 1 { 111 - return Err(io::Error::new( 112 - io::ErrorKind::InvalidInput, 113 - "Multiple input files not supported for zstd", 114 - )); 109 + bail!("Multiple input files not supported for zstd"); 115 110 } 116 111 let path = &paths[0]; 117 112 file_size = Some(std::fs::metadata(path)?.len()); ··· 147 142 } 148 143 149 144 /// Extract a zstd archive to an output file or pipe 150 - fn extract(&self, input: CmprssInput, output: CmprssOutput) -> Result<(), io::Error> { 145 + fn extract(&self, input: CmprssInput, output: CmprssOutput) -> Result { 151 146 if let CmprssOutput::Path(out_path) = &output { 152 147 if out_path.is_dir() { 153 - return cmprss_error( 154 - "Zstd does not support extracting to a directory. Please specify an output file.", 148 + bail!( 149 + "Zstd does not support extracting to a directory. Please specify an output file." 155 150 ); 156 151 } 157 152 } ··· 160 155 let input_stream: Box<dyn Read + Send> = match input { 161 156 CmprssInput::Path(paths) => { 162 157 if paths.len() > 1 { 163 - return Err(io::Error::new( 164 - io::ErrorKind::InvalidInput, 165 - "Multiple input files not supported for zstd extraction", 166 - )); 158 + bail!("Multiple input files not supported for zstd extraction"); 167 159 } 168 160 let path = &paths[0]; 169 161 file_size = Some(std::fs::metadata(path)?.len()); ··· 211 203 212 204 /// Test the default compression level 213 205 #[test] 214 - fn test_zstd_default_compression() -> Result<(), io::Error> { 206 + fn test_zstd_default_compression() -> Result { 215 207 let compressor = Zstd::default(); 216 208 test_compression(&compressor) 217 209 } 218 210 219 211 /// Test fast compression level 220 212 #[test] 221 - fn test_zstd_fast_compression() -> Result<(), io::Error> { 213 + fn test_zstd_fast_compression() -> Result { 222 214 let fast_compressor = Zstd { 223 215 compression_level: 1, 224 216 progress_args: ProgressArgs::default(), ··· 228 220 229 221 /// Test best compression level 230 222 #[test] 231 - fn test_zstd_best_compression() -> Result<(), io::Error> { 223 + fn test_zstd_best_compression() -> Result { 232 224 let best_compressor = Zstd { 233 225 compression_level: 22, 234 226 progress_args: ProgressArgs::default(),
+21 -27
src/main.rs
··· 3 3 pub mod test_utils; 4 4 pub mod utils; 5 5 6 + use anyhow::{anyhow, bail}; 6 7 use backends::*; 7 8 use clap::{Parser, Subcommand}; 8 9 use is_terminal::IsTerminal; 9 - use std::io; 10 10 use std::path::{Path, PathBuf}; 11 11 use utils::*; 12 12 ··· 51 51 52 52 /// Get the input filename or return a default file 53 53 /// This file will be used to generate the output filename 54 - fn get_input_filename(input: &CmprssInput) -> Result<&Path, io::Error> { 54 + fn get_input_filename(input: &CmprssInput) -> Result<&Path> { 55 55 match input { 56 56 CmprssInput::Path(paths) => match paths.first() { 57 57 Some(path) => Ok(path), 58 - None => Err(io::Error::other("error: no input specified")), 58 + None => bail!("error: no input specified"), 59 59 }, 60 60 CmprssInput::Pipe(_) => Ok(Path::new("archive")), 61 61 CmprssInput::Reader(_) => Ok(Path::new("piped_data")), ··· 211 211 } 212 212 213 213 /// Parse the common args and determine the details of the job requested 214 - fn get_job( 215 - compressor: Option<Box<dyn Compressor>>, 216 - common_args: &CommonArgs, 217 - ) -> Result<Job, io::Error> { 214 + fn get_job(compressor: Option<Box<dyn Compressor>>, common_args: &CommonArgs) -> Result<Job> { 218 215 let mut compressor = compressor; 219 216 let mut action = { 220 217 if common_args.compress { ··· 231 228 match get_path(in_file) { 232 229 Some(path) => inputs.push(path), 233 230 None => { 234 - return Err(io::Error::other("Specified input path does not exist")); 231 + bail!("Specified input path does not exist"); 235 232 } 236 233 } 237 234 } ··· 241 238 let path = Path::new(output); 242 239 if path.try_exists()? && !path.is_dir() { 243 240 // Output path exists, bail out 244 - return Err(io::Error::other("Specified output path already exists")); 241 + bail!("Specified output path already exists"); 245 242 } 246 243 Some(path) 247 244 } ··· 286 283 if let Some(path) = get_path(input) { 287 284 inputs.push(path); 288 285 } else { 289 - return Err(io::Error::other("Specified input path does not exist")); 286 + bail!("Specified input path does not exist"); 290 287 } 291 288 } 292 289 ··· 299 296 { 300 297 CmprssInput::Pipe(std::io::stdin()) 301 298 } else { 302 - return Err(io::Error::other("No specified input")); 299 + bail!("No specified input"); 303 300 } 304 301 } 305 302 false => CmprssInput::Path(inputs), ··· 318 315 Action::Compress => { 319 316 let c = compressor 320 317 .as_ref() 321 - .ok_or_else(|| io::Error::other("Must specify a compressor"))?; 318 + .ok_or_else(|| anyhow!("Must specify a compressor"))?; 322 319 CmprssOutput::Path(PathBuf::from( 323 320 c.default_compressed_filename(get_input_filename(&cmprss_input)?), 324 321 )) ··· 330 327 } 331 328 let c = compressor 332 329 .as_ref() 333 - .ok_or_else(|| io::Error::other("Must specify a compressor"))?; 330 + .ok_or_else(|| anyhow!("Must specify a compressor"))?; 334 331 CmprssOutput::Path(PathBuf::from( 335 332 c.default_extracted_filename(get_input_filename(&cmprss_input)?), 336 333 )) ··· 366 363 get_compressor_from_filename(get_input_filename(&cmprss_input)?); 367 364 let c = compressor 368 365 .as_ref() 369 - .ok_or_else(|| io::Error::other("Must specify a compressor"))?; 366 + .ok_or_else(|| anyhow!("Must specify a compressor"))?; 370 367 action = Action::Extract; 371 368 CmprssOutput::Path(PathBuf::from( 372 369 c.default_extracted_filename(get_input_filename(&cmprss_input)?), ··· 390 387 Action::Extract => { 391 388 if let CmprssInput::Path(paths) = &cmprss_input { 392 389 if paths.len() != 1 { 393 - return Err(io::Error::other("Expected a single archive to extract")); 390 + bail!("Expected a single archive to extract"); 394 391 } 395 392 compressor = get_compressor_from_filename(paths.first().unwrap()); 396 393 } ··· 402 399 action = Action::Extract; 403 400 404 401 if compressor.is_none() { 405 - return Err(io::Error::other(format!( 402 + bail!( 406 403 "Couldn't determine how to extract {:?}", 407 404 paths.first().unwrap() 408 - ))); 405 + ); 409 406 } 410 407 } else { 411 408 let (guessed_compressor, guessed_action) = ··· 428 425 } 429 426 } else { 430 427 if paths.len() != 1 { 431 - return Err(io::Error::other( 432 - "Expected a single input file for piping to stdout", 433 - )); 428 + bail!("Expected a single input file for piping to stdout"); 434 429 } 435 430 compressor = get_compressor_from_filename(paths.first().unwrap()); 436 431 if compressor.is_some() { 437 432 action = Action::Extract; 438 433 } else { 439 - return Err(io::Error::other("Can't guess compressor to use")); 434 + bail!("Can't guess compressor to use"); 440 435 } 441 436 } 442 437 } ··· 454 449 if compressor.is_some() { 455 450 action = Action::Compress; 456 451 } else { 457 - return Err(io::Error::other("Can't guess compressor to use")); 452 + bail!("Can't guess compressor to use"); 458 453 } 459 454 } 460 455 } ··· 476 471 } 477 472 } 478 473 479 - let compressor = 480 - compressor.ok_or_else(|| io::Error::other("Could not determine compressor to use"))?; 474 + let compressor = compressor.ok_or_else(|| anyhow!("Could not determine compressor to use"))?; 481 475 if action == Action::Unknown { 482 - return Err(io::Error::other("Could not determine action to take")); 476 + bail!("Could not determine action to take"); 483 477 } 484 478 485 479 Ok(Job { ··· 490 484 }) 491 485 } 492 486 493 - fn command(compressor: Option<Box<dyn Compressor>>, args: &CommonArgs) -> Result<(), io::Error> { 487 + fn command(compressor: Option<Box<dyn Compressor>>, args: &CommonArgs) -> Result { 494 488 let job = get_job(compressor, args)?; 495 489 496 490 match job.action { 497 491 Action::Compress => job.compressor.compress(job.input, job.output)?, 498 492 Action::Extract => job.compressor.extract(job.input, job.output)?, 499 493 _ => { 500 - return Err(io::Error::other("Unknown action requested")); 494 + bail!("Unknown action requested"); 501 495 } 502 496 }; 503 497
+4 -8
src/test_utils.rs
··· 1 1 use crate::utils::ExtractedTarget; 2 2 use std::fs; 3 - use std::io; 4 3 use std::path::Path; 5 4 use tempfile::tempdir; 6 5 7 - use crate::utils::{CmprssInput, CmprssOutput, CompressionLevelValidator, Compressor}; 6 + use crate::utils::{CmprssInput, CmprssOutput, CompressionLevelValidator, Compressor, Result}; 8 7 9 8 /// Test basic trait functionality that should be common across all compressors 10 9 pub fn test_compressor_interface<T: Compressor>( ··· 66 65 } 67 66 68 67 /// Test compression and extraction functionality with a simple string 69 - pub fn test_compressor_roundtrip<T: Compressor>( 70 - compressor: &T, 71 - test_data: &str, 72 - ) -> Result<(), io::Error> { 68 + pub fn test_compressor_roundtrip<T: Compressor>(compressor: &T, test_data: &str) -> Result { 73 69 let temp_dir = tempdir().expect("Failed to create temp dir"); 74 70 75 71 // Create test file ··· 107 103 } 108 104 109 105 /// Test compression and extraction with different content sizes 110 - pub fn test_compression<T: Compressor>(compressor: &T) -> Result<(), io::Error> { 106 + pub fn test_compression<T: Compressor>(compressor: &T) -> Result { 111 107 // Test with empty content 112 108 test_compressor_roundtrip(compressor, "")?; 113 109 ··· 126 122 compressor: &T, 127 123 expected_name: &str, 128 124 expected_extension: Option<&str>, 129 - ) -> Result<(), io::Error> { 125 + ) -> Result { 130 126 // Test interface methods 131 127 test_compressor_interface(compressor, expected_name, expected_extension); 132 128
+9 -21
src/utils.rs
··· 6 6 use std::path::{Path, PathBuf}; 7 7 use std::str::FromStr; 8 8 9 + pub type Result<T = ()> = anyhow::Result<T>; 10 + 9 11 /// Enum to represent whether a compressor extracts to a file or directory by default 10 12 #[derive(Debug, Clone, Copy, PartialEq, Eq)] 11 13 pub enum ExtractedTarget { ··· 126 128 impl FromStr for CompressionLevel { 127 129 type Err = &'static str; 128 130 129 - fn from_str(s: &str) -> Result<Self, Self::Err> { 131 + fn from_str(s: &str) -> std::result::Result<Self, Self::Err> { 130 132 // Check for an int 131 133 if let Ok(level) = s.parse::<i32>() { 132 134 return Ok(CompressionLevel { level }); ··· 216 218 "archive".to_string() 217 219 } 218 220 219 - fn compress(&self, input: CmprssInput, output: CmprssOutput) -> Result<(), io::Error>; 221 + fn compress(&self, input: CmprssInput, output: CmprssOutput) -> Result; 220 222 221 - fn extract(&self, input: CmprssInput, output: CmprssOutput) -> Result<(), io::Error>; 223 + fn extract(&self, input: CmprssInput, output: CmprssOutput) -> Result; 222 224 } 223 225 224 226 impl fmt::Debug for dyn Compressor { ··· 227 229 } 228 230 } 229 231 230 - pub fn cmprss_error(message: &str) -> Result<(), io::Error> { 231 - Err(io::Error::other(message)) 232 - } 233 - 234 232 /// Wrapper for Read + Send to allow Debug 235 233 pub struct ReadWrapper(pub Box<dyn Read + Send>); 236 234 ··· 288 286 #[cfg(test)] 289 287 mod tests { 290 288 use super::*; 291 - use std::io; 292 289 use std::path::Path; 293 290 294 291 /// A simple implementation of the Compressor trait for testing ··· 301 298 302 299 // We'll use the default implementation for extension() and other methods 303 300 304 - fn compress(&self, _: CmprssInput, _: CmprssOutput) -> Result<(), io::Error> { 305 - // Return success for testing purposes 301 + fn compress(&self, _: CmprssInput, _: CmprssOutput) -> Result { 306 302 Ok(()) 307 303 } 308 304 309 - fn extract(&self, _: CmprssInput, _: CmprssOutput) -> Result<(), io::Error> { 310 - // Return success for testing purposes 305 + fn extract(&self, _: CmprssInput, _: CmprssOutput) -> Result { 311 306 Ok(()) 312 307 } 313 308 } ··· 324 319 "cst" 325 320 } 326 321 327 - fn compress(&self, _: CmprssInput, _: CmprssOutput) -> Result<(), io::Error> { 322 + fn compress(&self, _: CmprssInput, _: CmprssOutput) -> Result { 328 323 Ok(()) 329 324 } 330 325 331 - fn extract(&self, _: CmprssInput, _: CmprssOutput) -> Result<(), io::Error> { 326 + fn extract(&self, _: CmprssInput, _: CmprssOutput) -> Result { 332 327 Ok(()) 333 328 } 334 329 } ··· 429 424 let default_level = CompressionLevel::default(); 430 425 let validator = DefaultCompressionValidator; 431 426 assert_eq!(default_level.level, validator.default_level()); 432 - } 433 - 434 - #[test] 435 - fn test_cmprss_error() { 436 - let result = cmprss_error("test error"); 437 - assert!(result.is_err()); 438 - assert_eq!(result.unwrap_err().to_string(), "test error"); 439 427 } 440 428 441 429 #[test]