Skip to main content

substrate/
util.rs

1//! Local projection helpers and timestamp utilities.
2
3use anyhow::{anyhow, Result};
4
5// -- Local directory naming --
6
7fn 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
27/// Pick a local directory name from opaque CMN metadata.
28///
29/// Attempts `id`, then `name`, and finally falls back to `hash`.
30pub 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// -- Version comparison --
41
42/// Result of comparing two timestamps for version ordering.
43#[derive(Debug, Clone, Copy, PartialEq, Eq)]
44pub enum VersionOrder {
45    /// The incoming timestamp is strictly newer.
46    Newer,
47    /// Both timestamps are identical.
48    Same,
49    /// The incoming timestamp is older.
50    Older,
51}
52
53/// Compare an incoming timestamp against an existing one for version ordering.
54///
55/// CMN uses strictly-newer semantics: content is only accepted when `incoming > existing`.
56pub 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
64// -- Timestamp validation --
65
66pub 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    // -- local_dir_name tests --
89
90    #[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    // -- timestamp tests --
146
147    #[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}