Skip to main content

use_brotli/
lib.rs

1#![forbid(unsafe_code)]
2#![doc = include_str!("../README.md")]
3
4//! Brotli labels and option metadata for `RustUse`.
5
6use core::fmt;
7
8/// Common Brotli file extension.
9pub const BROTLI_EXTENSION: &str = "br";
10/// Common Brotli-compressed tar extension.
11pub const TAR_BROTLI_EXTENSION: &str = "tar.br";
12/// Common Brotli-related extensions.
13pub const BROTLI_EXTENSIONS: &[&str] = &["br", "brotli", "tar.br"];
14
15/// Brotli extension labels.
16#[derive(Clone, Copy, Debug, Default, Eq, Hash, Ord, PartialEq, PartialOrd)]
17pub enum BrotliExtension {
18    /// `.br` payload.
19    Br,
20    /// `.tar.br` tar plus Brotli extension.
21    TarBr,
22    /// Unknown or unsupported Brotli extension label.
23    #[default]
24    Unknown,
25}
26
27impl BrotliExtension {
28    /// Returns a stable lowercase label.
29    #[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    /// Detects a Brotli extension label.
39    #[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/// Brotli compression level labels.
56#[derive(Clone, Copy, Debug, Default, Eq, Hash, Ord, PartialEq, PartialOrd)]
57pub enum BrotliLevel {
58    /// Prefer fastest compression.
59    Fastest,
60    /// Balance speed and compressed size.
61    #[default]
62    Balanced,
63    /// Prefer best compressed size.
64    Best,
65    /// Caller-specified numeric level.
66    Numeric(u32),
67    /// Unknown or intentionally unspecified level.
68    Unknown,
69}
70
71impl BrotliLevel {
72    /// Returns a stable lowercase label.
73    #[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    /// Returns the numeric level when this is [`BrotliLevel::Numeric`].
85    #[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/// High-level Brotli usage profile.
95#[derive(Clone, Copy, Debug, Default, Eq, Hash, Ord, PartialEq, PartialOrd)]
96pub enum BrotliProfile {
97    /// Generic binary payload profile.
98    #[default]
99    Generic,
100    /// Text-oriented profile.
101    Text,
102    /// Font-oriented profile.
103    Font,
104    /// Unknown or intentionally unspecified profile.
105    Unknown,
106}
107
108impl BrotliProfile {
109    /// Returns a stable lowercase label.
110    #[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/// Returns whether `extension` is a known Brotli extension label.
122#[must_use]
123pub fn is_brotli_extension(extension: &str) -> bool {
124    !matches!(
125        BrotliExtension::from_extension(extension),
126        BrotliExtension::Unknown
127    )
128}
129
130/// Returns whether `name` has a known Brotli filename encoding.
131#[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}