1#![forbid(unsafe_code)]
2#![doc = include_str!("../README.md")]
3
4use core::fmt;
7
8pub const SEVEN_ZIP_EXTENSION: &str = "7z";
10pub const SEVEN_ZIP_FIRST_VOLUME_EXTENSION: &str = "7z.001";
12pub const SEVEN_ZIP_EXTENSIONS: &[&str] = &["7z", "7z.001"];
14
15#[derive(Clone, Copy, Debug, Default, Eq, Hash, Ord, PartialEq, PartialOrd)]
17pub enum SevenZipFormat {
18 SevenZip,
20 SplitVolume,
22 #[default]
24 Unknown,
25}
26
27impl SevenZipFormat {
28 #[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#[derive(Clone, Copy, Debug, Default, Eq, Hash, Ord, PartialEq, PartialOrd)]
47pub enum SevenZipCompressionMethod {
48 Copy,
50 Lzma,
52 Lzma2,
54 Bzip2,
56 Deflate,
58 Ppmd,
60 #[default]
62 Unknown,
63}
64
65impl SevenZipCompressionMethod {
66 #[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#[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#[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}