Skip to main content

forensic_mount/
win_map.rs

1//! Pure mapping helpers from the platform-agnostic `ForensicFs` model onto
2//! Windows filesystem semantics (path components, file attributes, timestamps).
3//!
4//! This module is **cross-platform on purpose**: the Windows mount backend
5//! (`fuse_windows`, `#[cfg(windows)]`) is a thin Humble-Object shell over
6//! Dokan's FFI callbacks, so every testable decision lives here where the
7//! Linux/macOS `cargo test` job can exercise it. The attribute constants are
8//! the documented Windows ABI values ([MS-FSCC] §2.6 / `WinNT.h`), so they are
9//! defined once here and reused by the shell rather than pulled from a
10//! Windows-only binding crate.
11
12use std::time::{Duration, SystemTime, UNIX_EPOCH};
13
14use crate::types::{FsFileType, FsTimestamp};
15
16/// `FILE_ATTRIBUTE_READONLY` — the file is read-only.
17pub const FILE_ATTRIBUTE_READONLY: u32 = 0x0000_0001;
18/// `FILE_ATTRIBUTE_DIRECTORY` — the handle identifies a directory.
19pub const FILE_ATTRIBUTE_DIRECTORY: u32 = 0x0000_0010;
20/// `FILE_ATTRIBUTE_NORMAL` — no other attributes are set (valid only alone).
21pub const FILE_ATTRIBUTE_NORMAL: u32 = 0x0000_0080;
22
23/// Windows file-attribute bits for a `ForensicFs` file type.
24///
25/// Directories carry `FILE_ATTRIBUTE_DIRECTORY`; everything else is reported as
26/// a normal file. The mount is presented read-only at the volume level
27/// (`MountFlags::WRITE_PROTECT`), so a per-file read-only bit is redundant and
28/// is not set here.
29pub fn windows_attributes(ft: FsFileType) -> u32 {
30    match ft {
31        FsFileType::Directory => FILE_ATTRIBUTE_DIRECTORY,
32        _ => FILE_ATTRIBUTE_NORMAL,
33    }
34}
35
36/// Convert a Unix `FsTimestamp` to a `SystemTime` (Dokan converts it to a
37/// Windows `FILETIME` internally).
38///
39/// Non-positive seconds (missing/zero timestamps) map to the Unix epoch.
40pub 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
47/// Split a Windows path (`\dir\file`, also tolerating `/`) into its non-empty
48/// components, for walking `ForensicFs::lookup` from the root.
49pub fn path_components(path: &str) -> Vec<&str> {
50    path.split(['\\', '/']).filter(|c| !c.is_empty()).collect()
51}
52
53#[cfg(test)]
54mod tests {
55    use super::*;
56
57    #[test]
58    fn directory_maps_to_directory_attribute() {
59        assert_eq!(
60            windows_attributes(FsFileType::Directory),
61            FILE_ATTRIBUTE_DIRECTORY
62        );
63    }
64
65    #[test]
66    fn non_directory_types_map_to_normal() {
67        for ft in [
68            FsFileType::RegularFile,
69            FsFileType::Symlink,
70            FsFileType::CharDevice,
71            FsFileType::BlockDevice,
72            FsFileType::Fifo,
73            FsFileType::Socket,
74            FsFileType::Unknown,
75        ] {
76            assert_eq!(windows_attributes(ft), FILE_ATTRIBUTE_NORMAL);
77        }
78    }
79
80    #[test]
81    fn zero_and_negative_timestamps_map_to_epoch() {
82        assert_eq!(
83            to_system_time(FsTimestamp {
84                seconds: 0,
85                nanoseconds: 0
86            }),
87            UNIX_EPOCH
88        );
89        assert_eq!(
90            to_system_time(FsTimestamp {
91                seconds: -5,
92                nanoseconds: 123
93            }),
94            UNIX_EPOCH
95        );
96    }
97
98    #[test]
99    fn positive_timestamp_offsets_from_epoch() {
100        assert_eq!(
101            to_system_time(FsTimestamp {
102                seconds: 1,
103                nanoseconds: 500_000_000
104            }),
105            UNIX_EPOCH + Duration::new(1, 500_000_000)
106        );
107    }
108
109    #[test]
110    fn path_components_splits_on_both_separators_and_drops_empties() {
111        assert_eq!(path_components(r"\dir\file.txt"), vec!["dir", "file.txt"]);
112        assert_eq!(path_components("/a//b/"), vec!["a", "b"]);
113    }
114
115    #[test]
116    fn root_and_empty_paths_have_no_components() {
117        assert!(path_components("").is_empty());
118        assert!(path_components(r"\").is_empty());
119    }
120}