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
40pub fn validate_timestamp_not_future(
43 epoch_ms: u64,
44 now_epoch_ms: u64,
45 max_skew_ms: u64,
46) -> Result<()> {
47 if epoch_ms > now_epoch_ms + max_skew_ms {
48 return Err(anyhow!(
49 "Timestamp {} is {}ms in the future (tolerance: {}ms)",
50 epoch_ms,
51 epoch_ms - now_epoch_ms,
52 max_skew_ms
53 ));
54 }
55 Ok(())
56}
57
58#[cfg(test)]
59mod tests {
60 #![allow(clippy::expect_used, clippy::unwrap_used)]
61
62 use super::*;
63
64 #[test]
67 fn test_derive_local_path_segment_valid() {
68 assert_eq!(
69 derive_local_path_segment("strain-account"),
70 Some("strain-account".to_string())
71 );
72 assert_eq!(derive_local_path_segment("a.b"), Some("a.b".to_string()));
73 assert_eq!(
74 derive_local_path_segment("CMN Protocol Specification"),
75 Some("CMN-Protocol-Specification".to_string())
76 );
77 assert_eq!(
78 derive_local_path_segment("CMN协议规范"),
79 Some("CMN协议规范".to_string())
80 );
81 }
82
83 #[test]
84 fn test_derive_local_path_segment_invalid_or_fallback_cases() {
85 assert_eq!(derive_local_path_segment(""), None);
86 assert_eq!(
87 derive_local_path_segment(".hidden"),
88 Some("hidden".to_string())
89 );
90 assert_eq!(
91 derive_local_path_segment("bad/id"),
92 Some("bad-id".to_string())
93 );
94 assert_eq!(
95 derive_local_path_segment("bad id"),
96 Some("bad-id".to_string())
97 );
98 assert_eq!(derive_local_path_segment(".."), None);
99 assert_eq!(derive_local_path_segment("---"), None);
100 assert_eq!(derive_local_path_segment("\x01\x02"), None);
101 }
102
103 #[test]
104 fn test_local_dir_name() {
105 assert_eq!(
106 local_dir_name(Some("strain-account"), Some("Friendly Name"), "b3.hash"),
107 "strain-account"
108 );
109 assert_eq!(
110 local_dir_name(Some("../etc"), Some("Friendly Name"), "b3.hash"),
111 "-etc"
112 );
113 assert_eq!(
114 local_dir_name(Some(""), Some("Friendly Name"), "b3.hash"),
115 "Friendly-Name"
116 );
117 assert_eq!(local_dir_name(None, Some(""), "b3.hash"), "b3.hash");
118 assert_eq!(local_dir_name(None, None, "b3.hash"), "b3.hash");
119 }
120
121 #[test]
124 fn test_validate_timestamp_not_future_allows_within_skew() {
125 assert!(validate_timestamp_not_future(105, 100, 10).is_ok());
126 }
127
128 #[test]
129 fn test_validate_timestamp_not_future_rejects_far_future() {
130 assert!(validate_timestamp_not_future(111, 100, 10).is_err());
131 }
132}