microsandbox_core/config/
path_pair.rs

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