hyperlight_host/sandbox/snapshot/file/
reference.rs1use std::fmt;
22use std::str::FromStr;
23
24use oci_spec::image::{Digest as OciSpecDigest, DigestAlgorithm};
25
26#[derive(Clone, Debug, PartialEq, Eq, Hash)]
30pub struct OciTag(String);
31
32impl OciTag {
33 pub fn new(tag: impl Into<String>) -> crate::Result<Self> {
37 Self::try_from(tag.into())
38 }
39
40 pub fn as_str(&self) -> &str {
42 &self.0
43 }
44}
45
46fn validate_tag(tag: &str) -> crate::Result<()> {
47 let bytes = tag.as_bytes();
48 if bytes.is_empty() || bytes.len() > 128 {
49 return Err(crate::new_error!(
50 "tag {:?} is invalid: must be 1..=128 bytes",
51 tag
52 ));
53 }
54 let first = bytes[0];
55 if !(first.is_ascii_alphanumeric() || first == b'_') {
56 return Err(crate::new_error!(
57 "tag {:?} is invalid: first character must be alphanumeric or '_'",
58 tag
59 ));
60 }
61 for &b in &bytes[1..] {
62 if !(b.is_ascii_alphanumeric() || b == b'_' || b == b'.' || b == b'-') {
63 return Err(crate::new_error!(
64 "tag {:?} is invalid: characters after the first must be \
65 alphanumeric or one of '_', '.', '-'",
66 tag
67 ));
68 }
69 }
70 Ok(())
71}
72
73impl FromStr for OciTag {
74 type Err = crate::HyperlightError;
75
76 fn from_str(s: &str) -> crate::Result<Self> {
77 validate_tag(s)?;
78 Ok(Self(s.to_string()))
79 }
80}
81
82impl TryFrom<&str> for OciTag {
83 type Error = crate::HyperlightError;
84
85 fn try_from(s: &str) -> crate::Result<Self> {
86 s.parse()
87 }
88}
89
90impl TryFrom<String> for OciTag {
91 type Error = crate::HyperlightError;
92
93 fn try_from(s: String) -> crate::Result<Self> {
94 validate_tag(&s)?;
95 Ok(Self(s))
96 }
97}
98
99impl AsRef<str> for OciTag {
100 fn as_ref(&self) -> &str {
101 &self.0
102 }
103}
104
105impl fmt::Display for OciTag {
106 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
107 f.write_str(&self.0)
108 }
109}
110
111#[derive(Clone, Debug, PartialEq, Eq, Hash)]
115pub struct OciDigest(String);
116
117impl OciDigest {
118 pub fn as_str(&self) -> &str {
120 &self.0
121 }
122
123 pub(super) fn from_oci_spec_digest(digest: &OciSpecDigest) -> Self {
126 Self(digest.to_string())
127 }
128}
129
130fn validate_digest(s: &str) -> crate::Result<String> {
131 let digest = OciSpecDigest::from_str(s)
132 .map_err(|e| crate::new_error!("invalid OCI digest {:?}: {}", s, e))?;
133 if digest.algorithm() != &DigestAlgorithm::Sha256 {
134 return Err(crate::new_error!(
135 "OCI digest {:?} must use the sha256 algorithm, found {}",
136 s,
137 digest.algorithm()
138 ));
139 }
140 Ok(digest.to_string())
141}
142
143impl FromStr for OciDigest {
144 type Err = crate::HyperlightError;
145
146 fn from_str(s: &str) -> crate::Result<Self> {
147 Ok(Self(validate_digest(s)?))
148 }
149}
150
151impl TryFrom<&str> for OciDigest {
152 type Error = crate::HyperlightError;
153
154 fn try_from(s: &str) -> crate::Result<Self> {
155 s.parse()
156 }
157}
158
159impl TryFrom<String> for OciDigest {
160 type Error = crate::HyperlightError;
161
162 fn try_from(s: String) -> crate::Result<Self> {
163 Ok(Self(validate_digest(&s)?))
164 }
165}
166
167impl AsRef<str> for OciDigest {
168 fn as_ref(&self) -> &str {
169 &self.0
170 }
171}
172
173impl fmt::Display for OciDigest {
174 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
175 f.write_str(&self.0)
176 }
177}
178
179#[derive(Clone, Debug, PartialEq, Eq, Hash)]
182pub enum OciReference {
183 Tag(OciTag),
185 Digest(OciDigest),
187}
188
189impl From<OciTag> for OciReference {
190 fn from(tag: OciTag) -> Self {
191 OciReference::Tag(tag)
192 }
193}
194
195impl From<OciDigest> for OciReference {
196 fn from(digest: OciDigest) -> Self {
197 OciReference::Digest(digest)
198 }
199}
200
201impl FromStr for OciReference {
202 type Err = crate::HyperlightError;
203
204 fn from_str(s: &str) -> crate::Result<Self> {
207 if s.contains(':') {
208 Ok(OciReference::Digest(s.parse()?))
209 } else {
210 Ok(OciReference::Tag(s.parse()?))
211 }
212 }
213}
214
215impl fmt::Display for OciReference {
216 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
217 match self {
218 OciReference::Tag(t) => fmt::Display::fmt(t, f),
219 OciReference::Digest(d) => fmt::Display::fmt(d, f),
220 }
221 }
222}
223
224#[cfg(test)]
225mod tests {
226 use super::*;
227
228 const HEX64: &str = "0000000000000000000000000000000000000000000000000000000000000000";
231
232 #[test]
233 fn tag_accepts_grammar_and_length_bounds() {
234 assert!(OciTag::new("a").is_ok());
236 assert!(OciTag::new("Z").is_ok());
237 assert!(OciTag::new("9").is_ok());
238 assert!(OciTag::new("_").is_ok());
239 assert!(OciTag::new("v1.0_release-2").is_ok());
241 assert!(OciTag::new("a".repeat(128)).is_ok());
243 }
244
245 #[test]
246 fn tag_rejects_out_of_grammar_input() {
247 assert!(OciTag::new("").is_err());
249 assert!(OciTag::new("a".repeat(129)).is_err());
250 assert!(OciTag::new(".tag").is_err());
252 assert!(OciTag::new("-tag").is_err());
253 assert!(OciTag::new("a/b").is_err());
255 assert!(OciTag::new("a b").is_err());
256 }
257
258 #[test]
259 fn tag_never_contains_colon() {
260 assert!(OciTag::new("sha256:abc").is_err());
263 }
264
265 #[test]
266 fn digest_accepts_canonical_sha256() {
267 let s = format!("sha256:{HEX64}");
268 let d = OciDigest::try_from(s.as_str()).unwrap();
269 assert_eq!(d.as_str(), s);
270 }
271
272 #[test]
273 fn digest_rejects_non_sha256_algorithm() {
274 let s = format!("sha512:{}", "0".repeat(128));
275 assert!(OciDigest::try_from(s.as_str()).is_err());
276 }
277
278 #[test]
279 fn digest_rejects_malformed_input() {
280 assert!(OciDigest::try_from(HEX64).is_err());
282 assert!(OciDigest::try_from("sha256:abcd").is_err());
284 }
285
286 #[test]
287 fn reference_parses_tag_when_no_colon() {
288 let r: OciReference = "latest".parse().unwrap();
289 assert_eq!(r, OciReference::Tag(OciTag::new("latest").unwrap()));
290 }
291
292 #[test]
293 fn reference_parses_digest_when_colon_present() {
294 let s = format!("sha256:{HEX64}");
295 let r: OciReference = s.parse().unwrap();
296 assert_eq!(
297 r,
298 OciReference::Digest(OciDigest::try_from(s.as_str()).unwrap())
299 );
300 }
301
302 #[test]
303 fn tag_display_round_trips() {
304 let tag = OciTag::new("v1.2-rc1").unwrap();
305 assert_eq!(OciTag::new(tag.to_string()).unwrap(), tag);
306 }
307
308 #[test]
309 fn digest_display_round_trips() {
310 let d = OciDigest::try_from(format!("sha256:{HEX64}").as_str()).unwrap();
311 assert_eq!(OciDigest::try_from(d.to_string().as_str()).unwrap(), d);
312 }
313
314 #[test]
315 fn reference_display_round_trips() {
316 let tag_ref: OciReference = "latest".parse().unwrap();
317 assert_eq!(
318 tag_ref.to_string().parse::<OciReference>().unwrap(),
319 tag_ref
320 );
321
322 let digest_ref: OciReference = format!("sha256:{HEX64}").parse().unwrap();
323 assert_eq!(
324 digest_ref.to_string().parse::<OciReference>().unwrap(),
325 digest_ref
326 );
327 }
328}