Skip to main content

use_7z/
lib.rs

1#![forbid(unsafe_code)]
2#![doc = include_str!("../README.md")]
3
4//! 7-Zip archive labels and compression method metadata for `RustUse`.
5
6use core::fmt;
7
8/// Common 7-Zip archive extension.
9pub const SEVEN_ZIP_EXTENSION: &str = "7z";
10/// Common first split-volume 7-Zip extension.
11pub const SEVEN_ZIP_FIRST_VOLUME_EXTENSION: &str = "7z.001";
12/// Common 7-Zip-related extensions.
13pub const SEVEN_ZIP_EXTENSIONS: &[&str] = &["7z", "7z.001"];
14
15/// 7-Zip archive format labels.
16#[derive(Clone, Copy, Debug, Default, Eq, Hash, Ord, PartialEq, PartialOrd)]
17pub enum SevenZipFormat {
18    /// 7-Zip archive label.
19    SevenZip,
20    /// Split-volume 7-Zip archive label.
21    SplitVolume,
22    /// Unknown or intentionally unspecified 7-Zip format label.
23    #[default]
24    Unknown,
25}
26
27impl SevenZipFormat {
28    /// Returns a stable lowercase label.
29    #[must_use]
30    pub const fn as_str(self) -> &'static str {
31        match self {
32            Self::SevenZip => "7z",
33            Self::SplitVolume => "7z-volume",
34            Self::Unknown => "unknown",
35        }
36    }
37}
38
39impl fmt::Display for SevenZipFormat {
40    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
41        formatter.write_str(self.as_str())
42    }
43}
44
45/// 7-Zip compression method labels.
46#[derive(Clone, Copy, Debug, Default, Eq, Hash, Ord, PartialEq, PartialOrd)]
47pub enum SevenZipCompressionMethod {
48    /// Copy/stored method.
49    Copy,
50    /// LZMA method.
51    Lzma,
52    /// LZMA2 method.
53    Lzma2,
54    /// Bzip2 method.
55    Bzip2,
56    /// Deflate method.
57    Deflate,
58    /// PPMd method.
59    Ppmd,
60    /// Unknown or unsupported method.
61    #[default]
62    Unknown,
63}
64
65impl SevenZipCompressionMethod {
66    /// Returns a stable lowercase label.
67    #[must_use]
68    pub const fn as_str(self) -> &'static str {
69        match self {
70            Self::Copy => "copy",
71            Self::Lzma => "lzma",
72            Self::Lzma2 => "lzma2",
73            Self::Bzip2 => "bzip2",
74            Self::Deflate => "deflate",
75            Self::Ppmd => "ppmd",
76            Self::Unknown => "unknown",
77        }
78    }
79}
80
81impl fmt::Display for SevenZipCompressionMethod {
82    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
83        formatter.write_str(self.as_str())
84    }
85}
86
87/// Returns whether `extension` is a known 7-Zip extension label.
88#[must_use]
89pub fn is_7z_extension(extension: &str) -> bool {
90    let normalized = normalize_extension(extension);
91    normalized == "7z" || is_7z_volume_extension(&normalized)
92}
93
94/// Returns whether `name` has a known 7-Zip filename encoding.
95#[must_use]
96pub fn is_7z_filename(name: &str) -> bool {
97    let parts = filename_parts(name);
98
99    match parts.as_slice() {
100        [.., last] if last == "7z" => true,
101        [.., previous, last] if previous == "7z" && is_volume_number(last) => true,
102        _ => false,
103    }
104}
105
106fn is_7z_volume_extension(extension: &str) -> bool {
107    let parts = extension
108        .split('.')
109        .filter(|part| !part.is_empty())
110        .collect::<Vec<_>>();
111
112    matches!(parts.as_slice(), ["7z", part] if is_volume_number(part))
113}
114
115fn is_volume_number(part: &str) -> bool {
116    part.len() == 3 && part.bytes().all(|byte| byte.is_ascii_digit())
117}
118
119fn normalize_extension(extension: &str) -> String {
120    extension
121        .trim()
122        .trim_start_matches('.')
123        .to_ascii_lowercase()
124}
125
126fn filename_parts(name: &str) -> Vec<String> {
127    name.trim()
128        .to_ascii_lowercase()
129        .rsplit(['/', '\\'])
130        .next()
131        .unwrap_or_default()
132        .trim_start_matches('.')
133        .split('.')
134        .filter(|part| !part.is_empty())
135        .map(str::to_owned)
136        .collect()
137}
138
139#[cfg(test)]
140mod tests {
141    use super::{
142        SEVEN_ZIP_EXTENSIONS, SevenZipCompressionMethod, SevenZipFormat, is_7z_extension,
143        is_7z_filename,
144    };
145
146    #[test]
147    fn detects_7z_extensions() {
148        assert!(is_7z_extension(".7z"));
149        assert!(is_7z_extension("7z.001"));
150        assert!(is_7z_extension("7z.120"));
151        assert_eq!(SEVEN_ZIP_EXTENSIONS[0], "7z");
152    }
153
154    #[test]
155    fn detects_7z_filenames() {
156        assert!(is_7z_filename("bundle.7z"));
157        assert!(is_7z_filename("bundle.7z.001"));
158        assert!(!is_7z_filename("bundle.zip"));
159    }
160
161    #[test]
162    fn exposes_default_and_unknown_labels() {
163        assert_eq!(SevenZipFormat::default(), SevenZipFormat::Unknown);
164        assert_eq!(SevenZipFormat::SplitVolume.as_str(), "7z-volume");
165        assert_eq!(
166            SevenZipCompressionMethod::default(),
167            SevenZipCompressionMethod::Unknown
168        );
169        assert_eq!(SevenZipCompressionMethod::Ppmd.as_str(), "ppmd");
170    }
171}