···88use is_terminal::IsTerminal;
99use std::io;
1010use std::path::{Path, PathBuf};
1111-use std::{io, vec};
1211use utils::*;
13121413/// A compression multi-tool
···8281 action: Action,
8382}
84838585-/// Get a compressor from a filename
8484+/// Get a compressor from a filename, detecting multi-level formats like tar.gz
8685fn get_compressor_from_filename(filename: &Path) -> Option<Box<dyn Compressor>> {
8787- // Use just the filename component to avoid dots in directory names
8886 let file_name = filename.file_name()?.to_str()?;
8787+ let parts: Vec<&str> = file_name.split('.').collect();
89889090- // Prioritize checking for multi-level formats first
9191- {
9292- let parts: Vec<&str> = file_name.split('.').collect();
9393- // A potential multi-level format like "archive.tar.gz" will have at least 3 parts
9494- if parts.len() >= 3 {
9595- // Get all available single compressors for matching extensions
9696- let single_compressors: Vec<Box<dyn Compressor>> = vec![
9797- Box::<Tar>::default(),
9898- Box::<Gzip>::default(),
9999- Box::<Xz>::default(),
100100- Box::<Bzip2>::default(),
101101- Box::<Zip>::default(),
102102- Box::<Zstd>::default(),
103103- Box::<Lz4>::default(),
104104- ];
8989+ // Try multi-level detection (e.g., "archive.tar.gz" → [tar, gzip])
9090+ if parts.len() >= 3 {
9191+ // Extensions right-to-left, e.g., ["gz", "tar"]
9292+ let extensions: Vec<&str> = parts[1..].iter().rev().copied().collect();
10593106106- // Get extensions in reverse order (from right to left, e.g., "gz", then "tar")
107107- let mut extensions: Vec<String> = Vec::new();
108108- for i in 1..parts.len() {
109109- // Iterate from the last extension backwards
110110- // Stop before including the base filename part if it's just "filename.gz" (parts.len() would be 2)
111111- // This loop is for parts.len() >= 3, ensuring we look at actual extensions
112112- if parts.len() - i > 0 {
113113- // Ensure we don't go out of bounds for the base filename part
114114- extensions.push(parts[parts.len() - i].to_string());
115115- } else {
116116- break; // Should not happen if parts.len() >=3 and i starts at 1
117117- }
9494+ // Resolve each extension to a compressor name
9595+ let mut compressor_names: Vec<String> = Vec::new();
9696+ for ext in &extensions {
9797+ if let Some(c) = backends::compressor_from_str(ext) {
9898+ compressor_names.push(c.name().to_string());
9999+ } else {
100100+ compressor_names.clear();
101101+ break;
118102 }
103103+ }
119104120120- let mut compressor_types: Vec<String> = Vec::new();
121121- for ext_part in &extensions {
122122- // e.g., ext_part is "gz", then "tar"
123123- let mut found_match = false;
124124- for sc in &single_compressors {
125125- if sc.extension() == ext_part || sc.name() == ext_part {
126126- compressor_types.push(sc.name().to_string());
127127- found_match = true;
128128- break;
129129- }
130130- }
131131- if !found_match {
132132- // If any extension part is not recognized, this is not a valid multi-level chain we know.
133133- // Clear types and break, so we can fall back to simple single extension check.
134134- compressor_types.clear();
135135- break;
136136- }
137137- }
138138-139139- // If we successfully identified a chain of known compressor types:
140140- // compressor_types would be e.g. ["gzip", "tar"] (outermost to innermost)
141141- if !compressor_types.is_empty() {
142142- // MultiLevelCompressor::from_names expects innermost to outermost.
143143- compressor_types.reverse(); // e.g., ["tar", "gzip"]
144144- return Some(create_multi_level_compressor(&compressor_types));
105105+ if compressor_names.len() > 1 {
106106+ // Reverse to innermost-to-outermost order for MultiLevelCompressor
107107+ compressor_names.reverse();
108108+ if let Ok(multi) = MultiLevelCompressor::from_names(&compressor_names) {
109109+ return Some(Box::new(multi));
145110 }
146111 }
147112 }
148113149149- // Fallback: try matching a single known compressor extension
150150- for sc in [
151151- Box::<Tar>::default() as Box<dyn Compressor>,
152152- Box::<Gzip>::default(),
153153- Box::<Xz>::default(),
154154- Box::<Bzip2>::default(),
155155- Box::<Zip>::default(),
156156- Box::<Zstd>::default(),
157157- Box::<Lz4>::default(),
158158- ] {
159159- let expected_extension = format!(".{}", sc.extension());
160160- if file_name.ends_with(&expected_extension) {
161161- return Some(sc);
162162- }
163163- }
164164- None
165165-}
166166-167167-/// Get a single compressor from an extension string (no multi-level detection)
168168-fn get_compressor_from_extension(ext: &str) -> Option<Box<dyn Compressor>> {
169169- match ext {
170170- "tar" => Some(Box::<Tar>::default()),
171171- "gz" => Some(Box::<Gzip>::default()),
172172- "xz" => Some(Box::<Xz>::default()),
173173- "bz2" => Some(Box::<Bzip2>::default()),
174174- "zip" => Some(Box::<Zip>::default()),
175175- "zst" => Some(Box::<Zstd>::default()),
176176- "lz4" => Some(Box::<Lz4>::default()),
177177- _ => None,
178178- }
179179-}
180180-181181-/// Create a MultiLevelCompressor from a list of compressor types
182182-fn create_multi_level_compressor(compressor_types: &[String]) -> Box<dyn Compressor> {
183183- // Create a MultiLevelCompressor from the list of compressor types
184184- match MultiLevelCompressor::from_names(compressor_types) {
185185- Ok(multi) => Box::new(multi),
186186- Err(_) => {
187187- // Fallback to the first compressor if there's an error
188188- match compressor_types[0].as_str() {
189189- "tar" => Box::<Tar>::default(),
190190- "gzip" | "gz" => Box::<Gzip>::default(),
191191- "xz" => Box::<Xz>::default(),
192192- "bzip2" | "bz2" => Box::<Bzip2>::default(),
193193- "zip" => Box::<Zip>::default(),
194194- "zstd" | "zst" => Box::<Zstd>::default(),
195195- "lz4" => Box::<Lz4>::default(),
196196- _ => Box::<Tar>::default(), // Default to tar if unknown
197197- }
198198- }
199199- }
114114+ // Single extension fallback
115115+ let ext = parts.last()?;
116116+ backends::compressor_from_str(ext)
200117}
201118202119/// Convert an input path into a Path
···275192276193 if guessed_output == output_file {
277194 // Input is "archive.tar", output is "archive.tar.gz" — only add the outer layer
278278- let single_compressor = get_compressor_from_extension(output_ext.to_str().unwrap_or(""));
195195+ let single_compressor =
196196+ backends::compressor_from_str(output_ext.to_str().unwrap_or(""));
279197 (single_compressor.or(Some(c)), Action::Compress)
280198 } else if guessed_input == input_file {
281199 // Output is "archive.tar", input is "archive.tar.gz" — only strip the outer layer
282282- let single_compressor = get_compressor_from_extension(input_ext.to_str().unwrap_or(""));
200200+ let single_compressor =
201201+ backends::compressor_from_str(input_ext.to_str().unwrap_or(""));
283202 (single_compressor.or(Some(e)), Action::Extract)
284203 } else if c.name() == e.name() {
285204 // Same format for input and output, can't decide