this repo has no description
0
fork

Configure Feed

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

refactor(job): eliminate Action::Unknown in favor of Option<Action>

Action now cleanly models only the two real operations (Compress,
Extract); 'not yet resolved' is expressed as Option::None during
inference inside get_job. guess_from_filenames returns
Result<(Box<dyn Compressor>, Action)> and bails directly in the
previously-ambiguous cases instead of leaking Unknown upward.

Knock-on: main.rs's dispatch no longer needs its catch-all 'Unknown
action requested' bail — exhaustive matching on the two-variant enum.

+101 -110
+98 -101
src/job.rs
··· 13 13 use crate::backends::{self, Pipeline}; 14 14 use crate::utils::{CmprssInput, CmprssOutput, CommonArgs, Compressor, Result}; 15 15 16 - /// Extract an action hint from the CLI flags. Returns `Action::Unknown` when 17 - /// the user hasn't specified `--compress`/`--extract`/`--decompress`. 18 - fn action_from_flags(args: &CommonArgs) -> Action { 16 + /// Extract an action hint from the CLI flags. Returns `None` when the user 17 + /// hasn't specified `--compress`/`--extract`/`--decompress`, in which case 18 + /// the action will be inferred from filenames downstream. 19 + fn action_from_flags(args: &CommonArgs) -> Option<Action> { 19 20 if args.compress { 20 - Action::Compress 21 + Some(Action::Compress) 21 22 } else if args.extract || args.decompress { 22 - Action::Extract 23 + Some(Action::Extract) 23 24 } else { 24 - Action::Unknown 25 + None 25 26 } 26 27 } 27 28 ··· 37 38 /// directory is treated as another input to archive. 38 39 fn partition_paths( 39 40 args: &CommonArgs, 40 - action_hint: Action, 41 + action_hint: Option<Action>, 41 42 ) -> Result<(Vec<PathBuf>, Option<PathBuf>)> { 42 43 let mut inputs = Vec::new(); 43 44 if let Some(in_file) = &args.input { ··· 64 65 if !path.try_exists()? { 65 66 output = Some(path); 66 67 io_list.pop(); 67 - } else if path.is_dir() && action_hint == Action::Extract { 68 + } else if path.is_dir() && action_hint == Some(Action::Extract) { 68 69 // Only treat an existing directory as the output when the user 69 70 // hinted extraction. In Compress/Unknown, we keep it as another 70 71 // input — this matches e.g. `cmprss tar dir1/ dir2/`. ··· 112 113 pub enum Action { 113 114 Compress, 114 115 Extract, 115 - Unknown, 116 116 } 117 117 118 118 /// Parse the common args and determine the details of the job requested. ··· 160 160 let default_name = match action { 161 161 Action::Compress => compressor.default_compressed_filename(get_input_filename(&input)?), 162 162 Action::Extract => compressor.default_extracted_filename(get_input_filename(&input)?), 163 - Action::Unknown => bail!("Could not determine action to take"), 164 163 }; 165 164 Ok(Job { 166 165 compressor, ··· 174 173 /// (either `CmprssOutput::Path` or `CmprssOutput::Pipe`). 175 174 fn finalize_with_output( 176 175 mut compressor: Option<Box<dyn Compressor>>, 177 - mut action: Action, 176 + mut action: Option<Action>, 178 177 input: &CmprssInput, 179 178 output: &CmprssOutput, 180 179 ) -> Result<(Box<dyn Compressor>, Action)> { 181 - if compressor.is_none() || action == Action::Unknown { 180 + if compressor.is_none() || action.is_none() { 182 181 fill_missing_from_io(&mut compressor, &mut action, input, output)?; 183 182 } 184 183 let compressor = compressor.ok_or_else(|| anyhow!("Could not determine compressor to use"))?; 185 - if action == Action::Unknown { 186 - bail!("Could not determine action to take"); 187 - } 184 + let action = action.ok_or_else(|| anyhow!("Could not determine action to take"))?; 188 185 Ok((compressor, action)) 189 186 } 190 187 ··· 193 190 /// the input side. 194 191 fn finalize_without_output( 195 192 compressor: Option<Box<dyn Compressor>>, 196 - action: Action, 193 + action: Option<Action>, 197 194 input: &CmprssInput, 198 195 ) -> Result<(Box<dyn Compressor>, Action)> { 199 196 let input_path = get_input_filename(input)?; 200 197 match action { 201 - Action::Compress => { 198 + Some(Action::Compress) => { 202 199 let c = compressor.ok_or_else(|| anyhow!("Must specify a compressor"))?; 203 200 Ok((c, Action::Compress)) 204 201 } 205 - Action::Extract => { 202 + Some(Action::Extract) => { 206 203 let c = compressor 207 204 .or_else(|| get_compressor_from_filename(input_path)) 208 205 .ok_or_else(|| anyhow!("Must specify a compressor"))?; 209 206 Ok((c, Action::Extract)) 210 207 } 211 - Action::Unknown => match compressor { 208 + None => match compressor { 212 209 Some(c) => { 213 210 // Compare the compressor's extension against the input's. 214 211 let action = match get_compressor_from_filename(input_path) { ··· 232 229 /// of (Path, Pipe) input × (Path, Pipe) output. 233 230 fn fill_missing_from_io( 234 231 compressor: &mut Option<Box<dyn Compressor>>, 235 - action: &mut Action, 232 + action: &mut Option<Action>, 236 233 input: &CmprssInput, 237 234 output: &CmprssOutput, 238 235 ) -> Result { 239 236 match *action { 240 - Action::Compress => { 237 + Some(Action::Compress) => { 241 238 if let CmprssOutput::Path(path) = output { 242 239 *compressor = get_compressor_from_filename(path); 243 240 } 244 241 } 245 - Action::Extract => { 242 + Some(Action::Extract) => { 246 243 if let CmprssInput::Path(paths) = input { 247 244 if paths.len() != 1 { 248 245 bail!("Expected a single archive to extract"); ··· 250 247 *compressor = get_compressor_from_filename(paths.first().unwrap()); 251 248 } 252 249 } 253 - Action::Unknown => match (input, output) { 250 + None => match (input, output) { 254 251 (CmprssInput::Path(paths), CmprssOutput::Path(path)) => { 255 252 if path.is_dir() && paths.len() == 1 { 256 253 *compressor = get_compressor_from_filename(paths.first().unwrap()); 257 - *action = Action::Extract; 254 + *action = Some(Action::Extract); 258 255 if compressor.is_none() { 259 256 bail!( 260 257 "Couldn't determine how to extract {:?}", ··· 262 259 ); 263 260 } 264 261 } else { 265 - let (c, a) = guess_from_filenames(paths, path, compressor.take()); 266 - *compressor = c; 267 - *action = a; 262 + let (c, a) = guess_from_filenames(paths, path, compressor.take())?; 263 + *compressor = Some(c); 264 + *action = Some(a); 268 265 } 269 266 } 270 267 (CmprssInput::Path(paths), CmprssOutput::Pipe(_)) => { 271 268 if let Some(c) = compressor.as_deref() { 272 - *action = match get_compressor_from_filename(paths.first().unwrap()) { 269 + *action = Some(match get_compressor_from_filename(paths.first().unwrap()) { 273 270 Some(ic) if ic.name() == c.name() => Action::Extract, 274 271 _ => Action::Compress, 275 - }; 272 + }); 276 273 } else { 277 274 if paths.len() != 1 { 278 275 bail!("Expected a single input file for piping to stdout"); 279 276 } 280 277 *compressor = get_compressor_from_filename(paths.first().unwrap()); 281 278 if compressor.is_some() { 282 - *action = Action::Extract; 279 + *action = Some(Action::Extract); 283 280 } else { 284 281 bail!("Can't guess compressor to use"); 285 282 } ··· 287 284 } 288 285 (CmprssInput::Pipe(_), CmprssOutput::Path(path)) => { 289 286 if let Some(c) = compressor.as_deref() { 290 - *action = if get_compressor_from_filename(path) 291 - .is_some_and(|pc| c.name() == pc.name()) 292 - { 293 - Action::Compress 294 - } else { 295 - Action::Extract 296 - }; 287 + *action = Some( 288 + if get_compressor_from_filename(path) 289 + .is_some_and(|pc| c.name() == pc.name()) 290 + { 291 + Action::Compress 292 + } else { 293 + Action::Extract 294 + }, 295 + ); 297 296 } else { 298 297 *compressor = get_compressor_from_filename(path); 299 298 if compressor.is_some() { 300 - *action = Action::Compress; 299 + *action = Some(Action::Compress); 301 300 } else { 302 301 bail!("Can't guess compressor to use"); 303 302 } 304 303 } 305 304 } 306 305 (CmprssInput::Pipe(_), CmprssOutput::Pipe(_)) => { 307 - *action = Action::Compress; 306 + *action = Some(Action::Compress); 308 307 } 309 308 // Writer output and Reader input are only constructed internally 310 309 // by the Pipeline compressor; they don't reach get_job from main. 311 - (_, CmprssOutput::Writer(_)) => *action = Action::Compress, 312 - (CmprssInput::Reader(_), _) => *action = Action::Extract, 310 + (_, CmprssOutput::Writer(_)) => *action = Some(Action::Compress), 311 + (CmprssInput::Reader(_), _) => *action = Some(Action::Extract), 313 312 }, 314 313 } 315 314 Ok(()) ··· 390 389 Some(path) 391 390 } 392 391 393 - /// Guess compressor/action from the two filenames 394 - /// The compressor may already be given 392 + /// Guess compressor/action from the two filenames. The compressor may already 393 + /// be given via the subcommand. 394 + /// 395 + /// Returns an error when the two filenames don't give enough information to 396 + /// pick an action (e.g. the same format on both sides and the output isn't a 397 + /// directory). 395 398 fn guess_from_filenames( 396 399 input: &[PathBuf], 397 400 output: &Path, 398 401 compressor: Option<Box<dyn Compressor>>, 399 - ) -> (Option<Box<dyn Compressor>>, Action) { 402 + ) -> Result<(Box<dyn Compressor>, Action)> { 400 403 if input.len() != 1 { 401 - if let Some(guessed_compressor) = get_compressor_from_filename(output) { 402 - return (Some(guessed_compressor), Action::Compress); 404 + if let Some(c) = get_compressor_from_filename(output) { 405 + return Ok((c, Action::Compress)); 403 406 } 404 - 405 - // Check if output is a directory - this is likely an extraction 406 - if output.is_dir() { 407 - // Try to determine compressor from the input file's extension(s) 408 - if let Some(input_path) = input.first() 409 - && let Some(guessed_compressor) = get_compressor_from_filename(input_path) 410 - { 411 - return (Some(guessed_compressor), Action::Extract); 412 - } 407 + if output.is_dir() 408 + && let Some(first) = input.first() 409 + && let Some(c) = get_compressor_from_filename(first) 410 + { 411 + return Ok((c, Action::Extract)); 413 412 } 414 - 415 - // In theory we could be extracting multiple files to a directory 416 - // We'll fail somewhere else if that's not the case 417 - return (compressor, Action::Extract); 413 + // No extension hint anywhere, but we were given a compressor — 414 + // assume the user wants to extract multiple archives to a directory. 415 + let c = compressor.ok_or_else(|| anyhow!("Could not determine compressor to use"))?; 416 + return Ok((c, Action::Extract)); 418 417 } 419 418 let input = input.first().unwrap(); 420 419 421 - let guessed_compressor = get_compressor_from_filename(output); 422 - let guessed_extractor = get_compressor_from_filename(input); 423 - let guessed_compressor_name = if let Some(c) = &guessed_compressor { 424 - c.name() 425 - } else { 426 - "" 427 - }; 428 - let guessed_extractor_name = if let Some(e) = &guessed_extractor { 429 - e.name() 430 - } else { 431 - "" 432 - }; 420 + let output_guess = get_compressor_from_filename(output); 421 + let input_guess = get_compressor_from_filename(input); 433 422 434 - if let Some(c) = &compressor { 435 - if guessed_compressor_name == c.name() { 436 - return (compressor, Action::Compress); 437 - } else if guessed_extractor_name == c.name() { 438 - return (compressor, Action::Extract); 423 + // If the user supplied a compressor via subcommand, pick the action by 424 + // matching its name against the input/output extensions. 425 + if let Some(c) = compressor { 426 + let action = if output_guess 427 + .as_ref() 428 + .is_some_and(|og| og.name() == c.name()) 429 + { 430 + Action::Compress 431 + } else if input_guess.as_ref().is_some_and(|ig| ig.name() == c.name()) { 432 + Action::Extract 439 433 } else { 440 - // Default to compressing 441 - return (compressor, Action::Compress); 442 - } 434 + // Extensions don't match on either side; default to compressing. 435 + Action::Compress 436 + }; 437 + return Ok((c, action)); 443 438 } 444 439 445 - match (guessed_compressor, guessed_extractor) { 446 - (None, None) => (None, Action::Unknown), 447 - (Some(c), None) => (Some(c), Action::Compress), 448 - (None, Some(e)) => (Some(e), Action::Extract), 440 + match (output_guess, input_guess) { 441 + (None, None) => bail!("Could not determine compressor to use"), 442 + (Some(c), None) => Ok((c, Action::Compress)), 443 + (None, Some(e)) => Ok((e, Action::Extract)), 449 444 (Some(c), Some(e)) => { 450 - // Compare the input and output extensions to see if one has an extra extension 445 + // Both sides carry a known extension — decide whether this is 446 + // adding or stripping a single outer layer (e.g. tar → tar.gz). 451 447 let input_file = input.file_name().unwrap().to_str().unwrap(); 452 448 let input_ext = input.extension().unwrap_or_default(); 453 449 let output_file = output.file_name().unwrap().to_str().unwrap(); 454 450 let output_ext = output.extension().unwrap_or_default(); 455 - let guessed_output = input_file.to_string() + "." + output_ext.to_str().unwrap(); 456 - let guessed_input = output_file.to_string() + "." + input_ext.to_str().unwrap(); 451 + let layer_added = input_file.to_string() + "." + output_ext.to_str().unwrap(); 452 + let layer_stripped = output_file.to_string() + "." + input_ext.to_str().unwrap(); 457 453 458 - if guessed_output == output_file { 459 - // Input is "archive.tar", output is "archive.tar.gz" — only add the outer layer 460 - let single_compressor = 461 - backends::compressor_from_str(output_ext.to_str().unwrap_or("")); 462 - (single_compressor.or(Some(c)), Action::Compress) 463 - } else if guessed_input == input_file { 464 - // Output is "archive.tar", input is "archive.tar.gz" — only strip the outer layer 465 - let single_compressor = 466 - backends::compressor_from_str(input_ext.to_str().unwrap_or("")); 467 - (single_compressor.or(Some(e)), Action::Extract) 454 + if layer_added == output_file { 455 + // input="archive.tar", output="archive.tar.gz" — add the outer layer only. 456 + let single = 457 + backends::compressor_from_str(output_ext.to_str().unwrap_or("")).unwrap_or(c); 458 + Ok((single, Action::Compress)) 459 + } else if layer_stripped == input_file { 460 + // input="archive.tar.gz", output="archive.tar" — strip the outer layer only. 461 + let single = 462 + backends::compressor_from_str(input_ext.to_str().unwrap_or("")).unwrap_or(e); 463 + Ok((single, Action::Extract)) 468 464 } else if c.name() == e.name() { 469 - // Same format for input and output, can't decide 465 + // Same format on both sides: only meaningful when the output 466 + // is a directory (extracting in place). 470 467 if output.is_dir() { 471 - (Some(e), Action::Extract) 468 + Ok((e, Action::Extract)) 472 469 } else { 473 - (Some(c), Action::Unknown) 470 + bail!("Could not determine action to take"); 474 471 } 475 472 } else if output.is_dir() { 476 - (Some(e), Action::Extract) 473 + Ok((e, Action::Extract)) 477 474 } else { 478 - (None, Action::Unknown) 475 + bail!("Could not determine action to take"); 479 476 } 480 477 } 481 478 }
+3 -9
src/main.rs
··· 62 62 63 63 fn command(compressor: Option<Box<dyn Compressor>>, args: &CommonArgs) -> Result { 64 64 let job = get_job(compressor, args)?; 65 - 66 65 match job.action { 67 - Action::Compress => job.compressor.compress(job.input, job.output)?, 68 - Action::Extract => job.compressor.extract(job.input, job.output)?, 69 - _ => { 70 - anyhow::bail!("Unknown action requested"); 71 - } 72 - }; 73 - 74 - Ok(()) 66 + Action::Compress => job.compressor.compress(job.input, job.output), 67 + Action::Extract => job.compressor.extract(job.input, job.output), 68 + } 75 69 } 76 70 77 71 fn main() {