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