1use anyhow::{anyhow, Result};
4
5fn derive_local_path_segment(value: &str) -> Option<String> {
8 let sanitized: String = value
9 .chars()
10 .map(|ch| {
11 if "/\\:*?\"<>|".contains(ch) || ch.is_whitespace() || ch.is_control() {
12 '-'
13 } else {
14 ch
15 }
16 })
17 .collect();
18
19 let segment = sanitized.trim_matches('-').trim_start_matches('.');
20 if segment.is_empty() || segment == "." || segment == ".." {
21 None
22 } else {
23 Some(segment.to_string())
24 }
25}
26
27pub fn local_dir_name(id: Option<&str>, name: Option<&str>, hash: &str) -> String {
31 id.filter(|value| !value.is_empty())
32 .and_then(derive_local_path_segment)
33 .or_else(|| {
34 name.filter(|value| !value.is_empty())
35 .and_then(derive_local_path_segment)
36 })
37 .unwrap_or_else(|| hash.to_string())
38}
39
40#[derive(Debug, Clone, Copy, PartialEq, Eq)]
44pub enum VersionOrder {
45 Newer,
47 Same,
49 Older,
51}
52
53pub fn compare_version_timestamps(incoming_epoch_ms: u64, existing_epoch_ms: u64) -> VersionOrder {
57 match incoming_epoch_ms.cmp(&existing_epoch_ms) {
58 std::cmp::Ordering::Greater => VersionOrder::Newer,
59 std::cmp::Ordering::Equal => VersionOrder::Same,
60 std::cmp::Ordering::Less => VersionOrder::Older,
61 }
62}
63
64pub fn validate_timestamp_not_future(
67 epoch_ms: u64,
68 now_epoch_ms: u64,
69 max_skew_ms: u64,
70) -> Result<()> {
71 if epoch_ms > now_epoch_ms + max_skew_ms {
72 return Err(anyhow!(
73 "Timestamp {} is {}ms in the future (tolerance: {}ms)",
74 epoch_ms,
75 epoch_ms - now_epoch_ms,
76 max_skew_ms
77 ));
78 }
79 Ok(())
80}
81
82#[cfg(test)]
83mod tests {
84 #![allow(clippy::expect_used, clippy::unwrap_used)]
85
86 use super::*;
87
88 #[test]
91 fn test_derive_local_path_segment_valid() {
92 assert_eq!(
93 derive_local_path_segment("strain-account"),
94 Some("strain-account".to_string())
95 );
96 assert_eq!(derive_local_path_segment("a.b"), Some("a.b".to_string()));
97 assert_eq!(
98 derive_local_path_segment("CMN Protocol Specification"),
99 Some("CMN-Protocol-Specification".to_string())
100 );
101 assert_eq!(
102 derive_local_path_segment("CMN协议规范"),
103 Some("CMN协议规范".to_string())
104 );
105 }
106
107 #[test]
108 fn test_derive_local_path_segment_invalid_or_fallback_cases() {
109 assert_eq!(derive_local_path_segment(""), None);
110 assert_eq!(
111 derive_local_path_segment(".hidden"),
112 Some("hidden".to_string())
113 );
114 assert_eq!(
115 derive_local_path_segment("bad/id"),
116 Some("bad-id".to_string())
117 );
118 assert_eq!(
119 derive_local_path_segment("bad id"),
120 Some("bad-id".to_string())
121 );
122 assert_eq!(derive_local_path_segment(".."), None);
123 assert_eq!(derive_local_path_segment("---"), None);
124 assert_eq!(derive_local_path_segment("\x01\x02"), None);
125 }
126
127 #[test]
128 fn test_local_dir_name() {
129 assert_eq!(
130 local_dir_name(Some("strain-account"), Some("Friendly Name"), "b3.hash"),
131 "strain-account"
132 );
133 assert_eq!(
134 local_dir_name(Some("../etc"), Some("Friendly Name"), "b3.hash"),
135 "-etc"
136 );
137 assert_eq!(
138 local_dir_name(Some(""), Some("Friendly Name"), "b3.hash"),
139 "Friendly-Name"
140 );
141 assert_eq!(local_dir_name(None, Some(""), "b3.hash"), "b3.hash");
142 assert_eq!(local_dir_name(None, None, "b3.hash"), "b3.hash");
143 }
144
145 #[test]
148 fn test_validate_timestamp_not_future_allows_within_skew() {
149 assert!(validate_timestamp_not_future(105, 100, 10).is_ok());
150 }
151
152 #[test]
153 fn test_validate_timestamp_not_future_rejects_far_future() {
154 assert!(validate_timestamp_not_future(111, 100, 10).is_err());
155 }
156}