1use core::fmt::{self, Write as _};
4
5use crate::digest::{Digest, DigestError};
6
7#[derive(Clone, Copy, Debug, Eq, PartialEq)]
8pub enum Scheme {
9 Http,
10 Https,
11}
12
13impl fmt::Display for Scheme {
14 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
15 formatter.write_str(match self {
16 Self::Http => "http",
17 Self::Https => "https",
18 })
19 }
20}
21
22#[derive(Clone, Copy, Debug, Eq, PartialEq)]
23pub enum Selector<'a> {
24 Tag(&'a str),
25 Digest(Digest),
26}
27
28#[derive(Clone, Copy, Debug, Eq, PartialEq)]
30pub struct Repository<'a> {
31 registry: &'a str,
32 repository: &'a str,
33}
34
35impl<'a> Repository<'a> {
36 pub fn parse(input: &'a str) -> Result<Self, ReferenceError> {
37 let value = input
38 .strip_prefix("oci://")
39 .ok_or(ReferenceError::InvalidScheme)?;
40 let (registry, repository) = value
41 .split_once('/')
42 .ok_or(ReferenceError::MissingRepository)?;
43 validate_registry(registry)?;
44 validate_repository(repository)?;
45 Ok(Self {
46 registry,
47 repository,
48 })
49 }
50
51 pub const fn registry(&self) -> &'a str {
52 self.registry
53 }
54
55 pub const fn repository(&self) -> &'a str {
56 self.repository
57 }
58
59 pub fn with_tag(self, tag: &'a str) -> Result<Reference<'a>, ReferenceError> {
60 validate_tag(tag)?;
61 Ok(Reference {
62 registry: self.registry,
63 repository: self.repository,
64 selector: Selector::Tag(tag),
65 })
66 }
67
68 pub const fn with_digest(self, digest: Digest) -> Reference<'a> {
69 Reference {
70 registry: self.registry,
71 repository: self.repository,
72 selector: Selector::Digest(digest),
73 }
74 }
75
76 pub fn manifest_path<'buffer>(
77 &self,
78 selector: Selector<'_>,
79 buffer: &'buffer mut [u8],
80 ) -> Result<&'buffer str, ReferenceError> {
81 let mut writer = BufferWriter::new(buffer);
82 write!(writer, "/v2/{}/manifests/", self.repository)
83 .map_err(|_| ReferenceError::BufferTooSmall)?;
84 match selector {
85 Selector::Tag(tag) => writer
86 .write_str(tag)
87 .map_err(|_| ReferenceError::BufferTooSmall)?,
88 Selector::Digest(digest) => {
89 write!(writer, "{digest}").map_err(|_| ReferenceError::BufferTooSmall)?
90 }
91 }
92 writer.finish()
93 }
94
95 pub fn manifest_digest_path<'buffer>(
96 &self,
97 digest: Digest,
98 buffer: &'buffer mut [u8],
99 ) -> Result<&'buffer str, ReferenceError> {
100 write_path(
101 buffer,
102 format_args!("/v2/{}/manifests/{digest}", self.repository),
103 )
104 }
105
106 pub fn blob_path<'buffer>(
107 &self,
108 digest: Digest,
109 buffer: &'buffer mut [u8],
110 ) -> Result<&'buffer str, ReferenceError> {
111 write_path(
112 buffer,
113 format_args!("/v2/{}/blobs/{digest}", self.repository),
114 )
115 }
116
117 pub fn referrers_path<'buffer>(
118 &self,
119 digest: Digest,
120 buffer: &'buffer mut [u8],
121 ) -> Result<&'buffer str, ReferenceError> {
122 write_path(
123 buffer,
124 format_args!("/v2/{}/referrers/{digest}", self.repository),
125 )
126 }
127
128 pub fn tags_path<'buffer>(
129 &self,
130 buffer: &'buffer mut [u8],
131 ) -> Result<&'buffer str, ReferenceError> {
132 write_path(buffer, format_args!("/v2/{}/tags/list", self.repository))
133 }
134
135 pub fn tags_page_path<'buffer>(
136 &self,
137 page_size: u16,
138 buffer: &'buffer mut [u8],
139 ) -> Result<&'buffer str, ReferenceError> {
140 write_path(
141 buffer,
142 format_args!("/v2/{}/tags/list?n={page_size}", self.repository),
143 )
144 }
145}
146
147#[derive(Clone, Copy, Debug, Eq, PartialEq)]
148pub struct Reference<'a> {
149 registry: &'a str,
150 repository: &'a str,
151 selector: Selector<'a>,
152}
153
154impl<'a> Reference<'a> {
155 pub fn parse(input: &'a str) -> Result<Self, ReferenceError> {
156 let value = input
157 .strip_prefix("oci://")
158 .ok_or(ReferenceError::InvalidScheme)?;
159 let (registry, repository_and_selector) = value
160 .split_once('/')
161 .ok_or(ReferenceError::MissingRepository)?;
162 validate_registry(registry)?;
163
164 let (repository, selector) =
165 if let Some((repository, digest)) = repository_and_selector.rsplit_once('@') {
166 let digest = Digest::parse(digest).map_err(ReferenceError::Digest)?;
167 (repository, Selector::Digest(digest))
168 } else {
169 let (repository, tag) = repository_and_selector
170 .rsplit_once(':')
171 .ok_or(ReferenceError::MissingSelector)?;
172 validate_tag(tag)?;
173 (repository, Selector::Tag(tag))
174 };
175 validate_repository(repository)?;
176 Ok(Self {
177 registry,
178 repository,
179 selector,
180 })
181 }
182
183 pub const fn registry(&self) -> &'a str {
184 self.registry
185 }
186
187 pub const fn repository(&self) -> &'a str {
188 self.repository
189 }
190
191 pub const fn selector(&self) -> Selector<'a> {
192 self.selector
193 }
194
195 pub const fn as_repository(&self) -> Repository<'a> {
196 Repository {
197 registry: self.registry,
198 repository: self.repository,
199 }
200 }
201
202 pub fn manifest_path<'buffer>(
203 &self,
204 buffer: &'buffer mut [u8],
205 ) -> Result<&'buffer str, ReferenceError> {
206 self.as_repository().manifest_path(self.selector, buffer)
207 }
208
209 pub fn manifest_digest_path<'buffer>(
210 &self,
211 digest: Digest,
212 buffer: &'buffer mut [u8],
213 ) -> Result<&'buffer str, ReferenceError> {
214 self.as_repository().manifest_digest_path(digest, buffer)
215 }
216
217 pub fn blob_path<'buffer>(
218 &self,
219 digest: Digest,
220 buffer: &'buffer mut [u8],
221 ) -> Result<&'buffer str, ReferenceError> {
222 self.as_repository().blob_path(digest, buffer)
223 }
224
225 pub fn referrers_path<'buffer>(
226 &self,
227 digest: Digest,
228 buffer: &'buffer mut [u8],
229 ) -> Result<&'buffer str, ReferenceError> {
230 self.as_repository().referrers_path(digest, buffer)
231 }
232
233 pub fn tags_path<'buffer>(
234 &self,
235 buffer: &'buffer mut [u8],
236 ) -> Result<&'buffer str, ReferenceError> {
237 self.as_repository().tags_path(buffer)
238 }
239}
240
241#[derive(Clone, Copy, Debug, Eq, PartialEq)]
242pub enum ReferenceError {
243 InvalidScheme,
244 InvalidRegistry,
245 MissingRepository,
246 InvalidRepository,
247 MissingSelector,
248 InvalidTag,
249 Digest(DigestError),
250 BufferTooSmall,
251}
252
253impl fmt::Display for ReferenceError {
254 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
255 formatter.write_str(match self {
256 Self::InvalidScheme => "OCI reference must start with oci://",
257 Self::InvalidRegistry => "invalid OCI registry authority",
258 Self::MissingRepository => "OCI reference is missing a repository",
259 Self::InvalidRepository => "invalid OCI repository name",
260 Self::MissingSelector => "OCI reference is missing a tag or digest",
261 Self::InvalidTag => "invalid OCI tag",
262 Self::Digest(_) => "invalid OCI digest",
263 Self::BufferTooSmall => "request path buffer is too small",
264 })
265 }
266}
267
268fn validate_registry(registry: &str) -> Result<(), ReferenceError> {
269 if registry.is_empty()
270 || !registry.is_ascii()
271 || registry
272 .bytes()
273 .any(|byte| byte.is_ascii_whitespace() || matches!(byte, b'/' | b'@' | b'?' | b'#'))
274 {
275 return Err(ReferenceError::InvalidRegistry);
276 }
277 Ok(())
278}
279
280fn validate_repository(repository: &str) -> Result<(), ReferenceError> {
281 if repository.is_empty() || repository.len() > 255 {
282 return Err(ReferenceError::InvalidRepository);
283 }
284 for component in repository.split('/') {
285 let bytes = component.as_bytes();
286 if bytes.is_empty()
287 || !is_lower_alphanumeric(bytes[0])
288 || !is_lower_alphanumeric(*bytes.last().unwrap_or(&0))
289 {
290 return Err(ReferenceError::InvalidRepository);
291 }
292 let mut index = 1;
293 while index + 1 < bytes.len() {
294 if is_lower_alphanumeric(bytes[index]) {
295 index += 1;
296 continue;
297 }
298 match bytes[index] {
299 b'.' | b'_' => index += 1,
300 b'-' => {
301 while index < bytes.len() && bytes[index] == b'-' {
302 index += 1;
303 }
304 }
305 _ => return Err(ReferenceError::InvalidRepository),
306 }
307 if index >= bytes.len() || !is_lower_alphanumeric(bytes[index]) {
308 return Err(ReferenceError::InvalidRepository);
309 }
310 }
311 }
312 Ok(())
313}
314
315fn validate_tag(tag: &str) -> Result<(), ReferenceError> {
316 let bytes = tag.as_bytes();
317 if bytes.is_empty()
318 || bytes.len() > 128
319 || !matches!(bytes[0], b'a'..=b'z' | b'A'..=b'Z' | b'0'..=b'9' | b'_')
320 || !bytes[1..].iter().all(
321 |byte| matches!(*byte, b'a'..=b'z' | b'A'..=b'Z' | b'0'..=b'9' | b'_' | b'.' | b'-'),
322 )
323 {
324 return Err(ReferenceError::InvalidTag);
325 }
326 Ok(())
327}
328
329const fn is_lower_alphanumeric(byte: u8) -> bool {
330 matches!(byte, b'a'..=b'z' | b'0'..=b'9')
331}
332
333fn write_path<'a>(
334 buffer: &'a mut [u8],
335 arguments: fmt::Arguments<'_>,
336) -> Result<&'a str, ReferenceError> {
337 let mut writer = BufferWriter::new(buffer);
338 writer
339 .write_fmt(arguments)
340 .map_err(|_| ReferenceError::BufferTooSmall)?;
341 writer.finish()
342}
343
344struct BufferWriter<'a> {
345 buffer: &'a mut [u8],
346 length: usize,
347}
348
349impl<'a> BufferWriter<'a> {
350 fn new(buffer: &'a mut [u8]) -> Self {
351 Self { buffer, length: 0 }
352 }
353
354 fn finish(self) -> Result<&'a str, ReferenceError> {
355 core::str::from_utf8(&self.buffer[..self.length])
356 .map_err(|_| ReferenceError::BufferTooSmall)
357 }
358}
359
360impl fmt::Write for BufferWriter<'_> {
361 fn write_str(&mut self, value: &str) -> fmt::Result {
362 let end = self.length.checked_add(value.len()).ok_or(fmt::Error)?;
363 let destination = self.buffer.get_mut(self.length..end).ok_or(fmt::Error)?;
364 destination.copy_from_slice(value.as_bytes());
365 self.length = end;
366 Ok(())
367 }
368}
369
370#[cfg(test)]
371mod tests {
372 use std::format;
373
374 use super::{Reference, ReferenceError, Repository, Selector};
375
376 #[test]
377 fn parses_tags_and_digests() {
378 let tagged = Reference::parse("oci://example.com/team/image:v1.2").unwrap();
379 assert_eq!(tagged.registry(), "example.com");
380 assert_eq!(tagged.repository(), "team/image");
381 assert_eq!(tagged.selector(), Selector::Tag("v1.2"));
382
383 let digest = "sha256:ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad";
384 let input = format!("oci://example.com/image@{digest}");
385 let pinned = Reference::parse(&input).unwrap();
386 let mut path = [0; 128];
387 assert_eq!(
388 pinned.manifest_path(&mut path).unwrap(),
389 format!("/v2/image/manifests/{digest}")
390 );
391 }
392
393 #[test]
394 fn rejects_invalid_names_and_small_buffers() {
395 assert_eq!(
396 Reference::parse("oci://example.com/Upper:tag"),
397 Err(ReferenceError::InvalidRepository)
398 );
399 let reference = Reference::parse("oci://example.com/image:tag").unwrap();
400 assert_eq!(
401 reference.manifest_path(&mut [0; 4]),
402 Err(ReferenceError::BufferTooSmall)
403 );
404 }
405
406 #[test]
407 fn builds_references_and_tag_pages_from_repositories() {
408 let repository = Repository::parse("oci://example.com/team/image").unwrap();
409 assert_eq!(repository.registry(), "example.com");
410 assert_eq!(repository.repository(), "team/image");
411 assert_eq!(
412 repository.with_tag("latest").unwrap().selector(),
413 Selector::Tag("latest")
414 );
415 let mut path = [0; 64];
416 assert_eq!(
417 repository.tags_page_path(100, &mut path).unwrap(),
418 "/v2/team/image/tags/list?n=100"
419 );
420 assert_eq!(
421 Repository::parse("oci://example.com/image:tag"),
422 Err(ReferenceError::InvalidRepository)
423 );
424 }
425}