Skip to content

Commit bf4dac9

Browse files
Introduce a Transport enum for container images (#120)
Transport enum for different container image backends (registry, oci, etc.) TryFrom<&str> implementation for parsing transport prefixes from image references Display impl for Transport Tests generated by Claude Code Assisted-by: Claude Code (Sonnet 4) Signed-off-by: Pragyan Poudyal <pragyanpoudyal41999@gmail.com>
1 parent debb6fc commit bf4dac9

2 files changed

Lines changed: 266 additions & 0 deletions

File tree

src/imageproxy.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@
44
//!
55
//! More information: <https://github.com/containers/skopeo/pull/1476>
66
7+
pub mod transport;
8+
79
use cap_std_ext::prelude::CapStdExtCommandExt;
810
use cap_std_ext::{cap_std, cap_tempfile};
911
use futures_util::{Future, FutureExt};

src/transport.rs

Lines changed: 264 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,264 @@
1+
use std::fmt;
2+
use thiserror::Error;
3+
4+
#[derive(Debug, Error)]
5+
pub enum TransportConversionError {
6+
#[error("Invalid transport")]
7+
InvalidTransport(Box<str>),
8+
#[error("Missing ':' in imgref")]
9+
MissingColon,
10+
}
11+
12+
/// A backend/transport for OCI/Docker images.
13+
#[derive(Debug, PartialEq, Eq, Clone, Copy)]
14+
pub enum Transport {
15+
/// A remote Docker/OCI registry (`registry:` or `docker://`)
16+
Registry,
17+
/// A local OCI directory (`oci:`)
18+
OciDir,
19+
/// A local OCI archive tarball (`oci-archive:`)
20+
OciArchive,
21+
/// A local Docker archive tarball (`docker-archive:`)
22+
DockerArchive,
23+
/// Local container storage (`containers-storage:`)
24+
ContainerStorage,
25+
/// Local directory (`dir:`)
26+
Dir,
27+
/// Local Docker daemon (`docker-daemon:`)
28+
DockerDaemon,
29+
}
30+
31+
impl fmt::Display for Transport {
32+
/// Convert the transport back to its string representation.
33+
///
34+
/// Note: Registry transport defaults to "docker://" format.
35+
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
36+
match self {
37+
Transport::Registry => f.write_str("docker://"),
38+
Transport::OciDir => f.write_str("oci:"),
39+
Transport::OciArchive => f.write_str("oci-archive:"),
40+
Transport::DockerArchive => f.write_str("docker-archive:"),
41+
Transport::ContainerStorage => f.write_str("containers-storage:"),
42+
Transport::Dir => f.write_str("dir:"),
43+
Transport::DockerDaemon => f.write_str("docker-daemon:"),
44+
}
45+
}
46+
}
47+
48+
impl TryFrom<&str> for Transport {
49+
type Error = TransportConversionError;
50+
51+
/// Parse the transport type from a container image reference string, eg
52+
/// docker://quay.io/myimage, containers-storage:localhost/myimage
53+
///
54+
/// Supports various transport types like "registry:", "oci:", "docker://", etc.
55+
/// Returns an error for unknown transports or malformed references without colons.
56+
fn try_from(imgref: &str) -> Result<Self, TransportConversionError> {
57+
if let Some(colon_pos) = imgref.find(':') {
58+
let transport_prefix = &imgref[..colon_pos];
59+
60+
let transport = match transport_prefix {
61+
"registry" => Transport::Registry,
62+
"oci" => Transport::OciDir,
63+
"oci-archive" => Transport::OciArchive,
64+
"docker-archive" => Transport::DockerArchive,
65+
"containers-storage" => Transport::ContainerStorage,
66+
"dir" => Transport::Dir,
67+
"docker-daemon" => Transport::DockerDaemon,
68+
"docker" => {
69+
// Check if this is actually "docker://" format
70+
if imgref[colon_pos..].starts_with("://") {
71+
Transport::Registry
72+
} else {
73+
return Err(TransportConversionError::InvalidTransport(
74+
transport_prefix.into(),
75+
));
76+
}
77+
}
78+
prefix => return Err(TransportConversionError::InvalidTransport(prefix.into())),
79+
};
80+
81+
return Ok(transport);
82+
}
83+
84+
Err(TransportConversionError::MissingColon)
85+
}
86+
}
87+
88+
#[cfg(test)]
89+
mod tests {
90+
use super::*;
91+
92+
#[test]
93+
fn test_transport_from_str() {
94+
// Test specific transports
95+
assert!(matches!(
96+
Transport::try_from("registry:example.com/image"),
97+
Ok(Transport::Registry)
98+
));
99+
assert!(matches!(
100+
Transport::try_from("oci:/path/to/image"),
101+
Ok(Transport::OciDir)
102+
));
103+
assert!(matches!(
104+
Transport::try_from("oci-archive:/path/to/archive.tar"),
105+
Ok(Transport::OciArchive)
106+
));
107+
assert!(matches!(
108+
Transport::try_from("docker-archive:/path/to/archive.tar"),
109+
Ok(Transport::DockerArchive)
110+
));
111+
assert!(matches!(
112+
Transport::try_from("containers-storage:example.com/image"),
113+
Ok(Transport::ContainerStorage)
114+
));
115+
assert!(matches!(
116+
Transport::try_from("dir:/path/to/directory"),
117+
Ok(Transport::Dir)
118+
));
119+
assert!(matches!(
120+
Transport::try_from("docker-daemon:example.com/image"),
121+
Ok(Transport::DockerDaemon)
122+
));
123+
124+
// Test docker:// prefix
125+
assert!(matches!(
126+
Transport::try_from("docker://example.com/image"),
127+
Ok(Transport::Registry)
128+
));
129+
130+
// Test bare image references with colon (port or tag)
131+
assert!(matches!(
132+
Transport::try_from("example.com:8080/image"),
133+
Err(TransportConversionError::InvalidTransport(_))
134+
));
135+
assert!(matches!(
136+
Transport::try_from("example.com/image:tag"),
137+
Err(TransportConversionError::InvalidTransport(_))
138+
));
139+
140+
// Test unknown transport (should error)
141+
assert!(matches!(
142+
Transport::try_from("unknown:/path"),
143+
Err(TransportConversionError::InvalidTransport(_))
144+
));
145+
}
146+
147+
#[test]
148+
fn test_transport_error_cases() {
149+
// Test missing colon (bare image reference without transport)
150+
assert!(matches!(
151+
Transport::try_from("docker.io/library/hello-world"),
152+
Err(TransportConversionError::MissingColon)
153+
));
154+
assert!(matches!(
155+
Transport::try_from("example.com/image"),
156+
Err(TransportConversionError::MissingColon)
157+
));
158+
159+
// Test invalid transport prefixes
160+
assert!(matches!(
161+
Transport::try_from("invalid:example.com/image"),
162+
Err(TransportConversionError::InvalidTransport(_))
163+
));
164+
assert!(matches!(
165+
Transport::try_from("ftp:example.com/image"),
166+
Err(TransportConversionError::InvalidTransport(_))
167+
));
168+
169+
// Test docker: without :// (should error)
170+
assert!(matches!(
171+
Transport::try_from("docker:example.com/image"),
172+
Err(TransportConversionError::InvalidTransport(_))
173+
));
174+
175+
// Test empty string
176+
assert!(matches!(
177+
Transport::try_from(""),
178+
Err(TransportConversionError::MissingColon)
179+
));
180+
181+
// Test just colon
182+
assert!(matches!(
183+
Transport::try_from(":"),
184+
Err(TransportConversionError::InvalidTransport(_))
185+
));
186+
}
187+
188+
#[test]
189+
fn test_transport_edge_cases() {
190+
// Test transport at end of string
191+
assert!(matches!(
192+
Transport::try_from("registry:"),
193+
Ok(Transport::Registry)
194+
));
195+
assert!(matches!(Transport::try_from("oci:"), Ok(Transport::OciDir)));
196+
197+
// Test docker:// with empty path
198+
assert!(matches!(
199+
Transport::try_from("docker://"),
200+
Ok(Transport::Registry)
201+
));
202+
203+
// Test multiple colons (should use first colon position)
204+
assert!(matches!(
205+
Transport::try_from("registry:example.com:8080/image"),
206+
Ok(Transport::Registry)
207+
));
208+
assert!(matches!(
209+
Transport::try_from("oci:/path/with:colon/image"),
210+
Ok(Transport::OciDir)
211+
));
212+
}
213+
214+
#[test]
215+
fn test_error_display() {
216+
let err = TransportConversionError::InvalidTransport("unknown".into());
217+
assert_eq!(err.to_string(), "Invalid transport");
218+
219+
let err = TransportConversionError::MissingColon;
220+
assert_eq!(err.to_string(), "Missing ':' in imgref");
221+
}
222+
223+
#[test]
224+
fn test_transport_display() {
225+
// Test that each transport converts to its expected string representation
226+
assert_eq!(Transport::Registry.to_string(), "docker://");
227+
assert_eq!(Transport::OciDir.to_string(), "oci:");
228+
assert_eq!(Transport::OciArchive.to_string(), "oci-archive:");
229+
assert_eq!(Transport::DockerArchive.to_string(), "docker-archive:");
230+
assert_eq!(
231+
Transport::ContainerStorage.to_string(),
232+
"containers-storage:"
233+
);
234+
assert_eq!(Transport::Dir.to_string(), "dir:");
235+
assert_eq!(Transport::DockerDaemon.to_string(), "docker-daemon:");
236+
}
237+
238+
#[test]
239+
fn test_transport_roundtrip() {
240+
// Test roundtrip conversion for transports that map back to themselves
241+
let transports = [
242+
Transport::OciDir,
243+
Transport::OciArchive,
244+
Transport::DockerArchive,
245+
Transport::ContainerStorage,
246+
Transport::Dir,
247+
Transport::DockerDaemon,
248+
];
249+
250+
for original_transport in transports {
251+
let transport_str = original_transport.to_string();
252+
let parsed = Transport::try_from(transport_str.as_str()).unwrap();
253+
assert_eq!(
254+
parsed, original_transport,
255+
"Failed roundtrip for {original_transport:?}"
256+
);
257+
}
258+
259+
// Test special case for Registry (docker:// -> Registry)
260+
let registry_str = Transport::Registry.to_string();
261+
let parsed = Transport::try_from(registry_str.as_str()).unwrap();
262+
assert!(matches!(parsed, Transport::Registry));
263+
}
264+
}

0 commit comments

Comments
 (0)