• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

tamada / totebag / 21322732509

24 Jan 2026 10:37PM UTC coverage: 81.401% (+0.9%) from 80.516%
21322732509

push

github

web-flow
Merge pull request #66 from tamada/release/v0.8.10

Release/v0.8.10

215 of 251 new or added lines in 13 files covered. (85.66%)

6 existing lines in 3 files now uncovered.

1755 of 2156 relevant lines covered (81.4%)

7.99 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

88.89
/lib/src/format.rs
1
//! Archive format management module.
2
//! This module detects archive formats from file.
3
//! 
4
//! ## Format Detection Strategies
5
//! 
6
//! `totebag` provides three strategies to detect the archive format of a file:
7
//! 
8
//! 1. By file extension (default)
9
//! 2. By magic number (file signature)
10
//! 3. Fixed format (forcing a specific format)
11
//! 
12
//! ### By File Extension
13
//! 
14
//! This is the default strategy used by `totebag`.
15
//! It detects the archive format based on the file extension.
16
//! For example, a file with the extension `.zip` is recognized as a Zip archive.
17
//! 
18
//! ```rust
19
//! use std::path::PathBuf;
20
//! let fd = totebag::format::default_format_detector();
21
//! let format = fd.detect(&PathBuf::from("../testdata/test.zip"))
22
//!     .expect("zip format should be recognized by its file extension");
23
//! ```
24
//! 
25
//! ### By Magic Number
26
//! 
27
//! This strategy detects the archive format by reading the file's magic number (file signature). 
28
//! This method is more reliable than using file extensions, as it examines the actual content of the file.
29
//! However, it may be slightly slower due to the need to read the file.
30
//! Additionally, this method cannot distinguish just `.gz` file and `tar.gz` file.
31
//! See [infer](https://docs.rs/infer/latest/infer/) crate's documentation for more details about supported formats by magic number.
32
//! 
33
//! ```rust
34
//! use std::path::PathBuf;
35
//! let fd = totebag::format::magic_number_format_detector();
36
//! let format = fd.detect(&PathBuf::from("../testdata/test.rar"))
37
//!     .expect("rar format should be recognized by its magic number");
38
//! ```
39
//! 
40
//! ### Fixed format
41
//! 
42
//! This strategy forces `totebag` to treat the file as a specific archive format, regardless of its extension or content.
43
//! This can be useful when dealing with files that have incorrect extensions or when you want to override the default format detection behavior.
44
//! 
45
//! ```rust
46
//! use std::path::PathBuf;
47
//! let fd = totebag::format::fixed_format_detector(
48
//!     totebag::format::find_format_by_name("Rar").unwrap()
49
//! );
50
//! let format = fd.detect(&PathBuf::from("../testdata/test.zip"))
51
//!     .expect("this method always returns the fixed format (this example returns always rar format)");
52
//! ```
53
use std::fmt::Display;
54
use std::path::Path;
55
use std::sync::LazyLock;
56

57
static MANAGER: LazyLock<Manager> = LazyLock::new(Manager::default);
58

59
/// Archive format manager.
60
#[derive(Debug, Clone)]
61
struct Manager {
62
    formats: Vec<Format>,
63
}
64

65
/// Returns an instance of the format detector by file extension.
66
pub fn default_format_detector() -> Box<dyn FormatDetector> {
37✔
67
    Box::new(ExtensionFormatDetector {})
37✔
68
}
37✔
69

70
/// Returns an instance of the format detector by the magic number of file header.
71
pub fn magic_number_format_detector() -> Box<dyn FormatDetector> {
×
72
    Box::new(MagicNumberFormatDetector {})
×
73
}
×
74

75
/// Returns an instance of the format detector for the given format.
76
pub fn fixed_format_detector(format: &'static Format) -> Box<dyn FormatDetector> {
×
77
    Box::new(FixedFormatDetector::new(format))
×
78
}
×
79

80
/// The trait for detecting the archive format of a file.
81
pub trait FormatDetector {
82
    /// Detects the archive format of the given file path.
83
    /// Returns `Some(&`[`Format`]`)` if the format is recognized, otherwise returns `None`.
84
    fn detect(&self, path: &Path) -> Option<&Format>;
85
}
86

87
struct ExtensionFormatDetector;
88
struct MagicNumberFormatDetector;
89
struct FixedFormatDetector {
90
    format: &'static Format,
91
}
92

