Skip to main content

shell_tunnel/session/
id.rs

1//! Session identifier type.
2
3use std::fmt;
4use std::str::FromStr;
5use std::sync::atomic::{AtomicU64, Ordering};
6
7/// Global counter for session ID generation.
8static COUNTER: AtomicU64 = AtomicU64::new(1);
9
10/// Unique identifier for a shell session.
11///
12/// Session IDs are generated using an atomic counter, ensuring uniqueness
13/// within a single process lifetime. The ID is displayed as `sess-XXXXXXXX`
14/// where X is a hexadecimal digit.
15#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
16pub struct SessionId(u64);
17
18impl SessionId {
19    /// Create a new unique session ID.
20    pub fn new() -> Self {
21        Self(COUNTER.fetch_add(1, Ordering::Relaxed))
22    }
23
24    /// Get the raw u64 value.
25    pub fn as_u64(&self) -> u64 {
26        self.0
27    }
28
29    /// Create a SessionId from a raw u64 value.
30    ///
31    /// This is primarily for testing and deserialization.
32    pub fn from_raw(value: u64) -> Self {
33        Self(value)
34    }
35}
36
37impl Default for SessionId {
38    fn default() -> Self {
39        Self::new()
40    }
41}
42
43impl fmt::Display for SessionId {
44    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
45        write!(f, "sess-{:08x}", self.0)
46    }
47}
48
49impl FromStr for SessionId {
50    type Err = crate::error::ShellTunnelError;
51
52    fn from_str(s: &str) -> Result<Self, Self::Err> {
53        s.strip_prefix("sess-")
54            .and_then(|hex| u64::from_str_radix(hex, 16).ok())
55            .map(SessionId)
56            .ok_or_else(|| crate::error::ShellTunnelError::SessionNotFound(s.into()))
57    }
58}
59
60#[cfg(test)]
61mod tests {
62    use super::*;
63    use std::collections::HashSet;
64
65    #[test]
66    fn test_uniqueness() {
67        let mut ids = HashSet::new();
68        for _ in 0..10_000 {
69            let id = SessionId::new();
70            assert!(ids.insert(id), "Duplicate ID generated: {}", id);
71        }
72        assert_eq!(ids.len(), 10_000);
73    }
74
75    #[test]
76    fn test_display_format() {
77        let id = SessionId::from_raw(255);
78        assert_eq!(id.to_string(), "sess-000000ff");
79
80        let id2 = SessionId::from_raw(0x12345678);
81        assert_eq!(id2.to_string(), "sess-12345678");
82    }
83
84    #[test]
85    fn test_parse_valid() {
86        let id: SessionId = "sess-000000ff".parse().unwrap();
87        assert_eq!(id.as_u64(), 255);
88
89        let id2: SessionId = "sess-12345678".parse().unwrap();
90        assert_eq!(id2.as_u64(), 0x12345678);
91    }
92
93    #[test]
94    fn test_parse_invalid() {
95        // Missing prefix
96        assert!("000000ff".parse::<SessionId>().is_err());
97
98        // Wrong prefix
99        assert!("session-000000ff".parse::<SessionId>().is_err());
100
101        // Invalid hex
102        assert!("sess-gggggggg".parse::<SessionId>().is_err());
103
104        // Empty
105        assert!("".parse::<SessionId>().is_err());
106    }
107
108    #[test]
109    fn test_roundtrip() {
110        let original = SessionId::new();
111        let s = original.to_string();
112        let parsed: SessionId = s.parse().unwrap();
113        assert_eq!(original, parsed);
114    }
115
116    #[test]
117    fn test_hash_eq() {
118        let id1 = SessionId::from_raw(42);
119        let id2 = SessionId::from_raw(42);
120        let id3 = SessionId::from_raw(43);
121
122        assert_eq!(id1, id2);
123        assert_ne!(id1, id3);
124
125        let mut set = HashSet::new();
126        set.insert(id1);
127        assert!(set.contains(&id2));
128        assert!(!set.contains(&id3));
129    }
130}