oci_unpack/reference/
mod.rs

1mod mediatype;
2mod parser;
3
4use crate::digest::Digest;
5
6pub use mediatype::MediaType;
7
8/// Errors from [`Reference::try_from`].
9#[derive(thiserror::Error, Debug)]
10pub enum ParseError {
11    #[error("Missing repository.")]
12    MissingRepository,
13
14    #[error("{0}")]
15    InvalidDigest(#[from] crate::digest::DigestError),
16}
17
18/// Reference to an image in an OCI registry.
19///
20/// The parser tries to be close to what `docker pull` does:
21///
22/// * If the reference does not include the hostname of the registry,
23///   it uses Docker Hub, and the repository namespace defaults to
24///   `library` if there is none. For example:
25///
26///   * `debian` is parsed as `registry-1.docker.io/library/debian`.
27///   * `nixos/nix` is parsed as `registry-1.docker.io/nixos/nix`.
28/// * It accepts any tag value after the last `:` character. If no tag
29///   is given, it uses `latest`.
30/// * It accepts a fixed digest (the last part after a `@` character), but
31///   only SHA256 and SHA512.
32///
33/// However, it does not try to be bug-for-bug compatible with Docker.
34///
35/// # Examples
36///
37/// ```
38/// # use oci_unpack::*;
39/// const REFERENCE: &str = "registry.example.com/foo/bar:1.23.4@sha256:123456789012345678901234567890123456789012345678901234567890ABCD";
40///
41/// let reference = Reference::try_from(REFERENCE).unwrap();
42/// assert_eq!(reference.registry, "registry.example.com");
43/// assert_eq!(reference.repository.namespace(), Some("foo"));
44/// assert_eq!(reference.repository.name(), "bar");
45/// assert_eq!(reference.tag, "1.23.4");
46///
47/// let digest = reference.digest.as_ref().unwrap();
48/// assert_eq!(digest.algorithm(), DigestAlgorithm::SHA256);
49/// assert_eq!(digest.hash_value(), "123456789012345678901234567890123456789012345678901234567890ABCD");
50/// ```
51///
52/// ```
53/// # use oci_unpack::*;
54/// let reference = Reference::try_from("debian:stable").unwrap();
55///
56/// assert_eq!(reference.repository.to_string(), "library/debian");
57/// assert_eq!(reference.tag, "stable");
58/// ```
59#[derive(Clone, Debug, PartialEq)]
60pub struct Reference<'a> {
61    /// Address of the registry server.
62    pub registry: &'a str,
63
64    /// Repository name.
65    pub repository: Repository<'a>,
66
67    /// Image tag.
68    pub tag: &'a str,
69
70    /// Manifest digest, if present.
71    pub digest: Option<Digest>,
72}
73
74/// Represents a repository name, like `library/debian`
75/// or `nixos/nix`.
76#[derive(Copy, Clone, Debug, PartialEq)]
77pub struct Repository<'a>(RepositoryInner<'a>);
78
79impl<'a> Repository<'a> {
80    pub(crate) fn components(namespace: &'a str, name: &'a str) -> Self {
81        Repository(RepositoryInner::Components(namespace, name))
82    }
83
84    pub(crate) fn full(name: &'a str) -> Self {
85        Repository(RepositoryInner::Full(name))
86    }
87
88    /// Return the name of this repository.
89    ///
90    /// # Examples
91    ///
92    /// ```
93    /// # use oci_unpack::*;
94    /// let reference = Reference::try_from("foo/bar:stable").unwrap();
95    /// assert_eq!(reference.repository.name(), "bar");
96    /// ```
97    pub fn name(&self) -> &str {
98        match self.0 {
99            RepositoryInner::Full(full) => full.split_once('/').map(|s| s.1).unwrap_or(full),
100            RepositoryInner::Components(_, name) => name,
101        }
102    }
103
104    /// Return the namespace of this repository, or `None` if
105    /// the repository does not contain a `/` character.
106    ///
107    /// # Examples
108    ///
109    /// ```
110    /// # use oci_unpack::*;
111    /// let reference = Reference::try_from("foo/bar:stable").unwrap();
112    /// assert_eq!(reference.repository.namespace(), Some("foo"));
113    /// ```
114    pub fn namespace(&self) -> Option<&str> {
115        match self.0 {
116            RepositoryInner::Full(full) => full.split_once('/').map(|s| s.0),
117            RepositoryInner::Components(ns, _) => Some(ns),
118        }
119    }
120}
121
122#[derive(Copy, Clone, Debug, PartialEq)]
123enum RepositoryInner<'a> {
124    /// Full repository name. Namespace is optional.
125    Full(&'a str),
126
127    /// Namespace and name.
128    Components(&'a str, &'a str),
129}
130
131impl std::fmt::Display for Repository<'_> {
132    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
133        match self.0 {
134            RepositoryInner::Full(full) => f.write_str(full),
135            RepositoryInner::Components(a, b) => write!(f, "{a}/{b}"),
136        }
137    }
138}
139
140impl<'a> TryFrom<&'a str> for Reference<'a> {
141    type Error = ParseError;
142
143    fn try_from(reference: &'a str) -> Result<Self, Self::Error> {
144        parser::parse(reference)
145    }
146}