Skip to main content

use_mtree/
lib.rs

1#![forbid(unsafe_code)]
2#![doc = include_str!("../README.md")]
3
4//! mtree manifest labels and entry metadata for `RustUse`.
5
6use core::fmt;
7
8/// Common mtree manifest extension.
9pub const MTREE_EXTENSION: &str = "mtree";
10/// Common gzip-compressed mtree manifest extension.
11pub const MTREE_GZIP_EXTENSION: &str = "mtree.gz";
12/// Common mtree-related extensions.
13pub const MTREE_EXTENSIONS: &[&str] = &["mtree", "mtree.gz"];
14
15/// mtree dialect labels.
16#[derive(Clone, Copy, Debug, Default, Eq, Hash, Ord, PartialEq, PartialOrd)]
17pub enum MtreeFormat {
18    /// BSD mtree label.
19    Bsd,
20    /// NetBSD mtree label.
21    NetBsd,
22    /// FreeBSD mtree label.
23    FreeBsd,
24    /// Unknown or intentionally unspecified mtree format.
25    #[default]
26    Unknown,
27}
28
29impl MtreeFormat {
30    /// Returns a stable lowercase label.
31    #[must_use]
32    pub const fn as_str(self) -> &'static str {
33        match self {
34            Self::Bsd => "bsd",
35            Self::NetBsd => "netbsd",
36            Self::FreeBsd => "freebsd",
37            Self::Unknown => "unknown",
38        }
39    }
40}
41
42impl fmt::Display for MtreeFormat {
43    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
44        formatter.write_str(self.as_str())
45    }
46}
47
48/// mtree entry kind labels.
49#[derive(Clone, Copy, Debug, Default, Eq, Hash, Ord, PartialEq, PartialOrd)]
50pub enum MtreeEntryKind {
51    /// File entry.
52    File,
53    /// Directory entry.
54    Directory,
55    /// Link entry.
56    Link,
57    /// Device entry.
58    Device,
59    /// Unknown or unsupported entry kind.
60    #[default]
61    Unknown,
62}
63
64impl MtreeEntryKind {
65    /// Returns a stable lowercase label.
66    #[must_use]
67    pub const fn as_str(self) -> &'static str {
68        match self {
69            Self::File => "file",
70            Self::Directory => "directory",
71            Self::Link => "link",
72            Self::Device => "device",
73            Self::Unknown => "unknown",
74        }
75    }
76}
77
78impl fmt::Display for MtreeEntryKind {
79    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
80        formatter.write_str(self.as_str())
81    }
82}
83
84/// mtree keyword labels.
85#[derive(Clone, Copy, Debug, Default, Eq, Hash, Ord, PartialEq, PartialOrd)]
86pub enum MtreeKeyword {
87    /// Entry type keyword.
88    Type,
89    /// User ID keyword.
90    Uid,
91    /// Group ID keyword.
92    Gid,
93    /// Mode keyword.
94    Mode,
95    /// Size keyword.
96    Size,
97    /// Time keyword.
98    Time,
99    /// SHA-256 digest keyword.
100    Sha256Digest,
101    /// Unknown or unsupported keyword.
102    #[default]
103    Unknown,
104}
105
106impl MtreeKeyword {
107    /// Returns a stable lowercase label.
108    #[must_use]
109    pub const fn as_str(self) -> &'static str {
110        match self {
111            Self::Type => "type",
112            Self::Uid => "uid",
113            Self::Gid => "gid",
114            Self::Mode => "mode",
115            Self::Size => "size",
116            Self::Time => "time",
117            Self::Sha256Digest => "sha256digest",
118            Self::Unknown => "unknown",
119        }
120    }
121}
122
123impl fmt::Display for MtreeKeyword {
124    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
125        formatter.write_str(self.as_str())
126    }
127}
128
129/// Returns whether `extension` is a known mtree extension label.
130#[must_use]
131pub fn is_mtree_extension(extension: &str) -> bool {
132    matches!(
133        normalize_extension(extension).as_str(),
134        "mtree" | "mtree.gz"
135    )
136}
137
138/// Returns whether `name` has a known mtree filename encoding.
139#[must_use]
140pub fn is_mtree_filename(name: &str) -> bool {
141    let parts = filename_parts(name);
142
143    match parts.as_slice() {
144        [.., last] if last == "mtree" => true,
145        [.., previous, last] if previous == "mtree" && last == "gz" => true,
146        _ => false,
147    }
148}
149
150fn normalize_extension(extension: &str) -> String {
151    extension
152        .trim()
153        .trim_start_matches('.')
154        .to_ascii_lowercase()
155}
156
157fn filename_parts(name: &str) -> Vec<String> {
158    name.trim()
159        .to_ascii_lowercase()
160        .rsplit(['/', '\\'])
161        .next()
162        .unwrap_or_default()
163        .trim_start_matches('.')
164        .split('.')
165        .filter(|part| !part.is_empty())
166        .map(str::to_owned)
167        .collect()
168}
169
170#[cfg(test)]
171mod tests {
172    use super::{
173        MTREE_EXTENSIONS, MtreeEntryKind, MtreeFormat, MtreeKeyword, is_mtree_extension,
174        is_mtree_filename,
175    };
176
177    #[test]
178    fn detects_mtree_extensions() {
179        assert!(is_mtree_extension(".mtree"));
180        assert!(is_mtree_extension("mtree.gz"));
181        assert_eq!(MTREE_EXTENSIONS[0], "mtree");
182    }
183
184    #[test]
185    fn detects_mtree_filenames() {
186        assert!(is_mtree_filename("manifest.mtree"));
187        assert!(is_mtree_filename("manifest.MTREE.GZ"));
188        assert!(!is_mtree_filename("bundle.zip"));
189    }
190
191    #[test]
192    fn exposes_default_and_unknown_labels() {
193        assert_eq!(MtreeFormat::default(), MtreeFormat::Unknown);
194        assert_eq!(MtreeFormat::NetBsd.as_str(), "netbsd");
195        assert_eq!(MtreeEntryKind::default(), MtreeEntryKind::Unknown);
196        assert_eq!(MtreeEntryKind::Link.as_str(), "link");
197        assert_eq!(MtreeKeyword::default(), MtreeKeyword::Unknown);
198        assert_eq!(MtreeKeyword::Sha256Digest.as_str(), "sha256digest");
199    }
200}