93
impl FixedFormatDetector {
94
    pub(crate) fn new(format: &'static Format) -> Self {
1✔
95
        Self { format }
1✔
96
    }
1✔
97
}
98

99
impl FormatDetector for MagicNumberFormatDetector {
100
    fn detect(&self, filename: &Path) -> Option<&Format> {
5✔
101
        match infer::get_from_path(filename) {
5✔
102
            Err(e) => {
1✔
103
                log::error!("Failed to read file for format detection: {e:?}");
1✔
104
                None
1✔
105
            },
106
            Ok(Some(info)) => {
4✔
107
                match info.mime_type() {
4✔
108
                    "application/x-cab" => find_format_by_name("Cab"),
4✔
109
                    "application/x-cpio" => find_format_by_name("Cpio"),
4✔
110
                    "application/x-lzh" | "application/x-lha" => find_format_by_name("Lha"),
4✔
111
                    "application/x-7z-compressed" => find_format_by_name("SevenZ"),
4✔
112
                    "application/vnd.rar" => find_format_by_name("Rar"),
4✔
113
                    "application/x-tar" => find_format_by_name("Tar"),
3✔
114
                    "application/gzip" => find_format_by_name("TarGz"),
3✔
115
                    "application/x-bzip2" => find_format_by_name("TarBz2"),
2✔
116
                    "application/x-xz" => find_format_by_name("TarXz"),
2✔
117
                    "application/zstd" => find_format_by_name("TarZstd"),
2✔
118
                    "application/zip" | "application/java-archive" => find_format_by_name("Zip"),
2✔
119
                    other => {
×
NEW
120
                        log::error!("Unknown file format detected by magic number: {filename:?} (mime-type: {other})");
×
121
                        None
×
122
                    }
123
                }
124
            },
125
            Ok(None) => {
NEW
126
                log::error!("Could not detect file format from magic number: {filename:?}");
×
127
                None
×
128
            }
129
        }
130
    }
5✔
131
}
132

133
impl FormatDetector for ExtensionFormatDetector {
134
    fn detect(&self, path: &Path) -> Option<&Format> {
68✔
135
        MANAGER.formats.iter().find(|f| f.match_exts(path))
501✔
136
    }
68✔
137
}
138

139
impl FormatDetector for FixedFormatDetector {
140
    fn detect(&self, _path: &Path) -> Option<&Format> {
1✔
141
        Some(self.format)
1✔
142
    }
1✔
143
}
144

145
impl Default for Manager {
146
    fn default() -> Self {
2✔
147
        Manager::new(vec![
2✔
148
            Format::new("Cab", vec![".cab"]),
2✔
149
            Format::new("Cpio", vec![".cpio"]),
2✔
150
            Format::new("Lha", vec![".lha", ".lzh"]),
2✔
151
            Format::new("SevenZ", vec![".7z"]),
2✔
152
            Format::new("Rar", vec![".rar"]),
2✔
153
            Format::new("Tar", vec![".tar"]),
2✔
154
            Format::new("TarGz", vec![".tar.gz", ".tgz"]),
2✔
155
            Format::new("TarBz2", vec![".tar.bz2", ".tbz2"]),
2✔
156
            Format::new("TarXz", vec![".tar.xz", ".txz"]),
2✔
157
            Format::new("TarZstd", vec![".tar.zst", ".tzst", ".tar.zstd", ".tzstd"]),
2✔
158
            Format::new("Zip", vec![".zip", ".jar", ".war", ".ear"]),
2✔
159
        ])
160
    }
2✔
161
}
162

163
/// Returns `true` if all of the given file names are Some by the given [`FormatDetector::detect`] method.
164
pub fn is_all_archive_file<P: AsRef<Path>>(args: &[P], fd: &dyn FormatDetector) -> bool {
7✔
165
    args.iter().all(|p| fd.detect(p.as_ref()).is_some())
18✔
166
}
7✔
167

168
/// Find the format by its name.
169
/// If the given name is unknown format for totebag, it returns `None`.
170
pub fn find_format_by_name<S: AsRef<str>>(name: S) -> Option<&'static Format> {
8✔
171
    let name = name.as_ref().to_lowercase();
8✔
172
    log::debug!("find format by name: {name}");
8✔
173
    MANAGER.formats.iter().find(|f| f.name.to_lowercase() == name)
77✔
174
}
8✔
175

176
/// Find the instance of [`Format`] from the given file extension.
177
pub fn find_format_by_ext<S: AsRef<str>>(ext: S) -> Option<&'static Format> {
4✔
178
    let ext = ext.as_ref();
