Skip to main content

use_lz4/
lib.rs

1#![forbid(unsafe_code)]
2#![doc = include_str!("../README.md")]
3
4//! LZ4 labels and option metadata for `RustUse`.
5
6use core::fmt;
7
8/// Common LZ4 file extension.
9pub const LZ4_EXTENSION: &str = "lz4";
10/// Common LZ4-compressed tar extension.
11pub const TAR_LZ4_EXTENSION: &str = "tar.lz4";
12/// Common LZ4-related extensions.
13pub const LZ4_EXTENSIONS: &[&str] = &["lz4", "tar.lz4"];
14
15/// LZ4 extension labels.
16#[derive(Clone, Copy, Debug, Default, Eq, Hash, Ord, PartialEq, PartialOrd)]
17pub enum Lz4Extension {
18    /// `.lz4` payload.
19    Lz4,
20    /// `.tar.lz4` tar plus LZ4 extension.
21    TarLz4,
22    /// Unknown or unsupported LZ4 extension label.
23    #[default]
24    Unknown,
25}
26
27impl Lz4Extension {
28    /// Returns a stable lowercase label.
29    #[must_use]
30    pub const fn as_str(self) -> &'static str {
31        match self {
32            Self::Lz4 => "lz4",
33            Self::TarLz4 => "tar.lz4",
34            Self::Unknown => "unknown",
35        }
36    }
37
38    /// Detects an LZ4 extension label.
39    #[must_use]
40    pub fn from_extension(extension: &str) -> Self {
41        match normalize_extension(extension).as_str() {
42            "lz4" => Self::Lz4,
43            "tar.lz4" => Self::TarLz4,
44            _ => Self::Unknown,
45        }
46    }
47}
48
49impl fmt::Display for Lz4Extension {
50    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
51        formatter.write_str(self.as_str())
52    }
53}
54
55/// LZ4 frame kind labels.
56#[derive(Clone, Copy, Debug, Default, Eq, Hash, Ord, PartialEq, PartialOrd)]
57pub enum Lz4FrameKind {
58    /// Standard LZ4 frame.
59    #[default]
60    Standard,
61    /// Legacy LZ4 frame.
62    Legacy,
63    /// Skippable frame.
64    Skippable,
65    /// Raw block label.
66    Block,
67    /// Unknown or intentionally unspecified frame kind.
68    Unknown,
69}
70
71impl Lz4FrameKind {
72    /// Returns a stable lowercase label.
73    #[must_use]
74    pub const fn as_str(self) -> &'static str {
75        match self {
76            Self::Standard => "standard",
77            Self::Legacy => "legacy",
78            Self::Skippable => "skippable",
79            Self::Block => "block",
80            Self::Unknown => "unknown",
81        }
82    }
83}
84
85/// High-level LZ4 usage profile.
86#[derive(Clone, Copy, Debug, Default, Eq, Hash, Ord, PartialEq, PartialOrd)]
87pub enum Lz4Profile {
88    /// Prefer compression speed.
89    Speed,
90    /// Prefer compressed size.
91    Size,
92    /// Balance speed and size.
93    #[default]
94    Balanced,
95    /// Unknown or intentionally unspecified profile.
96    Unknown,
97}
98
99impl Lz4Profile {
100    /// Returns a stable lowercase label.
101    #[must_use]
102    pub const fn as_str(self) -> &'static str {
103        match self {
104            Self::Speed => "speed",
105            Self::Size => "size",
106            Self::Balanced => "balanced",
107            Self::Unknown => "unknown",
108        }
109    }
110}
111
112/// Returns whether `extension` is a known LZ4 extension label.
113#[must_use]
114pub fn is_lz4_extension(extension: &str) -> bool {
115    !matches!(
116        Lz4Extension::from_extension(extension),
117        Lz4Extension::Unknown
118    )
119}
120
121/// Returns whether `name` has a known LZ4 filename encoding.
122#[must_use]
123pub fn is_lz4_filename(name: &str) -> bool {
124    let parts = filename_parts(name);
125
126    match parts.as_slice() {
127        [.., last] if last == "lz4" => true,
128        [.., previous, last] if previous == "tar" && last == "lz4" => true,
129        _ => false,
130    }
131}
132
133fn normalize_extension(extension: &str) -> String {
134    extension
135        .trim()
136        .trim_start_matches('.')
137        .to_ascii_lowercase()
138}
139
140fn filename_parts(name: &str) -> Vec<String> {
141    name.trim()
142        .to_ascii_lowercase()
143        .rsplit(['/', '\\'])
144        .next()
145        .unwrap_or_default()
146        .trim_start_matches('.')
147        .split('.')
148        .filter(|part| !part.is_empty())
149        .map(str::to_owned)
150        .collect()
151}
152
153#[cfg(test)]
154mod tests {
155    use super::{
156        LZ4_EXTENSIONS, Lz4Extension, Lz4FrameKind, Lz4Profile, is_lz4_extension, is_lz4_filename,
157    };
158
159    #[test]
160    fn detects_lz4_extensions() {
161        assert!(is_lz4_extension(".lz4"));
162        assert!(is_lz4_extension("tar.lz4"));
163        assert_eq!(
164            Lz4Extension::from_extension("tar.lz4"),
165            Lz4Extension::TarLz4
166        );
167        assert_eq!(LZ4_EXTENSIONS[0], "lz4");
168    }
169
170    #[test]
171    fn detects_lz4_filenames() {
172        assert!(is_lz4_filename("release.tar.lz4"));
173        assert!(is_lz4_filename("payload.LZ4"));
174        assert!(!is_lz4_filename("bundle.zip"));
175    }
176
177    #[test]
178    fn exposes_default_and_unknown_labels() {
179        assert_eq!(Lz4Extension::default(), Lz4Extension::Unknown);
180        assert_eq!(Lz4FrameKind::default().as_str(), "standard");
181        assert_eq!(Lz4Profile::Unknown.as_str(), "unknown");
182    }
183}