Skip to main content

microsandbox_core/config/
port_pair.rs

1use std::{fmt, str::FromStr};
2
3use serde::{Deserialize, Serialize};
4
5use crate::MicrosandboxError;
6
7//--------------------------------------------------------------------------------------------------
8// Types
9//--------------------------------------------------------------------------------------------------
10
11/// Represents a port mapping between host and guest systems, following Docker's port mapping convention.
12///
13/// ## Format
14/// The port pair can be specified in two formats:
15/// - `host:guest` - Maps the host port to a different guest port (e.g., "8080:80")
16/// - `port` or `port:port` - Maps the same port number on both host and guest (e.g., "8080" or "8080:8080")
17///
18/// ## Examples
19///
20/// Creating port pairs:
21/// ```
22/// use microsandbox_core::config::PortPair;
23///
24/// // Same port on host and guest (8080:8080)
25/// let same_port = PortPair::with_same(8080);
26///
27/// // Different ports (host 8080 maps to guest 80)
28/// let distinct_ports = PortPair::with_distinct(8080, 80);
29///
30/// // Parse from string
31/// let from_str = "8080:80".parse::<PortPair>().unwrap();
32/// assert_eq!(from_str, distinct_ports);
33/// ```
34#[derive(Debug, Clone, PartialEq, Eq)]
35pub enum PortPair {
36    /// The guest port and the host port are distinct.
37    Distinct {
38        /// The host port.
39        host: u16,
40
41        /// The guest port.
42        guest: u16,
43    },
44
45    /// The guest port and the host port are the same.
46    Same(u16),
47}
48
49//--------------------------------------------------------------------------------------------------
50// Methods
51//--------------------------------------------------------------------------------------------------
52
53impl PortPair {
54    /// Creates a new `PortPair` with the same guest and host port.
55    pub fn with_same(port: u16) -> Self {
56        Self::Same(port)
57    }
58
59    /// Creates a new `PortPair` with distinct guest and host ports.
60    pub fn with_distinct(host: u16, guest: u16) -> Self {
61        Self::Distinct { host, guest }
62    }
63
64    /// Returns the host port.
65    pub fn get_host(&self) -> u16 {
66        match self {
67            Self::Distinct { host, .. } | Self::Same(host) => *host,
68        }
69    }
70
71    /// Returns the guest port.
72    pub fn get_guest(&self) -> u16 {
73        match self {
74            Self::Distinct { guest, .. } | Self::Same(guest) => *guest,
75        }
76    }
77}
78
79//--------------------------------------------------------------------------------------------------
80// Trait Implementations
81//--------------------------------------------------------------------------------------------------
82
83impl FromStr for PortPair {
84    type Err = MicrosandboxError;
85
86    fn from_str(s: &str) -> Result<Self, Self::Err> {
87        if s.is_empty() {
88            return Err(MicrosandboxError::InvalidPortPair(s.to_string()));
89        }
90
91        if s.contains(':') {
92            let (host, guest) = s.split_once(':').unwrap();
93            if guest.is_empty() || host.is_empty() {
94                return Err(MicrosandboxError::InvalidPortPair(s.to_string()));
95            }
96
97            if guest == host {
98                return Ok(Self::Same(
99                    host.parse()
100                        .map_err(|_| MicrosandboxError::InvalidPortPair(s.to_string()))?,
101                ));
102            } else {
103                return Ok(Self::Distinct {
104                    host: host
105                        .parse()
106                        .map_err(|_| MicrosandboxError::InvalidPortPair(s.to_string()))?,
107                    guest: guest
108                        .parse()
109                        .map_err(|_| MicrosandboxError::InvalidPortPair(s.to_string()))?,
110                });
111            }
112        }
113
114        Ok(Self::Same(s.parse().map_err(|_| {
115            MicrosandboxError::InvalidPortPair(s.to_string())
116        })?))
117    }
118}
119
120impl fmt::Display for PortPair {
121    /// Formats the port pair following the format "host:guest".
122    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
123        match self {
124            Self::Distinct { host, guest } => {
125                write!(f, "{}:{}", host, guest)
126            }
127            Self::Same(port) => write!(f, "{}:{}", port, port),
128        }
129    }
130}
131
132impl Serialize for PortPair {
133    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
134    where
135        S: serde::Serializer,
136    {
137        serializer.serialize_str(&self.to_string())
138    }
139}
140
141impl<'de> Deserialize<'de> for PortPair {
142    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
143    where
144        D: serde::Deserializer<'de>,
145    {
146        let s = String::deserialize(deserializer)?;
147        Self::from_str(&s).map_err(serde::de::Error::custom)
148    }
149}
150
151//--------------------------------------------------------------------------------------------------
152// Tests
153//--------------------------------------------------------------------------------------------------
154
155#[cfg(test)]
156mod tests {
157    use super::*;
158
159    #[test]
160    fn test_port_pair_from_str() {
161        // Test same ports
162        assert_eq!("8080".parse::<PortPair>().unwrap(), PortPair::Same(8080));
163        assert_eq!(
164            "8080:8080".parse::<PortPair>().unwrap(),
165            PortPair::Same(8080)
166        );
167
168        // Test distinct ports (host:guest format)
169        assert_eq!(
170            "8080:80".parse::<PortPair>().unwrap(),
171            PortPair::Distinct {
172                host: 8080,
173                guest: 80
174            }
175        );
176
177        // Test invalid formats
178        assert!("".parse::<PortPair>().is_err());
179        assert!(":80".parse::<PortPair>().is_err());
180        assert!("80:".parse::<PortPair>().is_err());
181        assert!("invalid".parse::<PortPair>().is_err());
182        assert!("invalid:80".parse::<PortPair>().is_err());
183        assert!("80:invalid".parse::<PortPair>().is_err());
184    }
185
186    #[test]
187    fn test_port_pair_display() {
188        // Test same ports
189        assert_eq!(PortPair::Same(8080).to_string(), "8080:8080");
190
191        // Test distinct ports (host:guest format)
192        assert_eq!(
193            PortPair::Distinct {
194                host: 8080,
195                guest: 80
196            }
197            .to_string(),
198            "8080:80"
199        );
200    }
201
202    #[test]
203    fn test_port_pair_getters() {
204        // Test same ports
205        let same = PortPair::Same(8080);
206        assert_eq!(same.get_host(), 8080);
207        assert_eq!(same.get_guest(), 8080);
208
209        // Test distinct ports
210        let distinct = PortPair::Distinct {
211            host: 8080,
212            guest: 80,
213        };
214        assert_eq!(distinct.get_host(), 8080);
215        assert_eq!(distinct.get_guest(), 80);
216    }
217
218    #[test]
219    fn test_port_pair_constructors() {
220        assert_eq!(PortPair::with_same(8080), PortPair::Same(8080));
221        assert_eq!(
222            PortPair::with_distinct(8080, 80),
223            PortPair::Distinct {
224                host: 8080,
225                guest: 80
226            }
227        );
228    }
229}