1use core::{fmt, str};
4
5use crate::{
6 buffer::BufferWriter,
7 digest::Digest,
8 json::Value,
9 metadata::{JsonString, MetadataError},
10 reference::{Reference, ReferenceError, Repository, Scheme, Selector},
11};
12
13pub const MANIFEST_ACCEPT: &str = concat!(
14 "application/vnd.oci.image.index.v1+json, ",
15 "application/vnd.oci.image.manifest.v1+json, ",
16 "application/vnd.docker.distribution.manifest.list.v2+json, ",
17 "application/vnd.docker.distribution.manifest.v2+json"
18);
19
20pub const OCI_INDEX_ACCEPT: &str = "application/vnd.oci.image.index.v1+json";
21
22#[derive(Clone, Copy, Debug, Eq, PartialEq)]
23pub enum Method {
24 Get,
25 Head,
26}
27
28#[derive(Clone, Copy, Debug, Eq, PartialEq)]
29pub struct Target<'a> {
30 pub scheme: Scheme,
31 pub authority: &'a str,
32 pub path_and_query: &'a str,
33}
34
35impl<'a> Target<'a> {
36 pub fn parse(url: &'a str) -> Result<Self, RegistryError> {
37 let (scheme, remainder) = if let Some(value) = url.strip_prefix("https://") {
38 (Scheme::Https, value)
39 } else if let Some(value) = url.strip_prefix("http://") {
40 (Scheme::Http, value)
41 } else {
42 return Err(RegistryError::InvalidUrl);
43 };
44 let (authority, path_and_query) = match remainder.find('/') {
45 Some(index) => (&remainder[..index], &remainder[index..]),
46 None => (remainder, "/"),
47 };
48 if authority.is_empty()
49 || !authority.is_ascii()
50 || authority
51 .bytes()
52 .any(|byte| byte.is_ascii_whitespace() || matches!(byte, b'@' | b'#'))
53 || !path_and_query.starts_with('/')
54 {
55 return Err(RegistryError::InvalidUrl);
56 }
57 Ok(Self {
58 scheme,
59 authority,
60 path_and_query,
61 })
62 }
63
64 pub fn same_origin(&self, other: &Self) -> bool {
65 self.scheme == other.scheme && self.authority.eq_ignore_ascii_case(other.authority)
66 }
67}
68
69#[derive(Clone, Copy, Debug, Eq, PartialEq)]
70pub struct Request<'a> {
71 pub method: Method,
72 pub target: Target<'a>,
73 pub accept: &'a str,
74 pub authorization: Option<&'a str>,
75}
76
77#[derive(Clone, Copy, Debug, Eq, PartialEq)]
78pub enum ReadOperation {
79 Probe,
80 Catalog,
81 Manifest,
82 Blob,
83 Referrers,
84 Tags,
85}
86
87pub struct RequestPlanner<'reference> {
88 repository: Repository<'reference>,
89 selector: Option<Selector<'reference>>,
90 scheme: Scheme,
91}
92
93impl<'reference> RequestPlanner<'reference> {
94 pub const fn new(reference: Reference<'reference>) -> Self {
95 Self::with_scheme(reference, Scheme::Https)
96 }
97
98 pub const fn with_scheme(reference: Reference<'reference>, scheme: Scheme) -> Self {
99 Self {
100 repository: reference.as_repository(),
101 selector: Some(reference.selector()),
102 scheme,
103 }
104 }
105
106 pub const fn for_repository(repository: Repository<'reference>) -> Self {
107 Self::for_repository_with_scheme(repository, Scheme::Https)
108 }
109
110 pub const fn for_repository_with_scheme(
111 repository: Repository<'reference>,
112 scheme: Scheme,
113 ) -> Self {
114 Self {
115 repository,
116 selector: None,
117 scheme,
118 }
119 }
120
121 pub fn probe<'buffer>(
122 &'buffer self,
123 buffer: &'buffer mut [u8],
124 ) -> Result<Request<'buffer>, RegistryError> {
125 copy_request(
126 buffer,
127 self.scheme,
128 self.repository.registry(),
129 "/v2/",
130 "*/*",
131 )
132 }
133
134 pub fn manifest<'buffer>(
135 &'buffer self,
136 buffer: &'buffer mut [u8],
137 ) -> Result<Request<'buffer>, RegistryError> {
138 let selector = self.selector.ok_or(ReferenceError::MissingSelector)?;
139 Ok(self.request(
140 self.repository.manifest_path(selector, buffer)?,
141 MANIFEST_ACCEPT,
142 ))
143 }
144
145 pub fn head_manifest<'buffer>(
146 &'buffer self,
147 buffer: &'buffer mut [u8],
148 ) -> Result<Request<'buffer>, RegistryError> {
149 let mut request = self.manifest(buffer)?;
150 request.method = Method::Head;
151 Ok(request)
152 }
153
154 pub fn manifest_by_digest<'buffer>(
155 &'buffer self,
156 digest: Digest,
157 buffer: &'buffer mut [u8],
158 ) -> Result<Request<'buffer>, RegistryError> {
159 Ok(self.request(
160 self.repository.manifest_digest_path(digest, buffer)?,
161 MANIFEST_ACCEPT,
162 ))
163 }
164
165 pub fn blob<'buffer>(
166 &'buffer self,
167 digest: Digest,
168 accept: &'buffer str,
169 buffer: &'buffer mut [u8],
170 ) -> Result<Request<'buffer>, RegistryError> {
171 Ok(self.request(self.repository.blob_path(digest, buffer)?, accept))
172 }
173
174 pub fn head_blob<'buffer>(
175 &'buffer self,
176 digest: Digest,
177 accept: &'buffer str,
178 buffer: &'buffer mut [u8],
179 ) -> Result<Request<'buffer>, RegistryError> {
180 let mut request = self.blob(digest, accept, buffer)?;
181 request.method = Method::Head;
182 Ok(request)
183 }
184
185 pub fn referrers<'buffer>(
186 &'buffer self,
187 digest: Digest,
188 buffer: &'buffer mut [u8],
189 ) -> Result<Request<'buffer>, RegistryError> {
190 Ok(self.request(
191 self.repository.referrers_path(digest, buffer)?,
192 OCI_INDEX_ACCEPT,
193 ))
194 }
195
196 pub fn tags<'buffer>(
197 &'buffer self,
198 buffer: &'buffer mut [u8],
199 ) -> Result<Request<'buffer>, RegistryError> {
200 Ok(self.request(self.repository.tags_path(buffer)?, "application/json"))
201 }
202
203 pub fn tags_page<'buffer>(
204 &'buffer self,
205 page_size: u16,
206 buffer: &'buffer mut [u8],
207 ) -> Result<Request<'buffer>, RegistryError> {
208 if page_size == 0 {
209 return Err(RegistryError::InvalidPageSize);
210 }
211 Ok(self.request(
212 self.repository.tags_page_path(page_size, buffer)?,
213 "application/json",
214 ))
215 }
216
217 fn request<'buffer>(
218 &'buffer self,
219 path: &'buffer str,
220 accept: &'buffer str,
221 ) -> Request<'buffer> {
222 get_request(self.scheme, self.repository.registry(), path, accept)
223 }
224
225 pub fn referrers_fallback<'buffer>(
226 &'buffer self,
227 digest: Digest,
228 buffer: &'buffer mut [u8],
229 ) -> Result<Request<'buffer>, RegistryError> {
230 let mut digest_text = [0; 71];
231 let length = write_display(&mut digest_text, digest)?;
232 digest_text[6] = b'-';
233 let mut path = [0; 384];
234 let prefix = b"/v2/";
235 let middle = b"/manifests/";
236 let needed = prefix.len() + self.repository.repository().len() + middle.len() + length;
237 if needed > path.len() {
238 return Err(RegistryError::BufferTooSmall);
239 }
240 let mut position = 0;
241 for part in [
242 prefix.as_slice(),
243 self.repository.repository().as_bytes(),
244 middle.as_slice(),
245 &digest_text[..length],
246 ] {
247 path[position..position + part.len()].copy_from_slice(part);
248 position += part.len();
249 }
250 let path = str::from_utf8(&path[..position]).map_err(|_| RegistryError::InvalidUrl)?;
251 copy_request(
252 buffer,
253 self.scheme,
254 self.repository.registry(),
255 path,
256 MANIFEST_ACCEPT,
257 )
258 }
259}
260
261pub struct RegistryRequestPlanner<'authority> {
263 authority: &'authority str,
264 scheme: Scheme,
265}
266
267impl<'authority> RegistryRequestPlanner<'authority> {
268 pub fn new(authority: &'authority str) -> Result<Self, RegistryError> {
269 Self::with_scheme(authority, Scheme::Https)
270 }
271
272 pub fn with_scheme(authority: &'authority str, scheme: Scheme) -> Result<Self, RegistryError> {
273 validate_authority(authority)?;
274 Ok(Self { authority, scheme })
275 }
276
277 pub fn probe<'buffer>(
278 &'buffer self,
279 buffer: &'buffer mut [u8],
280 ) -> Result<Request<'buffer>, RegistryError> {
281 copy_request(buffer, self.scheme, self.authority, "/v2/", "*/*")
282 }
283
284 pub fn catalog<'buffer>(
285 &'buffer self,
286 buffer: &'buffer mut [u8],
287 ) -> Result<Request<'buffer>, RegistryError> {
288 copy_request(
289 buffer,
290 self.scheme,
291 self.authority,
292 "/v2/_catalog",
293 "application/json",
294 )
295 }
296
297 pub fn catalog_page<'buffer>(
298 &'buffer self,
299 page_size: u16,
300 buffer: &'buffer mut [u8],
301 ) -> Result<Request<'buffer>, RegistryError> {
302 if page_size == 0 {
303 return Err(RegistryError::InvalidPageSize);
304 }
305 const PREFIX: &[u8] = b"/v2/_catalog?n=";
306 if buffer.len() < PREFIX.len() {
307 return Err(RegistryError::BufferTooSmall);
308 }
309 buffer[..PREFIX.len()].copy_from_slice(PREFIX);
310 let digits = write_display(&mut buffer[PREFIX.len()..], page_size)?;
311 request_from_path(
312 buffer,
313 PREFIX.len() + digits,
314 self.scheme,
315 self.authority,
316 "application/json",
317 )
318 }
319}
320
321#[derive(Clone, Copy, Debug, Eq, PartialEq)]
322pub struct Header<'a> {
323 pub name: &'a str,
324 pub value: &'a [u8],
325}
326
327#[derive(Clone, Copy, Debug, Eq, PartialEq)]
328pub struct ResponseHead<'a> {
329 pub status: u16,
330 pub headers: &'a [Header<'a>],
331}
332
333impl<'a> ResponseHead<'a> {
334 pub fn header(&self, name: &str) -> Option<&'a [u8]> {
335 self.headers
336 .iter()
337 .find(|header| header.name.eq_ignore_ascii_case(name))
338 .map(|header| header.value)
339 }
340
341 pub fn classify(&self, allow_not_found: bool) -> Result<ResponseAction<'a>, RegistryError> {
342 match self.status {
343 200..=299 => Ok(ResponseAction::Success),
344 301 | 302 | 303 | 307 | 308 => {
345 let location = self
346 .header("location")
347 .ok_or(RegistryError::MissingHeader("Location"))?;
348 Ok(ResponseAction::Redirect(
349 str::from_utf8(location).map_err(|_| RegistryError::InvalidHeader)?,
350 ))
351 }
352 401 => {
353 let challenge = self
354 .header("www-authenticate")
355 .ok_or(RegistryError::MissingHeader("WWW-Authenticate"))?;
356 Ok(ResponseAction::Authenticate(AuthChallenge::parse(
357 str::from_utf8(challenge).map_err(|_| RegistryError::InvalidHeader)?,
358 )?))
359 }
360 404 if allow_not_found => Ok(ResponseAction::NotFound),
361 429 | 500..=599 => Ok(ResponseAction::Retry(RetryAdvice {
362 status: self.status,
363 after: self
364 .header("retry-after")
365 .map(parse_retry_after)
366 .transpose()?,
367 })),
368 status => Err(RegistryError::HttpStatus(status)),
369 }
370 }
371
372 pub fn next_link(&self) -> Result<Option<&'a str>, RegistryError> {
373 let Some(value) = self.header("link") else {
374 return Ok(None);
375 };
376 let value = str::from_utf8(value).map_err(|_| RegistryError::InvalidHeader)?;
377 for link in value.split(',') {
378 let link = link.trim();
379 let Some(end) = link.find('>') else {
380 continue;
381 };
382 if !link.starts_with('<') {
383 continue;
384 }
385 if link[end + 1..].split(';').any(|parameter| {
386 parameter.trim().eq_ignore_ascii_case("rel=\"next\"")
387 || parameter.trim().eq_ignore_ascii_case("rel=next")
388 }) {
389 return Ok(Some(&link[1..end]));
390 }
391 }
392 Ok(None)
393 }
394}
395
396#[derive(Clone, Copy, Debug, Eq, PartialEq)]
397pub enum ResponseAction<'a> {
398 Success,
399 Redirect(&'a str),
400 Authenticate(AuthChallenge<'a>),
401 NotFound,
402 Retry(RetryAdvice<'a>),
403}
404
405#[derive(Clone, Copy, Debug, Eq, PartialEq)]
406pub struct RetryAdvice<'a> {
407 pub status: u16,
408 pub after: Option<RetryAfter<'a>>,
409}
410
411#[derive(Clone, Copy, Debug, Eq, PartialEq)]
412pub enum RetryAfter<'a> {
413 Seconds(u64),
414 HttpDate(&'a str),
415}
416
417#[derive(Clone, Copy, Debug, Eq, PartialEq)]
418pub enum AuthChallenge<'a> {
419 Bearer(BearerChallenge<'a>),
420 Basic { realm: Option<&'a str> },
421}
422
423#[derive(Clone, Copy, Debug, Eq, PartialEq)]
424pub struct BearerChallenge<'a> {
425 pub realm: &'a str,
426 pub service: Option<&'a str>,
427 pub scope: Option<&'a str>,
428}
429
430impl<'a> AuthChallenge<'a> {
431 pub fn parse(value: &'a str) -> Result<Self, RegistryError> {
432 let (scheme, parameters) = value
433 .split_once(' ')
434 .map(|(scheme, parameters)| (scheme, parameters.trim()))
435 .unwrap_or((value, ""));
436 if scheme.eq_ignore_ascii_case("bearer") {
437 let mut realm = None;
438 let mut service = None;
439 let mut scope = None;
440 let mut remaining = parameters;
441 while !remaining.trim_start().is_empty() {
442 remaining = remaining.trim_start();
443 let equals = remaining.find('=').ok_or(RegistryError::InvalidChallenge)?;
444 let name = remaining[..equals].trim();
445 remaining = &remaining[equals + 1..];
446 let (parameter, rest) = quoted_parameter(remaining)?;
447 match name {
448 "realm" => realm = unique(realm, parameter)?,
449 "service" => service = unique(service, parameter)?,
450 "scope" => scope = unique(scope, parameter)?,
451 _ => {}
452 }
453 remaining = rest.trim_start();
454 if let Some(rest) = remaining.strip_prefix(',') {
455 remaining = rest;
456 } else if !remaining.is_empty() {
457 return Err(RegistryError::InvalidChallenge);
458 }
459 }
460 Ok(Self::Bearer(BearerChallenge {
461 realm: realm.ok_or(RegistryError::InvalidChallenge)?,
462 service,
463 scope,
464 }))
465 } else if scheme.eq_ignore_ascii_case("basic") {
466 let realm = parameters
467 .strip_prefix("realm=")
468 .map(quoted_parameter)
469 .transpose()?
470 .map(|value| value.0);
471 Ok(Self::Basic { realm })
472 } else {
473 Err(RegistryError::UnsupportedAuthentication)
474 }
475 }
476}
477
478#[derive(Clone, Copy, Debug, Eq, PartialEq)]
479pub struct TokenResponse<'a> {
480 pub token: JsonString<'a>,
481 pub expires_in: Option<u64>,
482}
483
484impl<'a> TokenResponse<'a> {
485 pub fn parse(bytes: &'a [u8]) -> Result<Self, RegistryError> {
486 let object = Value::parse_document(bytes)?.object()?;
487 let token = match (object.get("token")?, object.get("access_token")?) {
488 (Some(token), _) | (None, Some(token)) => token.string()?,
489 (None, None) => return Err(RegistryError::InvalidTokenResponse),
490 };
491 let expires_in = object.get("expires_in")?.map(Value::u64).transpose()?;
492 Ok(Self { token, expires_in })
493 }
494
495 pub fn bearer_authorization(self, buffer: &mut [u8]) -> Result<&str, RegistryError> {
497 const PREFIX: &[u8] = b"Bearer ";
498 if buffer.len() < PREFIX.len() {
499 return Err(RegistryError::BufferTooSmall);
500 }
501 buffer[..PREFIX.len()].copy_from_slice(PREFIX);
502 let token_length = self
503 .token
504 .decode_into(&mut buffer[PREFIX.len()..])
505 .map_err(MetadataError::from)?
506 .len();
507 let length = PREFIX.len() + token_length;
508 let token = &buffer[PREFIX.len()..length];
509 if token.is_empty() || !token.iter().all(|byte| (b'!'..=b'~').contains(byte)) {
510 return Err(RegistryError::InvalidTokenResponse);
511 }
512 str::from_utf8(&buffer[..length]).map_err(|_| RegistryError::InvalidTokenResponse)
513 }
514}
515
516#[derive(Clone, Copy, Debug, Eq, PartialEq)]
517pub struct Credentials<'a> {
518 pub username: &'a str,
519 pub password: &'a str,
520}
521
522pub fn basic_authorization<'buffer>(
524 credentials: Credentials<'_>,
525 buffer: &'buffer mut [u8],
526) -> Result<&'buffer str, RegistryError> {
527 const PREFIX: &[u8] = b"Basic ";
528 if credentials.username.contains(':')
529 || credentials
530 .username
531 .bytes()
532 .chain(credentials.password.bytes())
533 .any(|byte| matches!(byte, b'\r' | b'\n'))
534 {
535 return Err(RegistryError::InvalidCredentials);
536 }
537 let mut input = credentials
538 .username
539 .bytes()
540 .chain(core::iter::once(b':'))
541 .chain(credentials.password.bytes());
542 let mut output = PREFIX.len();
543 buffer
544 .get_mut(..PREFIX.len())
545 .ok_or(RegistryError::BufferTooSmall)?
546 .copy_from_slice(PREFIX);
547 while let Some(first) = input.next() {
548 let second = input.next();
549 let third = input.next();
550 let bytes = [first, second.unwrap_or(0), third.unwrap_or(0)];
551 let length = 1 + usize::from(second.is_some()) + usize::from(third.is_some());
552 let end = output.checked_add(4).ok_or(RegistryError::BufferTooSmall)?;
553 let destination = buffer
554 .get_mut(output..end)
555 .ok_or(RegistryError::BufferTooSmall)?;
556 use base64ct::{Base64, Encoding};
557 Base64::encode(&bytes[..length], destination).map_err(|_| RegistryError::BufferTooSmall)?;
558 output = end;
559 }
560 str::from_utf8(&buffer[..output]).map_err(|_| RegistryError::InvalidCredentials)
561}
562
563pub fn bearer_token_url<'buffer>(
565 challenge: BearerChallenge<'_>,
566 buffer: &'buffer mut [u8],
567) -> Result<&'buffer str, RegistryError> {
568 let mut length = 0;
569 append(buffer, &mut length, challenge.realm.as_bytes())?;
570 let mut separator = if challenge.realm.contains('?') {
571 b'&'
572 } else {
573 b'?'
574 };
575 for (name, value) in [("service", challenge.service), ("scope", challenge.scope)] {
576 let Some(value) = value else {
577 continue;
578 };
579 append(buffer, &mut length, &[separator])?;
580 append(buffer, &mut length, name.as_bytes())?;
581 append(buffer, &mut length, b"=")?;
582 for byte in value.bytes() {
583 if byte.is_ascii_alphanumeric() || matches!(byte, b'-' | b'.' | b'_' | b'~') {
584 append(buffer, &mut length, &[byte])?;
585 } else {
586 append(
587 buffer,
588 &mut length,
589 &[b'%', HEX[(byte >> 4) as usize], HEX[(byte & 0x0f) as usize]],
590 )?;
591 }
592 }
593 separator = b'&';
594 }
595 str::from_utf8(&buffer[..length]).map_err(|_| RegistryError::InvalidChallenge)
596}
597
598const HEX: &[u8; 16] = b"0123456789ABCDEF";
599
600pub trait CredentialProvider {
601 type Error;
602
603 fn credentials<'a>(
604 &'a mut self,
605 authority: &str,
606 scope: Option<&str>,
607 ) -> Result<Option<Credentials<'a>>, Self::Error>;
608}
609
610pub trait ExternalUrlPolicy {
611 fn allow(&mut self, target: &Target<'_>) -> bool;
612}
613
614pub fn validate_redirect<'a>(
615 current: Target<'_>,
616 location: &'a str,
617 allow_insecure_downgrade: bool,
618) -> Result<Redirect<'a>, RegistryError> {
619 let target = if location.starts_with("https://") || location.starts_with("http://") {
620 Target::parse(location)?
621 } else {
622 return Err(RegistryError::RelativeRedirectRequiresResolution);
623 };
624 if current.scheme == Scheme::Https && target.scheme == Scheme::Http && !allow_insecure_downgrade
625 {
626 return Err(RegistryError::InsecureRedirect);
627 }
628 Ok(Redirect {
629 strip_authorization: !current.same_origin(&target),
630 target,
631 })
632}
633
634#[derive(Clone, Copy, Debug, Eq, PartialEq)]
635pub struct Redirect<'a> {
636 pub target: Target<'a>,
637 pub strip_authorization: bool,
638}
639
640#[derive(Clone, Copy, Debug, Eq, PartialEq, derive_more::Display, derive_more::From)]
641pub enum RegistryError {
642 #[display("invalid registry reference: {_0}")]
643 #[from]
644 Reference(ReferenceError),
645 #[display("invalid registry metadata: {_0}")]
646 #[from(MetadataError, crate::json::JsonError)]
647 Metadata(MetadataError),
648 #[display("invalid registry URL")]
649 InvalidUrl,
650 #[display("invalid registry response header")]
651 InvalidHeader,
652 #[display("registry response lacks {_0}")]
653 MissingHeader(&'static str),
654 #[display("invalid authentication challenge")]
655 InvalidChallenge,
656 #[display("unsupported registry authentication scheme")]
657 UnsupportedAuthentication,
658 #[display("invalid registry token response")]
659 InvalidTokenResponse,
660 #[display("registry returned HTTP {_0}")]
661 HttpStatus(u16),
662 #[display("registry scratch buffer is too small")]
663 BufferTooSmall,
664 #[display("registry page size must be non-zero")]
665 InvalidPageSize,
666 #[display("HTTPS to HTTP redirect was rejected")]
667 InsecureRedirect,
668 #[display("relative redirect requires transport URL resolution")]
669 RelativeRedirectRequiresResolution,
670 #[display("invalid registry credentials")]
671 InvalidCredentials,
672}
673
674fn request_from_path<'a>(
675 buffer: &'a mut [u8],
676 path_length: usize,
677 scheme: Scheme,
678 authority: &'a str,
679 accept: &'a str,
680) -> Result<Request<'a>, RegistryError> {
681 let path = str::from_utf8(&buffer[..path_length]).map_err(|_| RegistryError::InvalidUrl)?;
682 Ok(get_request(scheme, authority, path, accept))
683}
684
685fn get_request<'a>(
686 scheme: Scheme,
687 authority: &'a str,
688 path: &'a str,
689 accept: &'a str,
690) -> Request<'a> {
691 Request {
692 method: Method::Get,
693 target: Target {
694 scheme,
695 authority,
696 path_and_query: path,
697 },
698 accept,
699 authorization: None,
700 }
701}
702
703fn validate_authority(authority: &str) -> Result<(), RegistryError> {
704 if authority.is_empty()
705 || !authority.is_ascii()
706 || authority
707 .bytes()
708 .any(|byte| byte.is_ascii_whitespace() || matches!(byte, b'/' | b'@' | b'?' | b'#'))
709 {
710 return Err(RegistryError::InvalidUrl);
711 }
712 Ok(())
713}
714
715fn copy_request<'a>(
716 buffer: &'a mut [u8],
717 scheme: Scheme,
718 authority: &'a str,
719 path: &str,
720 accept: &'a str,
721) -> Result<Request<'a>, RegistryError> {
722 let destination = buffer
723 .get_mut(..path.len())
724 .ok_or(RegistryError::BufferTooSmall)?;
725 destination.copy_from_slice(path.as_bytes());
726 request_from_path(buffer, path.len(), scheme, authority, accept)
727}
728
729fn append(buffer: &mut [u8], length: &mut usize, bytes: &[u8]) -> Result<(), RegistryError> {
730 crate::buffer::append(buffer, length, bytes).map_err(|_| RegistryError::BufferTooSmall)
731}
732
733fn parse_retry_after(value: &[u8]) -> Result<RetryAfter<'_>, RegistryError> {
734 let value = str::from_utf8(value).map_err(|_| RegistryError::InvalidHeader)?;
735 Ok(match value.trim().parse() {
736 Ok(seconds) => RetryAfter::Seconds(seconds),
737 Err(_) => RetryAfter::HttpDate(value.trim()),
738 })
739}
740
741fn quoted_parameter(value: &str) -> Result<(&str, &str), RegistryError> {
742 let value = value.trim_start();
743 if !value.starts_with('"') {
744 return Err(RegistryError::InvalidChallenge);
745 }
746 let bytes = value.as_bytes();
747 let mut index = 1;
748 while index < bytes.len() {
749 match bytes[index] {
750 b'\\' => {
751 index = index
752 .checked_add(2)
753 .ok_or(RegistryError::InvalidChallenge)?
754 }
755 b'"' => return Ok((&value[1..index], &value[index + 1..])),
756 _ => index += 1,
757 }
758 }
759 Err(RegistryError::InvalidChallenge)
760}
761
762fn unique<'a>(current: Option<&'a str>, value: &'a str) -> Result<Option<&'a str>, RegistryError> {
763 if current.is_some() {
764 Err(RegistryError::InvalidChallenge)
765 } else {
766 Ok(Some(value))
767 }
768}
769
770fn write_display(buffer: &mut [u8], value: impl fmt::Display) -> Result<usize, RegistryError> {
771 let mut writer = BufferWriter::new(buffer);
772 fmt::write(&mut writer, format_args!("{value}")).map_err(|_| RegistryError::BufferTooSmall)?;
773 Ok(writer.len())
774}
775
776#[cfg(test)]
777mod tests {
778 use super::{
779 basic_authorization, bearer_token_url, validate_redirect, AuthChallenge, Credentials,
780 Header, Redirect, RegistryError, RegistryRequestPlanner, RequestPlanner, ResponseAction,
781 ResponseHead, RetryAfter, Target, TokenResponse,
782 };
783 use crate::reference::{Repository, Scheme};
784
785 #[test]
786 fn plans_paginated_catalogs_and_tags() {
787 let registry = RegistryRequestPlanner::new("registry.example").unwrap();
788 let mut catalog_path = [0; 64];
789 assert_eq!(
790 registry
791 .catalog_page(100, &mut catalog_path)
792 .unwrap()
793 .target
794 .path_and_query,
795 "/v2/_catalog?n=100"
796 );
797 assert!(matches!(
798 registry.catalog_page(0, &mut catalog_path),
799 Err(RegistryError::InvalidPageSize)
800 ));
801
802 let repository = Repository::parse("oci://registry.example/team/image").unwrap();
803 let planner = RequestPlanner::for_repository(repository);
804 let mut tags_path = [0; 64];
805 assert_eq!(
806 planner
807 .tags_page(50, &mut tags_path)
808 .unwrap()
809 .target
810 .path_and_query,
811 "/v2/team/image/tags/list?n=50"
812 );
813 assert!(matches!(
814 planner.manifest(&mut tags_path),
815 Err(RegistryError::Reference(
816 crate::reference::ReferenceError::MissingSelector
817 ))
818 ));
819 }
820
821 #[test]
822 fn parses_bearer_challenges() {
823 let challenge = AuthChallenge::parse(
824 r#"Bearer realm="https://auth.example/token",service="registry",scope="repository:a/b:pull""#,
825 )
826 .unwrap();
827 let AuthChallenge::Bearer(challenge) = challenge else {
828 panic!("expected bearer challenge");
829 };
830 assert_eq!(challenge.realm, "https://auth.example/token");
831 assert_eq!(challenge.service, Some("registry"));
832 assert_eq!(challenge.scope, Some("repository:a/b:pull"));
833 }
834
835 #[test]
836 fn classifies_redirect_auth_and_retry_responses() {
837 let headers = [Header {
838 name: "Retry-After",
839 value: b"12",
840 }];
841 assert!(matches!(
842 ResponseHead {
843 status: 429,
844 headers: &headers
845 }
846 .classify(false),
847 Ok(ResponseAction::Retry(advice)) if advice.after == Some(RetryAfter::Seconds(12))
848 ));
849 }
850
851 #[test]
852 fn strips_auth_across_origins_and_rejects_downgrades() {
853 let current = Target::parse("https://registry.example/v2/blob").unwrap();
854 assert_eq!(
855 validate_redirect(current, "https://objects.example/blob", false).unwrap(),
856 Redirect {
857 target: Target::parse("https://objects.example/blob").unwrap(),
858 strip_authorization: true,
859 }
860 );
861 assert!(validate_redirect(current, "http://registry.example/blob", false).is_err());
862 assert_eq!(current.scheme, Scheme::Https);
863 }
864
865 #[test]
866 fn parses_token_alias_and_pagination_link() {
867 let token = TokenResponse::parse(br#"{"access_token":"abc","expires_in":60}"#).unwrap();
868 assert_eq!(token.token.as_str(), Some("abc"));
869 assert_eq!(token.expires_in, Some(60));
870 let mut authorization = [0; 32];
871 assert_eq!(
872 token.bearer_authorization(&mut authorization).unwrap(),
873 "Bearer abc"
874 );
875 let aliases =
876 TokenResponse::parse(br#"{"token":"preferred","access_token":"alias"}"#).unwrap();
877 let mut authorization = [0; 32];
878 assert_eq!(
879 aliases.bearer_authorization(&mut authorization).unwrap(),
880 "Bearer preferred"
881 );
882
883 let headers = [Header {
884 name: "Link",
885 value: br#"</v2/name/tags/list?n=2&last=b>; rel="next""#,
886 }];
887 assert_eq!(
888 ResponseHead {
889 status: 200,
890 headers: &headers
891 }
892 .next_link()
893 .unwrap(),
894 Some("/v2/name/tags/list?n=2&last=b")
895 );
896 }
897
898 #[test]
899 fn builds_bearer_urls_and_basic_credentials() {
900 let AuthChallenge::Bearer(challenge) = AuthChallenge::parse(
901 r#"Bearer realm="https://auth.example/token",service="registry.example",scope="repository:team/image:pull""#,
902 )
903 .unwrap()
904 else {
905 panic!("expected bearer challenge");
906 };
907 let mut url = [0; 192];
908 assert_eq!(
909 bearer_token_url(challenge, &mut url).unwrap(),
910 "https://auth.example/token?service=registry.example&scope=repository%3Ateam%2Fimage%3Apull"
911 );
912
913 let mut authorization = [0; 64];
914 assert_eq!(
915 basic_authorization(
916 Credentials {
917 username: "user",
918 password: "p@ss",
919 },
920 &mut authorization,
921 )
922 .unwrap(),
923 "Basic dXNlcjpwQHNz"
924 );
925 }
926}