4✔
179
    let ext = if !ext.starts_with('.') {
4✔
180
        format!(".{ext}")
1✔
181
    } else {
182
        ext.to_string()
3✔
183
    }.to_lowercase();
4✔
184
    MANAGER.formats.iter().find(|f| f.exts.contains(&ext))
40✔
185
}
4✔
186

187
impl Manager {
188
    pub(crate) fn new(formats: Vec<Format>) -> Self {
2✔
189
        Self { formats }
2✔
190
    }
2✔
191
}
192

193
/// Represents the archive format.
194
#[derive(Debug, PartialEq, Eq, Clone)]
195
pub struct Format {
196
    /// The general format name.
197
    pub name: String,
198
    exts: Vec<String>,
199
}
200

201
impl Display for Format {
202
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
×
203
        write!(f, "{}", self.name)
×
204
    }
×
205
}
206

207
impl AsRef<str> for Format {
208
    fn as_ref(&self) -> &str {
×
209
        &self.name
×
210
    }
×
211
}
212

213
impl From<Format> for String {
214
    fn from(f: Format) -> Self {
×
215
        f.name
×
216
    }
×
217
}
218

219
impl Format {
220
    /// Create an instanceof Format with the name and its extensions.
221
    pub fn new<T: Into<String>>(name: T, exts: Vec<T>) -> Self {
22✔
222
        Self {
223
            name: name.into(),
22✔
224
            exts: exts.into_iter().map(|e| e.into().to_lowercase()).collect(),
42✔
225
        }
226
    }
22✔
227

228
    /// Returns `true` if the given file name has the extension of this format.
229
    pub fn match_exts<P: AsRef<Path>>(&self, p: P) -> bool {
501✔
230
        let p = p.as_ref();
501✔
231
        let name = p.to_str().unwrap().to_lowercase();
501✔
232
        for ext in &self.exts {
752✔
233
            if name.ends_with(ext) {
752✔
234
                return true;
59✔
235
            }
693✔
236
        }
237
        false
442✔
238
    }
501✔
239
}
240

