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