1#![forbid(unsafe_code)]
2#![doc = include_str!("../README.md")]
3
4use core::fmt;
7
8pub const BROTLI_EXTENSION: &str = "br";
10pub const TAR_BROTLI_EXTENSION: &str = "tar.br";
12pub const BROTLI_EXTENSIONS: &[&str] = &["br", "brotli", "tar.br"];
14
15#[derive(Clone, Copy, Debug, Default, Eq, Hash, Ord, PartialEq, PartialOrd)]
17pub enum BrotliExtension {
18 Br,
20 TarBr,
22 #[default]
24 Unknown,
25}
26
27impl BrotliExtension {
28 #[must_use]
30 pub const fn as_str(self) -> &'static str {
31 match self {
32 Self::Br => "br",
33 Self::TarBr => "tar.br",
34 Self::Unknown => "unknown",
35 }
36 }
37
38 #[must_use]
40 pub fn from_extension(extension: &str) -> Self {
41 match normalize_extension(extension).as_str() {
42 "br" | "brotli" => Self::Br,
43 "tar.br" | "tar.brotli" => Self::TarBr,
44 _ => Self::Unknown,
45 }
46 }
47}
48
49impl fmt::Display for BrotliExtension {
50 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
51 formatter.write_str(self.as_str())
52 }
53}
54
55#[derive(Clone, Copy, Debug, Default, Eq, Hash, Ord, PartialEq, PartialOrd)]
57pub enum BrotliLevel {
58 Fastest,
60 #[default]
62 Balanced,
63 Best,
65 Numeric(u32),
67 Unknown,
69}
70
71impl BrotliLevel {
72 #[must_use]
74 pub const fn as_str(self) -> &'static str {
75 match self {
76 Self::Fastest => "fastest",
77 Self::Balanced => "balanced",
78 Self::Best => "best",
79 Self::Numeric(_) => "numeric",
80 Self::Unknown => "unknown",
81 }
82 }
83
84 #[must_use]
86 pub const fn numeric(self) -> Option<u32> {
87 match self {
88 Self::Numeric(level) => Some(level),
89 Self::Fastest | Self::Balanced | Self::Best | Self::Unknown => None,
90 }
91 }
92}
93
94#[derive(Clone, Copy, Debug, Default, Eq, Hash, Ord, PartialEq, PartialOrd)]
96pub enum BrotliProfile {
97 #[default]
99 Generic,
100 Text,
102 Font,
104 Unknown,
106}
107
108impl BrotliProfile {
109 #[must_use]
111 pub const fn as_str(self) -> &'static str {
112 match self {
113 Self::Generic => "generic",
114 Self::Text => "text",
115 Self::Font => "font",
116 Self::Unknown => "unknown",
117 }
118 }
119}
120
121#[must_use]
123pub fn is_brotli_extension(extension: &str) -> bool {
124 !matches!(
125 BrotliExtension::from_extension(extension),
126 BrotliExtension::Unknown
127 )
128}
129
130#[must_use]
132pub fn is_brotli_filename(name: &str) -> bool {
133 let parts = filename_parts(name);
134
135 match parts.as_slice() {
136 [.., last] if matches!(last.as_str(), "br" | "brotli") => true,
137 [.., previous, last] if previous == "tar" && matches!(last.as_str(), "br" | "brotli") => {
138 true
139 },
140 _ => false,
141 }
142}
143
144fn normalize_extension(extension: &str) -> String {
145 extension
146 .trim()
147 .trim_start_matches('.')
148 .to_ascii_lowercase()
149}
150
151fn filename_parts(name: &str) -> Vec<String> {
152 name.trim()
153 .to_ascii_lowercase()
154 .rsplit(['/', '\\'])
155 .next()
156 .unwrap_or_default()
157 .trim_start_matches('.')
158 .split('.')
159 .filter(|part| !part.is_empty())
160 .map(str::to_owned)
161 .collect()
162}
163
164#[cfg(test)]
165mod tests {
166 use super::{
167 BROTLI_EXTENSIONS, BrotliExtension, BrotliLevel, BrotliProfile, is_brotli_extension,
168 is_brotli_filename,
169 };
170
171 #[test]
172 fn detects_brotli_extensions() {
173 assert!(is_brotli_extension(".br"));
174 assert!(is_brotli_extension("tar.br"));
175 assert_eq!(
176 BrotliExtension::from_extension("tar.br"),
177 BrotliExtension::TarBr
178 );
179 assert_eq!(BROTLI_EXTENSIONS[0], "br");
180 }
181
182 #[test]
183 fn detects_brotli_filenames() {
184 assert!(is_brotli_filename("release.tar.br"));
185 assert!(is_brotli_filename("payload.BR"));
186 assert!(!is_brotli_filename("bundle.zip"));
187 }
188
189 #[test]
190 fn exposes_default_and_unknown_labels() {
191 assert_eq!(BrotliExtension::default(), BrotliExtension::Unknown);
192 assert_eq!(BrotliLevel::default().as_str(), "balanced");
193 assert_eq!(BrotliLevel::Numeric(11).numeric(), Some(11));
194 assert_eq!(BrotliProfile::Unknown.as_str(), "unknown");
195 }
196}