use std::time::{Duration, SystemTime, UNIX_EPOCH};
use crate::{FsFileType, FsTimestamp};
pub const FILE_ATTRIBUTE_READONLY: u32 = 0x0000_0001;
pub const FILE_ATTRIBUTE_DIRECTORY: u32 = 0x0000_0010;
pub const FILE_ATTRIBUTE_NORMAL: u32 = 0x0000_0080;
pub fn windows_attributes(ft: FsFileType) -> u32 {
match ft {
FsFileType::Directory => FILE_ATTRIBUTE_DIRECTORY,
_ => FILE_ATTRIBUTE_NORMAL,
}
}
pub fn to_system_time(ts: FsTimestamp) -> SystemTime {
if ts.seconds <= 0 {
return UNIX_EPOCH;
}
UNIX_EPOCH + Duration::new(ts.seconds as u64, ts.nanoseconds)
}
pub fn path_components(path: &str) -> Vec<&str> {
path.split(['\\', '/']).filter(|c| !c.is_empty()).collect()
}
pub fn split_path_stream(path: &str) -> (&str, Option<&str>) {
let comp_start = path.rfind(['\\', '/']).map_or(0, |i| i + 1);
let (head, last) = path.split_at(comp_start);
let Some(colon) = last.find(':') else {
return (path, None);
};
let file_len = head.len() + colon;
let stream = &last[colon + 1..];
let stream = stream.strip_suffix(":$DATA").unwrap_or(stream);
let stream = stream.strip_suffix("$DATA").unwrap_or(stream);
let stream = if stream.is_empty() {
None
} else {
Some(stream)
};
(&path[..file_len], stream)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn directory_maps_to_directory_attribute() {
assert_eq!(
windows_attributes(FsFileType::Directory),
FILE_ATTRIBUTE_DIRECTORY
);
}
#[test]
fn non_directory_types_map_to_normal() {
for ft in [
FsFileType::RegularFile,
FsFileType::Symlink,
FsFileType::CharDevice,
FsFileType::BlockDevice,
FsFileType::Fifo,
FsFileType::Socket,
FsFileType::Unknown,
] {
assert_eq!(windows_attributes(ft), FILE_ATTRIBUTE_NORMAL);
}
}
#[test]
fn zero_and_negative_timestamps_map_to_epoch() {
assert_eq!(
to_system_time(FsTimestamp {
seconds: 0,
nanoseconds: 0
}),
UNIX_EPOCH
);
assert_eq!(
to_system_time(FsTimestamp {
seconds: -5,
nanoseconds: 123
}),
UNIX_EPOCH
);
}
#[test]
fn positive_timestamp_offsets_from_epoch() {
assert_eq!(
to_system_time(FsTimestamp {
seconds: 1,
nanoseconds: 500_000_000
}),
UNIX_EPOCH + Duration::new(1, 500_000_000)
);
}
#[test]
fn path_components_splits_on_both_separators_and_drops_empties() {
assert_eq!(path_components(r"\dir\file.txt"), vec!["dir", "file.txt"]);
assert_eq!(path_components("/a//b/"), vec!["a", "b"]);
}
#[test]
fn root_and_empty_paths_have_no_components() {
assert!(path_components("").is_empty());
assert!(path_components(r"\").is_empty());
}
#[test]
fn split_path_stream_plain_path_has_no_stream() {
assert_eq!(
split_path_stream(r"\dir\file.txt"),
(r"\dir\file.txt", None)
);
assert_eq!(split_path_stream(r"\"), (r"\", None));
assert_eq!(split_path_stream(""), ("", None));
}
#[test]
fn split_path_stream_extracts_named_ads() {
assert_eq!(
split_path_stream(r"\dir\file.txt:4n6.status"),
(r"\dir\file.txt", Some("4n6.status"))
);
assert_eq!(
split_path_stream(r"\file:4n6.macb"),
(r"\file", Some("4n6.macb"))
);
}
#[test]
fn split_path_stream_strips_data_type_suffix() {
assert_eq!(
split_path_stream(r"\file:4n6.status:$DATA"),
(r"\file", Some("4n6.status"))
);
}
#[test]
fn split_path_stream_unnamed_main_stream_is_none() {
assert_eq!(split_path_stream(r"\file::$DATA"), (r"\file", None));
}
#[test]
fn split_path_stream_only_splits_the_final_component() {
assert_eq!(
split_path_stream(r"\a\b\c:4n6.status"),
(r"\a\b\c", Some("4n6.status"))
);
}
}