forensic_mount/
win_map.rs1use std::time::{Duration, SystemTime, UNIX_EPOCH};
13
14use crate::{FsFileType, FsTimestamp};
15
16pub const FILE_ATTRIBUTE_READONLY: u32 = 0x0000_0001;
18pub const FILE_ATTRIBUTE_DIRECTORY: u32 = 0x0000_0010;
20pub const FILE_ATTRIBUTE_NORMAL: u32 = 0x0000_0080;
22
23pub fn windows_attributes(ft: FsFileType) -> u32 {
30 match ft {
31 FsFileType::Directory => FILE_ATTRIBUTE_DIRECTORY,
32 _ => FILE_ATTRIBUTE_NORMAL,
33 }
34}
35
36pub fn to_system_time(ts: FsTimestamp) -> SystemTime {
41 if ts.seconds <= 0 {
42 return UNIX_EPOCH;
43 }
44 UNIX_EPOCH + Duration::new(ts.seconds as u64, ts.nanoseconds)
45}
46
47pub fn path_components(path: &str) -> Vec<&str> {
50 path.split(['\\', '/']).filter(|c| !c.is_empty()).collect()
51}
52
53pub fn split_path_stream(path: &str) -> (&str, Option<&str>) {
62 let comp_start = path.rfind(['\\', '/']).map_or(0, |i| i + 1);
65 let (head, last) = path.split_at(comp_start);
66 let Some(colon) = last.find(':') else {
67 return (path, None);
68 };
69 let file_len = head.len() + colon;
72 let stream = &last[colon + 1..];
73 let stream = stream.strip_suffix(":$DATA").unwrap_or(stream);
74 let stream = stream.strip_suffix("$DATA").unwrap_or(stream);
75 let stream = if stream.is_empty() {
76 None
77 } else {
78 Some(stream)
79 };
80 (&path[..file_len], stream)
81}
82
83#[cfg(test)]
84mod tests {
85 use super::*;
86
87 #[test]
88 fn directory_maps_to_directory_attribute() {
89 assert_eq!(
90 windows_attributes(FsFileType::Directory),
91 FILE_ATTRIBUTE_DIRECTORY
92 );
93 }
94
95 #[test]
96 fn non_directory_types_map_to_normal() {
97 for ft in [
98 FsFileType::RegularFile,
99 FsFileType::Symlink,
100 FsFileType::CharDevice,
101 FsFileType::BlockDevice,
102 FsFileType::Fifo,
103 FsFileType::Socket,
104 FsFileType::Unknown,
105 ] {
106 assert_eq!(windows_attributes(ft), FILE_ATTRIBUTE_NORMAL);
107 }
108 }
109
110 #[test]
111 fn zero_and_negative_timestamps_map_to_epoch() {
112 assert_eq!(
113 to_system_time(FsTimestamp {
114 seconds: 0,
115 nanoseconds: 0
116 }),
117 UNIX_EPOCH
118 );
119 assert_eq!(
120 to_system_time(FsTimestamp {
121 seconds: -5,
122 nanoseconds: 123
123 }),
124 UNIX_EPOCH
125 );
126 }
127
128 #[test]
129 fn positive_timestamp_offsets_from_epoch() {
130 assert_eq!(
131 to_system_time(FsTimestamp {
132 seconds: 1,
133 nanoseconds: 500_000_000
134 }),
135 UNIX_EPOCH + Duration::new(1, 500_000_000)
136 );
137 }
138
139 #[test]
140 fn path_components_splits_on_both_separators_and_drops_empties() {
141 assert_eq!(path_components(r"\dir\file.txt"), vec!["dir", "file.txt"]);
142 assert_eq!(path_components("/a//b/"), vec!["a", "b"]);
143 }
144
145 #[test]
146 fn root_and_empty_paths_have_no_components() {
147 assert!(path_components("").is_empty());
148 assert!(path_components(r"\").is_empty());
149 }
150
151 #[test]
152 fn split_path_stream_plain_path_has_no_stream() {
153 assert_eq!(
154 split_path_stream(r"\dir\file.txt"),
155 (r"\dir\file.txt", None)
156 );
157 assert_eq!(split_path_stream(r"\"), (r"\", None));
158 assert_eq!(split_path_stream(""), ("", None));
159 }
160
161 #[test]
162 fn split_path_stream_extracts_named_ads() {
163 assert_eq!(
164 split_path_stream(r"\dir\file.txt:4n6.status"),
165 (r"\dir\file.txt", Some("4n6.status"))
166 );
167 assert_eq!(
168 split_path_stream(r"\file:4n6.macb"),
169 (r"\file", Some("4n6.macb"))
170 );
171 }
172
173 #[test]
174 fn split_path_stream_strips_data_type_suffix() {
175 assert_eq!(
176 split_path_stream(r"\file:4n6.status:$DATA"),
177 (r"\file", Some("4n6.status"))
178 );
179 }
180
181 #[test]
182 fn split_path_stream_unnamed_main_stream_is_none() {
183 assert_eq!(split_path_stream(r"\file::$DATA"), (r"\file", None));
186 }
187
188 #[test]
189 fn split_path_stream_only_splits_the_final_component() {
190 assert_eq!(
193 split_path_stream(r"\a\b\c:4n6.status"),
194 (r"\a\b\c", Some("4n6.status"))
195 );
196 }
197}