1use crate::{FsAllocation, FsDeletedNode};
22
23#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
28pub struct Macb {
29 pub modified: i64,
30 pub accessed: i64,
31 pub changed: i64,
32 pub born: i64,
33}
34
35#[derive(Debug, Clone, Copy)]
38pub struct Mark {
39 pub allocation: FsAllocation,
40 pub macb: Macb,
41}
42
43impl Mark {
44 pub fn from_node(node: &FsDeletedNode) -> Self {
48 Self {
49 allocation: node.allocation,
50 macb: Macb {
51 modified: node.mtime.seconds,
52 accessed: node.atime.seconds,
53 changed: node.ctime.seconds,
54 born: node.crtime.seconds,
55 },
56 }
57 }
58}
59
60pub const UNIX_XATTR_NAMES: [&str; 5] = [
62 "user.4n6.status",
63 "user.4n6.macb.modified",
64 "user.4n6.macb.accessed",
65 "user.4n6.macb.changed",
66 "user.4n6.macb.born",
67];
68
69#[derive(Debug, Clone, Copy, PartialEq, Eq)]
71pub enum MarkStream {
72 Status,
74 Macb,
76}
77
78pub const ADS_STREAMS: [MarkStream; 2] = [MarkStream::Status, MarkStream::Macb];
80
81impl MarkStream {
82 pub fn base(self) -> &'static str {
84 match self {
85 MarkStream::Status => "4n6.status",
86 MarkStream::Macb => "4n6.macb",
87 }
88 }
89
90 pub fn from_base(base: &str) -> Option<Self> {
93 match base {
94 "4n6.status" => Some(MarkStream::Status),
95 "4n6.macb" => Some(MarkStream::Macb),
96 _ => None,
97 }
98 }
99
100 pub fn ads_full_name(self) -> String {
103 format!(":{}:$DATA", self.base())
104 }
105}
106
107pub fn status_str(allocation: FsAllocation) -> &'static str {
109 match allocation {
110 FsAllocation::Deleted => "deleted",
111 FsAllocation::Orphan => "orphan",
112 }
113}
114
115#[allow(clippy::many_single_char_names)] pub fn iso8601_utc(secs: i64) -> String {
119 let days = secs.div_euclid(86_400);
120 let tod = secs.rem_euclid(86_400);
121 let (h, m, s) = (tod / 3600, (tod % 3600) / 60, tod % 60);
122 let (y, mon, d) = civil_from_days(days);
123 format!("{y:04}-{mon:02}-{d:02}T{h:02}:{m:02}:{s:02}Z")
124}
125
126pub fn unix_xattr_value(mark: &Mark, name: &str) -> Option<Vec<u8>> {
129 let v = match name {
130 "user.4n6.status" => status_str(mark.allocation).to_string(),
131 "user.4n6.macb.modified" => iso8601_utc(mark.macb.modified),
132 "user.4n6.macb.accessed" => iso8601_utc(mark.macb.accessed),
133 "user.4n6.macb.changed" => iso8601_utc(mark.macb.changed),
134 "user.4n6.macb.born" => iso8601_utc(mark.macb.born),
135 _ => return None,
136 };
137 Some(v.into_bytes())
138}
139
140pub fn ads_stream_value(mark: &Mark, stream: MarkStream) -> Vec<u8> {
143 match stream {
144 MarkStream::Status => status_str(mark.allocation).as_bytes().to_vec(),
145 MarkStream::Macb => {
146 let obj = serde_json::json!({
147 "modified": iso8601_utc(mark.macb.modified),
148 "accessed": iso8601_utc(mark.macb.accessed),
149 "changed": iso8601_utc(mark.macb.changed),
150 "born": iso8601_utc(mark.macb.born),
151 });
152 serde_json::to_vec(&obj).unwrap_or_default()
153 }
154 }
155}
156
157#[allow(clippy::many_single_char_names)] pub(crate) fn civil_from_days(z: i64) -> (i64, u32, u32) {
161 let z = z + 719_468;
162 let era = if z >= 0 { z } else { z - 146_096 } / 146_097;
163 let doe = (z - era * 146_097) as u64; let yoe = (doe - doe / 1460 + doe / 36_524 - doe / 146_096) / 365; let y = yoe as i64 + era * 400;
166 let doy = doe - (365 * yoe + yoe / 4 - yoe / 100); let mp = (5 * doy + 2) / 153; let d = (doy - (153 * mp + 2) / 5 + 1) as u32; let m = if mp < 10 { mp + 3 } else { mp - 9 } as u32; (if m <= 2 { y + 1 } else { y }, m, d)
171}
172
173#[cfg(test)]
174mod tests {
175 use super::*;
176 use crate::{FsFileType, FsTimestamp};
177
178 fn ts(secs: i64) -> FsTimestamp {
179 FsTimestamp {
180 seconds: secs,
181 nanoseconds: 0,
182 }
183 }
184
185 fn sample_mark(allocation: FsAllocation) -> Mark {
186 Mark {
187 allocation,
188 macb: Macb {
189 modified: 1_700_000_000,
190 accessed: 1_700_000_100,
191 changed: 1_700_000_200,
192 born: 1_700_000_300,
193 },
194 }
195 }
196
197 #[test]
198 fn status_str_maps_allocation() {
199 assert_eq!(status_str(FsAllocation::Deleted), "deleted");
200 assert_eq!(status_str(FsAllocation::Orphan), "orphan");
201 }
202
203 #[test]
204 fn iso8601_utc_renders_zulu() {
205 assert_eq!(iso8601_utc(1_700_000_000), "2023-11-14T22:13:20Z");
206 assert_eq!(iso8601_utc(200), "1970-01-01T00:03:20Z");
207 assert_eq!(iso8601_utc(0), "1970-01-01T00:00:00Z");
208 }
209
210 #[test]
211 fn unix_xattr_names_are_the_schema() {
212 assert_eq!(
213 UNIX_XATTR_NAMES,
214 [
215 "user.4n6.status",
216 "user.4n6.macb.modified",
217 "user.4n6.macb.accessed",
218 "user.4n6.macb.changed",
219 "user.4n6.macb.born",
220 ]
221 );
222 }
223
224 #[test]
225 fn unix_xattr_value_status_and_macb() {
226 let d = sample_mark(FsAllocation::Deleted);
227 let o = sample_mark(FsAllocation::Orphan);
228 assert_eq!(
229 unix_xattr_value(&d, "user.4n6.status").as_deref(),
230 Some(&b"deleted"[..])
231 );
232 assert_eq!(
233 unix_xattr_value(&o, "user.4n6.status").as_deref(),
234 Some(&b"orphan"[..])
235 );
236 assert_eq!(
237 unix_xattr_value(&d, "user.4n6.macb.modified").as_deref(),
238 Some(iso8601_utc(1_700_000_000).as_bytes())
239 );
240 assert_eq!(
241 unix_xattr_value(&d, "user.4n6.macb.born").as_deref(),
242 Some(iso8601_utc(1_700_000_300).as_bytes())
243 );
244 assert!(unix_xattr_value(&d, "user.4n6.nope").is_none());
246 assert!(unix_xattr_value(&d, "user.other").is_none());
247 }
248
249 #[test]
250 fn ads_stream_base_names_and_full_names() {
251 assert_eq!(MarkStream::Status.base(), "4n6.status");
252 assert_eq!(MarkStream::Macb.base(), "4n6.macb");
253 assert_eq!(MarkStream::Status.ads_full_name(), ":4n6.status:$DATA");
254 assert_eq!(MarkStream::Macb.ads_full_name(), ":4n6.macb:$DATA");
255 assert_eq!(ADS_STREAMS, [MarkStream::Status, MarkStream::Macb]);
256 }
257
258 #[test]
259 fn ads_from_base_recognizes_only_ours() {
260 assert_eq!(
261 MarkStream::from_base("4n6.status"),
262 Some(MarkStream::Status)
263 );
264 assert_eq!(MarkStream::from_base("4n6.macb"), Some(MarkStream::Macb));
265 assert_eq!(MarkStream::from_base("Zone.Identifier"), None);
266 assert_eq!(MarkStream::from_base(""), None);
267 }
268
269 #[test]
270 fn ads_status_stream_is_the_status_word() {
271 let d = sample_mark(FsAllocation::Deleted);
272 let o = sample_mark(FsAllocation::Orphan);
273 assert_eq!(ads_stream_value(&d, MarkStream::Status), b"deleted");
274 assert_eq!(ads_stream_value(&o, MarkStream::Status), b"orphan");
275 }
276
277 #[test]
278 fn ads_macb_stream_is_json_of_the_four_iso_times() {
279 let d = sample_mark(FsAllocation::Deleted);
280 let bytes = ads_stream_value(&d, MarkStream::Macb);
281 let v: serde_json::Value = serde_json::from_slice(&bytes).expect("valid JSON");
282 assert_eq!(v["modified"], iso8601_utc(1_700_000_000));
283 assert_eq!(v["accessed"], iso8601_utc(1_700_000_100));
284 assert_eq!(v["changed"], iso8601_utc(1_700_000_200));
285 assert_eq!(v["born"], iso8601_utc(1_700_000_300));
286 }
287
288 #[test]
293 fn unix_and_windows_channels_agree_on_values() {
294 let d = sample_mark(FsAllocation::Deleted);
295 assert_eq!(
297 unix_xattr_value(&d, "user.4n6.status").unwrap(),
298 ads_stream_value(&d, MarkStream::Status)
299 );
300 let macb_json: serde_json::Value =
303 serde_json::from_slice(&ads_stream_value(&d, MarkStream::Macb)).unwrap();
304 for (xattr, field) in [
305 ("user.4n6.macb.modified", "modified"),
306 ("user.4n6.macb.accessed", "accessed"),
307 ("user.4n6.macb.changed", "changed"),
308 ("user.4n6.macb.born", "born"),
309 ] {
310 let unix = String::from_utf8(unix_xattr_value(&d, xattr).unwrap()).unwrap();
311 assert_eq!(unix, macb_json[field].as_str().unwrap());
312 }
313 }
314
315 #[test]
316 fn from_node_applies_the_macb_mapping() {
317 let node = FsDeletedNode {
318 ino: 42,
319 name: b"notes.txt".to_vec(),
320 parent_ino: Some(5),
321 size: 12,
322 file_type: FsFileType::RegularFile,
323 allocation: FsAllocation::Deleted,
324 record_id: 12345,
325 atime: ts(200),
326 mtime: ts(100),
327 ctime: ts(300),
328 crtime: ts(400),
329 };
330 let mark = Mark::from_node(&node);
331 assert_eq!(mark.allocation, FsAllocation::Deleted);
332 assert_eq!(mark.macb.modified, 100); assert_eq!(mark.macb.accessed, 200); assert_eq!(mark.macb.changed, 300); assert_eq!(mark.macb.born, 400); }
337}