1#![forbid(unsafe_code)]
2#![doc = include_str!("../README.md")]
3
4use core::fmt;
7
8pub const MTREE_EXTENSION: &str = "mtree";
10pub const MTREE_GZIP_EXTENSION: &str = "mtree.gz";
12pub const MTREE_EXTENSIONS: &[&str] = &["mtree", "mtree.gz"];
14
15#[derive(Clone, Copy, Debug, Default, Eq, Hash, Ord, PartialEq, PartialOrd)]
17pub enum MtreeFormat {
18 Bsd,
20 NetBsd,
22 FreeBsd,
24 #[default]
26 Unknown,
27}
28
29impl MtreeFormat {
30 #[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#[derive(Clone, Copy, Debug, Default, Eq, Hash, Ord, PartialEq, PartialOrd)]
50pub enum MtreeEntryKind {
51 File,
53 Directory,
55 Link,
57 Device,
59 #[default]
61 Unknown,
62}
63
64impl MtreeEntryKind {
65 #[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#[derive(Clone, Copy, Debug, Default, Eq, Hash, Ord, PartialEq, PartialOrd)]
86pub enum MtreeKeyword {
87 Type,
89 Uid,
91 Gid,
93 Mode,
95 Size,
97 Time,
99 Sha256Digest,
101 #[default]
103 Unknown,
104}
105
106impl MtreeKeyword {
107 #[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#[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#[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}