241
#[cfg(test)]
242
mod tests {
243
    use super::*;
244

245
    #[test]
246
    fn test_format() {
1✔
247
        use std::path::PathBuf;
248
        let fd = default_format_detector();
1✔
249
        assert_eq!(fd.detect(&PathBuf::from("hoge.unknown")), None);
1✔
250
        assert_eq!(fd.detect(&PathBuf::from("test.cab")), Some(&MANAGER.formats[0]));
1✔
251
        assert_eq!(fd.detect(&PathBuf::from("test.cpio")), Some(&MANAGER.formats[1]));
1✔
252
        assert_eq!(fd.detect(&PathBuf::from("test.lha")), Some(&MANAGER.formats[2]));
1✔
253
        assert_eq!(fd.detect(&PathBuf::from("test.lzh")), Some(&MANAGER.formats[2]));
1✔
254
        assert_eq!(fd.detect(&PathBuf::from("test.7z")), Some(&MANAGER.formats[3]));
1✔
255
        assert_eq!(fd.detect(&PathBuf::from("test.rar")), Some(&MANAGER.formats[4]));
1✔
256
        assert_eq!(fd.detect(&PathBuf::from("test.tar")), Some(&MANAGER.formats[5]));
1✔
257
        assert_eq!(fd.detect(&PathBuf::from("test.tar.gz")), Some(&MANAGER.formats[6]));
1✔
258
        assert_eq!(fd.detect(&PathBuf::from("test.tgz")), Some(&MANAGER.formats[6]));
1✔
259
        assert_eq!(fd.detect(&PathBuf::from("test.tar.bz2")), Some(&MANAGER.formats[7]));
1✔
260
        assert_eq!(fd.detect(&PathBuf::from("test.tbz2")), Some(&MANAGER.formats[7]));
1✔
261
        assert_eq!(fd.detect(&PathBuf::from("test.tar.xz")), Some(&MANAGER.formats[8]));
1✔
262
        assert_eq!(fd.detect(&PathBuf::from("test.txz")), Some(&MANAGER.formats[8]));
1✔
263
        assert_eq!(fd.detect(&PathBuf::from("test.tar.zst")), Some(&MANAGER.formats[9]));
1✔
264
        assert_eq!(fd.detect(&PathBuf::from("test.tzst")), Some(&MANAGER.formats[9]));
1✔
265
        assert_eq!(fd.detect(&PathBuf::from("test.tar.zstd")), Some(&MANAGER.formats[9]));
1✔
266
        assert_eq!(fd.detect(&PathBuf::from("test.tzstd")), Some(&MANAGER.formats[9]));
1✔
267
        assert_eq!(fd.detect(&PathBuf::from("test.zip")), Some(&MANAGER.formats[10]));
1✔
268
        assert_eq!(fd.detect(&PathBuf::from("test.jar")), Some(&MANAGER.formats[10]));
1✔
269
        assert_eq!(fd.detect(&PathBuf::from("test.ear")), Some(&MANAGER.formats[10]));
1✔
270
        assert_eq!(fd.detect(&PathBuf::from("test.war")), Some(&MANAGER.formats[10]));
1✔
271
    }
1✔
272

273
    #[test]
274
    fn test_is_all_args_archives() {
1✔
275
        let fd = default_format_detector();
1✔
276
        assert!(is_all_archive_file(&[
1✔
277
            "test.zip",
1✔
278
            "test.tar",
1✔
279
            "test.tar.gz",
1✔
280
            "test.tgz",
1✔
281
            "test.tar.bz2",
1✔
282
            "test.tbz2",
1✔
283
            "test.rar",
1✔
284
        ], fd.as_ref()));
1✔
285
    }
1✔
286

287
    #[test]
288
    fn test_find_by_name() {
1✔
289
        let format = find_format_by_name("zip").unwrap();
1✔
290
        assert_eq!(format.name, "Zip");
1✔
291
        let format = find_format_by_name("TaRZsTd").unwrap();
1✔
292
        assert_eq!(format.name, "TarZstd");
1✔
293
        let format = find_format_by_name("unknown");
1✔
294
        assert!(format.is_none());
1✔
295
    }
1✔
296

297
    #[test]
298
    fn test_find_by_ext() {
1✔
299
        let format = find_format_by_ext(".ZIP").unwrap();
1✔
300
        assert_eq!(format.name, "Zip");
1✔
301
        let format = find_format_by_ext("tAr.Gz").unwrap();
1✔
302
        assert_eq!(format.name, "TarGz");
1✔
303
        let format = find_format_by_ext(".unknown");
1✔
304
        assert!(format.is_none());
1✔
305
    }
1✔
306

307
    #[test]
308
    fn test_extension_format_detector() {
1✔
309
        let detector = ExtensionFormatDetector {};
1✔
310
        let format = detector.detect(Path::new("test.zip")).unwrap();
1✔
311
        assert_eq!(format.name, "Zip");
1✔
312
        let format = detector.detect(Path::new("test.tar.gz")).unwrap();
1✔
313
        assert_eq!(format.name, "TarGz");
1✔
314
        let format = detector.detect(Path::new("test.rar")).unwrap();
1✔
315
        assert_eq!(format.name, "Rar");
1✔
316
        let format = detector.detect(Path::new("test.unknown"));
1✔
317
        assert!(format.is_none());
1✔
318
    }
1✔
319

320
    #[test]
321
    fn test_magic_number_format_detector() {
1✔
322
        let detector = MagicNumberFormatDetector {};
1✔
323
        let format = detector.detect(Path::new("../testdata/test.zip")).unwrap();
1✔
324
        assert_eq!(format.name, "Zip");
1✔
325
        let format = detector.detect(Path::new("../testdata/test.tar.gz")).unwrap();
1✔
326
        assert_eq!(format.name, "TarGz");
1✔
327
        let format = detector.detect(Path::new("../testdata/test.rar")).unwrap();
1✔
328
        assert_eq!(format.name, "Rar");
1✔
329
        let format = detector.detect(Path::new("../testdata/camouflage_of_zip.rar")).unwrap();
1✔
330
        assert_eq!(format.name, "Zip");
1✔
331
        let format = detector.detect(Path::new("../testdata/not_exist_file.rar"));
1✔
332
        assert!(format.is_none());
1✔
333
    }
1✔
334

335
    #[test]
336
    fn test_fixed_format_detector() {
1✔
337
        let format = find_format_by_name("Zip").unwrap();
1✔
338
        let detector = FixedFormatDetector::new(format);
1✔
339
        let detected_format = detector.detect(Path::new("anyfile.anyext")).unwrap();
1✔
340
        assert_eq!(detected_format.name, "Zip");
1✔
341
    }
1✔
342
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc