···243243 // Add all extensions: input.txt → input.txt.tar.gz
244244 let base = in_path
245245 .file_name()
246246- .unwrap_or_else(|| std::ffi::OsStr::new("archive"))
247247- .to_str()
248248- .unwrap();
246246+ .map(|n| n.to_string_lossy().into_owned())
247247+ .unwrap_or_else(|| "archive".to_string());
249248 format!("{}.{}", base, self.format_chain())
250249 }
251250···256255 // Strip all known extensions: input.tar.gz → input
257256 let mut name = in_path
258257 .file_name()
259259- .unwrap_or_else(|| std::ffi::OsStr::new("archive"))
260260- .to_str()
261261- .unwrap()
262262- .to_string();
258258+ .map(|n| n.to_string_lossy().into_owned())
259259+ .unwrap_or_else(|| "archive".to_string());
263260 for comp in self.compressors.iter().rev() {
264261 let ext = format!(".{}", comp.extension());
265262 if let Some(stripped) = name.strip_suffix(&ext) {
+6-2
src/backends/sevenz.rs
···44 CmprssInput, CmprssOutput, CommonArgs, CompressionLevelValidator, Compressor,
55 DefaultCompressionValidator, ExtractedTarget, LevelArgs, Result,
66};
77-use anyhow::bail;
77+use anyhow::{anyhow, bail};
88use clap::Args;
99use indicatif::ProgressBar;
1010use sevenz_rust2::{
···8282 match input {
8383 CmprssInput::Path(paths) => {
8484 for path in paths {
8585- let name = path.file_name().unwrap().to_string_lossy().to_string();
8585+ let name = path
8686+ .file_name()
8787+ .ok_or_else(|| anyhow!("input path has no file name: {:?}", path))?
8888+ .to_string_lossy()
8989+ .to_string();
8690 if path.is_file() {
8791 push_file_entry(&mut aw, &name, &path, bar)?;
8892 } else if path.is_dir() {
+4-2
src/backends/tar.rs
···11extern crate tar;
2233-use anyhow::bail;
33+use anyhow::{anyhow, bail};
44use clap::Args;
55use indicatif::ProgressBar;
66use std::fs::{File, OpenOptions};
···240240 match input {
241241 CmprssInput::Path(paths) => {
242242 for path in paths {
243243- let name = path.file_name().unwrap();
243243+ let name = path
244244+ .file_name()
245245+ .ok_or_else(|| anyhow!("input path has no file name: {:?}", path))?;
244246 if path.is_file() {
245247 append_file_entry(&mut archive, Path::new(name), &path, bar)?;
246248 } else if path.is_dir() {
+8-3
src/backends/zip.rs
···44 CmprssInput, CmprssOutput, CommonArgs, CompressionLevelValidator, Compressor,
55 DefaultCompressionValidator, ExtractedTarget, LevelArgs, Result,
66};
77-use anyhow::bail;
77+use anyhow::{anyhow, bail};
88use clap::Args;
99use indicatif::ProgressBar;
1010use std::fs::{File, OpenOptions};
···9999 CmprssInput::Path(paths) => {
100100 for path in paths {
101101 if path.is_file() {
102102- let name = path.file_name().unwrap().to_string_lossy();
102102+ let name = path
103103+ .file_name()
104104+ .ok_or_else(|| anyhow!("input path has no file name: {:?}", path))?
105105+ .to_string_lossy();
103106 zip_writer.start_file(name, options)?;
104107 let f = File::open(&path)?;
105108 let mut reader = ProgressReader::new(f, bar.cloned());
···308311 let entry = entry?;
309312 let entry_path = entry.path();
310313 // Get relative path for archive entry
314314+ // `entry_path` is a direct child of `path`, which itself sits under
315315+ // `base`, so stripping always succeeds.
311316 let name = entry_path
312317 .strip_prefix(base)
313313- .unwrap()
318318+ .expect("entry path is under base")
314319 .to_string_lossy()
315320 .replace('\\', "/");
316321 if entry_path.is_file() {
+34-28
src/job.rs
···305305 }
306306 Some(Action::Extract) => {
307307 if let CmprssInput::Path(paths) = input {
308308- if paths.len() != 1 {
308308+ let [archive_path] = paths.as_slice() else {
309309 bail!("Expected exactly one input archive");
310310- }
311311- *compressor = get_compressor_from_filename(paths.first().unwrap());
310310+ };
311311+ *compressor = get_compressor_from_filename(archive_path);
312312 }
313313 }
314314 None => match (input, output) {
315315- (CmprssInput::Path(paths), CmprssOutput::Path(path)) => {
316316- if path.is_dir() && paths.len() == 1 {
317317- *compressor = get_compressor_from_filename(paths.first().unwrap());
315315+ (CmprssInput::Path(paths), CmprssOutput::Path(path)) => match paths.as_slice() {
316316+ [single] if path.is_dir() => {
317317+ *compressor = get_compressor_from_filename(single);
318318 *action = Some(Action::Extract);
319319 if compressor.is_none() {
320320- bail!(
321321- "Could not determine compressor for {:?}",
322322- paths.first().unwrap()
323323- );
320320+ bail!("Could not determine compressor for {:?}", single);
324321 }
325325- } else {
322322+ }
323323+ _ => {
326324 let (c, a) = guess_from_filenames(paths, path, compressor.take())?;
327325 *compressor = Some(c);
328326 *action = Some(a);
329327 }
330330- }
328328+ },
331329 (CmprssInput::Path(paths), CmprssOutput::Pipe(_)) => {
330330+ // `resolve_input` guarantees `paths` is non-empty when it
331331+ // returns `CmprssInput::Path`, so `first()` is always Some —
332332+ // but surface a clean error instead of relying on the invariant.
333333+ let first = paths
334334+ .first()
335335+ .ok_or_else(|| anyhow!("No input file specified"))?;
332336 if let Some(c) = compressor.as_deref() {
333333- *action = Some(match get_compressor_from_filename(paths.first().unwrap()) {
337337+ *action = Some(match get_compressor_from_filename(first) {
334338 Some(ic) if ic.name() == c.name() => Action::Extract,
335339 _ => Action::Compress,
336340 });
···338342 if paths.len() != 1 {
339343 bail!("Expected exactly one input file when writing to stdout");
340344 }
341341- *compressor = get_compressor_from_filename(paths.first().unwrap());
345345+ *compressor = get_compressor_from_filename(first);
342346 if compressor.is_some() {
343347 *action = Some(Action::Extract);
344348 } else {
···448452 output: &Path,
449453 compressor: Option<Box<dyn Compressor>>,
450454) -> Result<(Box<dyn Compressor>, Action)> {
451451- if input.len() != 1 {
452452- if let Some(c) = get_compressor_from_filename(output) {
453453- return Ok((c, Action::Compress));
454454- }
455455- if output.is_dir()
456456- && let Some(first) = input.first()
457457- && let Some(c) = get_compressor_from_filename(first)
458458- {
455455+ let input = match input {
456456+ [single] => single,
457457+ _ => {
458458+ if let Some(c) = get_compressor_from_filename(output) {
459459+ return Ok((c, Action::Compress));
460460+ }
461461+ if output.is_dir()
462462+ && let Some(first) = input.first()
463463+ && let Some(c) = get_compressor_from_filename(first)
464464+ {
465465+ return Ok((c, Action::Extract));
466466+ }
467467+ // No extension hint anywhere, but we were given a compressor —
468468+ // assume the user wants to extract multiple archives to a directory.
469469+ let c = compressor.ok_or_else(|| anyhow!("Could not determine compressor to use"))?;
459470 return Ok((c, Action::Extract));
460471 }
461461- // No extension hint anywhere, but we were given a compressor —
462462- // assume the user wants to extract multiple archives to a directory.
463463- let c = compressor.ok_or_else(|| anyhow!("Could not determine compressor to use"))?;
464464- return Ok((c, Action::Extract));
465465- }
466466- let input = input.first().unwrap();
472472+ };
467473468474 let output_guess = get_compressor_from_filename(output);
469475 let input_guess = get_compressor_from_filename(input);
···143143 return Ok(CompressionLevel { level });
144144 }
145145146146- // Try to parse special names
147147- let s = s.to_lowercase();
148148- match s.as_str() {
149149- "none" | "fast" | "best" => Ok(CompressionLevel {
150150- // We'll use the DefaultCompressionValidator values here
151151- // The actual compressor will interpret these values according to its own validator
152152- level: DefaultCompressionValidator.name_to_level(&s).unwrap(),
153153- }),
154154- _ => Err("Invalid compression level"),
155155- }
146146+ // Otherwise expect a named level ("none"/"fast"/"best"). The concrete
147147+ // compressor re-interprets this value through its own validator, so we
148148+ // start from the default mapping.
149149+ let level = DefaultCompressionValidator
150150+ .name_to_level(&s.to_lowercase())
151151+ .ok_or("Invalid compression level")?;
152152+ Ok(CompressionLevel { level })
156153 }
157154}
158155···213210 /// Just checks the extension by default
214211 /// Some compressors may overwrite this to do more advanced detection
215212 fn is_archive(&self, in_path: &Path) -> bool {
216216- if in_path.extension().is_none() {
217217- return false;
218218- }
219219- in_path.extension().unwrap() == self.extension()
213213+ in_path
214214+ .extension()
215215+ .is_some_and(|ext| ext == self.extension())
220216 }
221217222218 /// Generate the default name for the compressed file