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