microsandbox_core/config/
reference_path.rs

1use std::{
2    fmt::{self, Display},
3    path::PathBuf,
4    str::FromStr,
5};
6
7use crate::{oci::Reference, MicrosandboxError};
8
9//--------------------------------------------------------------------------------------------------
10// Types
11//--------------------------------------------------------------------------------------------------
12
13/// Represents either an OCI image reference or a path to a rootfs on disk.
14///
15/// This type is used to specify the source of a container's root filesystem:
16/// - For OCI images (e.g., "docker.io/library/ubuntu:latest"), use `Reference` variant
17/// - For local rootfs directories (e.g., "/path/to/rootfs" or "./rootfs"), use `Path` variant
18#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
19#[serde(try_from = "String")]
20#[serde(into = "String")]
21pub enum ReferenceOrPath {
22    /// An OCI-compliant image reference (e.g., "docker.io/library/ubuntu:latest").
23    /// This is used when the rootfs should be pulled from a container registry.
24    Reference(Reference),
25
26    /// A path to a rootfs directory on the local filesystem.
27    /// This can be either absolute (e.g., "/path/to/rootfs") or relative (e.g., "./rootfs").
28    Path(PathBuf),
29}
30
31//--------------------------------------------------------------------------------------------------
32// Trait Implementations
33//--------------------------------------------------------------------------------------------------
34
35impl FromStr for ReferenceOrPath {
36    type Err = MicrosandboxError;
37
38    /// Parses a string into a ReferenceOrPath.
39    ///
40    /// The parsing rules are:
41    /// - If the string starts with "." or "/", it is interpreted as a path to a local rootfs
42    /// - Otherwise, it is interpreted as an OCI image reference
43    ///
44    /// # Examples
45    ///
46    /// ```
47    /// # use std::str::FromStr;
48    /// # use microsandbox_core::config::ReferenceOrPath;
49    /// // Parse as local rootfs path
50    /// let local = ReferenceOrPath::from_str("./my-rootfs").unwrap();
51    /// let absolute = ReferenceOrPath::from_str("/var/lib/my-rootfs").unwrap();
52    ///
53    /// // Parse as OCI image reference
54    /// let image = ReferenceOrPath::from_str("ubuntu:latest").unwrap();
55    /// let full = ReferenceOrPath::from_str("docker.io/library/debian:11").unwrap();
56    /// ```
57    fn from_str(s: &str) -> Result<Self, Self::Err> {
58        // Check if the string starts with "." or "/" to determine if it's a path
59        if s.starts_with('.') || s.starts_with('/') {
60            Ok(ReferenceOrPath::Path(PathBuf::from(s)))
61        } else {
62            // Parse as an image reference
63            let reference = Reference::from_str(s)?;
64            Ok(ReferenceOrPath::Reference(reference))
65        }
66    }
67}
68
69impl Display for ReferenceOrPath {
70    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
71        match self {
72            ReferenceOrPath::Path(path) => write!(f, "{}", path.display()),
73            ReferenceOrPath::Reference(reference) => write!(f, "{}", reference),
74        }
75    }
76}
77
78impl TryFrom<String> for ReferenceOrPath {
79    type Error = MicrosandboxError;
80
81    fn try_from(s: String) -> Result<Self, Self::Error> {
82        s.parse()
83    }
84}
85
86impl Into<String> for ReferenceOrPath {
87    fn into(self) -> String {
88        self.to_string()
89    }
90}
91
92//--------------------------------------------------------------------------------------------------
93// Tests
94//--------------------------------------------------------------------------------------------------
95
96#[cfg(test)]
97mod tests {
98    use super::*;
99
100    #[test]
101    fn test_path_relative() {
102        // Test relative paths with different formats
103        let cases = vec![
104            "./path/to/file",
105            "./single",
106            ".",
107            "./path/with/multiple/segments",
108            "./path.with.dots",
109            "./path-with-dashes",
110            "./path_with_underscores",
111        ];
112
113        for case in cases {
114            let reference = ReferenceOrPath::from_str(case).unwrap();
115            match &reference {
116                ReferenceOrPath::Path(path) => {
117                    assert_eq!(path, &PathBuf::from(case));
118                    assert_eq!(reference.to_string(), case);
119                }
120                _ => panic!("Expected Path variant for {}", case),
121            }
122        }
123    }
124
125    #[test]
126    fn test_path_absolute() {
127        // Test absolute paths with different formats
128        let cases = vec![
129            "/absolute/path",
130            "/root",
131            "/path/with/multiple/segments",
132            "/path.with.dots",
133            "/path-with-dashes",
134            "/path_with_underscores",
135        ];
136
137        for case in cases {
138            let reference = ReferenceOrPath::from_str(case).unwrap();
139            match &reference {
140                ReferenceOrPath::Path(path) => {
141                    assert_eq!(path, &PathBuf::from(case));
142                    assert_eq!(reference.to_string(), case);
143                }
144                _ => panic!("Expected Path variant for {}", case),
145            }
146        }
147    }
148
149    #[test]
150    fn test_image_reference_simple() {
151        // Test simple image references
152        let cases = vec![
153            "alpine:latest",
154            "ubuntu:20.04",
155            "nginx:1.19",
156            "redis:6",
157            "postgres:13-alpine",
158        ];
159
160        for case in cases {
161            let reference = ReferenceOrPath::from_str(case).unwrap();
162            match &reference {
163                ReferenceOrPath::Reference(ref_) => {
164                    assert_eq!(reference.to_string(), ref_.to_string());
165                }
166                _ => panic!("Expected Reference variant for {}", case),
167            }
168        }
169    }
170
171    #[test]
172    fn test_image_reference_with_registry() {
173        // Test image references with registry
174        let cases = vec![
175            "docker.io/library/alpine:latest",
176            "registry.example.com/myapp:v1.0",
177            "ghcr.io/owner/repo:tag",
178            "k8s.gcr.io/pause:3.2",
179            "quay.io/organization/image:1.0",
180        ];
181
182        for case in cases {
183            let reference = ReferenceOrPath::from_str(case).unwrap();
184            match &reference {
185                ReferenceOrPath::Reference(ref_) => {
186                    assert_eq!(reference.to_string(), ref_.to_string());
187                }
188                _ => panic!("Expected Reference variant for {}", case),
189            }
190        }
191    }
192
193    #[test]
194    fn test_image_reference_with_digest() {
195        let valid_digest = "deadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef";
196        let cases = vec![
197            format!("alpine@sha256:{}", valid_digest),
198            format!("docker.io/library/ubuntu@sha256:{}", valid_digest),
199            format!("registry.example.com/myapp:v1.0@sha256:{}", valid_digest),
200        ];
201
202        for case in cases {
203            let reference = ReferenceOrPath::from_str(&case).unwrap();
204            match &reference {
205                ReferenceOrPath::Reference(ref_) => {
206                    assert_eq!(reference.to_string(), ref_.to_string());
207                }
208                _ => panic!("Expected Reference variant for {}", case),
209            }
210        }
211    }
212
213    #[test]
214    fn test_image_reference_with_port() {
215        // Test image references with registry port
216        let cases = vec![
217            "localhost:5000/myapp:latest",
218            "registry.example.com:5000/app:v1",
219            "192.168.1.1:5000/image:tag",
220        ];
221
222        for case in cases {
223            let reference = ReferenceOrPath::from_str(case).unwrap();
224            match &reference {
225                ReferenceOrPath::Reference(ref_) => {
226                    assert_eq!(reference.to_string(), ref_.to_string());
227                }
228                _ => panic!("Expected Reference variant for {}", case),
229            }
230        }
231    }
232
233    #[test]
234    fn test_empty_input() {
235        // Test empty input
236        assert!(ReferenceOrPath::from_str("").is_err());
237    }
238
239    #[test]
240    fn test_display_formatting() {
241        // Test display formatting for both variants
242        let test_cases = vec![
243            ("./local/path", "./local/path"),
244            ("/absolute/path", "/absolute/path"),
245            ("alpine:latest", "sandboxes.io/library/alpine:latest"),
246            (
247                "registry.example.com/app:v1.0",
248                "registry.example.com/library/app:v1.0",
249            ),
250        ];
251
252        for (input, expected) in test_cases {
253            let reference = ReferenceOrPath::from_str(input).unwrap();
254            assert_eq!(reference.to_string(), expected);
255        }
256    }
257
258    #[test]
259    fn test_serde_path_roundtrip() {
260        let test_cases = vec![
261            ReferenceOrPath::Path(PathBuf::from("./local/rootfs")),
262            ReferenceOrPath::Path(PathBuf::from("/absolute/path/to/rootfs")),
263            ReferenceOrPath::Path(PathBuf::from(".")),
264            ReferenceOrPath::Path(PathBuf::from("/root")),
265        ];
266
267        for case in test_cases {
268            let serialized = serde_yaml::to_string(&case).unwrap();
269            let deserialized: ReferenceOrPath = serde_yaml::from_str(&serialized).unwrap();
270            assert_eq!(case, deserialized);
271        }
272    }
273
274    #[test]
275    fn test_serde_reference_roundtrip() {
276        let test_cases = vec![
277            "alpine:latest",
278            "docker.io/library/ubuntu:20.04",
279            "registry.example.com:5000/myapp:v1.0",
280            "ghcr.io/owner/repo:tag",
281        ];
282
283        for case in test_cases {
284            let reference = ReferenceOrPath::from_str(case).unwrap();
285            let serialized = serde_yaml::to_string(&reference).unwrap();
286            let deserialized: ReferenceOrPath = serde_yaml::from_str(&serialized).unwrap();
287            assert_eq!(reference, deserialized);
288        }
289    }
290
291    #[test]
292    fn test_serde_yaml_format() {
293        // Test Path variant serialization format
294        let path = ReferenceOrPath::Path(PathBuf::from("/test/rootfs"));
295        let serialized = serde_yaml::to_string(&path).unwrap();
296        assert_eq!(serialized.trim(), "/test/rootfs");
297
298        // Test Reference variant serialization format
299        let reference = ReferenceOrPath::from_str("ubuntu:latest").unwrap();
300        let serialized = serde_yaml::to_string(&reference).unwrap();
301        assert!(serialized.trim().contains("ubuntu:latest"));
302    }
303
304    #[test]
305    fn test_serde_invalid_input() {
306        // Test deserializing invalid YAML
307        let invalid_yaml = "- not a valid reference path";
308        assert!(serde_yaml::from_str::<ReferenceOrPath>(invalid_yaml).is_err());
309
310        // Test deserializing invalid reference format
311        let invalid_reference = "invalid!reference:format";
312        assert!(serde_yaml::from_str::<ReferenceOrPath>(invalid_reference).is_err());
313    }
314}