Skip to main content

use_oci_media_type/
lib.rs

1#![forbid(unsafe_code)]
2#![doc = include_str!("../README.md")]
3
4use core::{fmt, str::FromStr};
5use std::error::Error;
6
7/// OCI image manifest media type.
8pub const IMAGE_MANIFEST: &str = "application/vnd.oci.image.manifest.v1+json";
9/// OCI image index media type.
10pub const IMAGE_INDEX: &str = "application/vnd.oci.image.index.v1+json";
11/// OCI image config media type.
12pub const IMAGE_CONFIG: &str = "application/vnd.oci.image.config.v1+json";
13/// OCI artifact manifest media type.
14pub const ARTIFACT_MANIFEST: &str = "application/vnd.oci.artifact.manifest.v1+json";
15/// OCI uncompressed tar layer media type.
16pub const LAYER_TAR: &str = "application/vnd.oci.image.layer.v1.tar";
17/// OCI gzip-compressed tar layer media type.
18pub const LAYER_TAR_GZIP: &str = "application/vnd.oci.image.layer.v1.tar+gzip";
19/// OCI zstd-compressed tar layer media type.
20pub const LAYER_TAR_ZSTD: &str = "application/vnd.oci.image.layer.v1.tar+zstd";
21
22/// Errors returned when media type text is invalid.
23#[derive(Clone, Copy, Debug, Eq, PartialEq)]
24pub enum MediaTypeError {
25    Empty,
26    MissingSlash,
27    InvalidCharacter,
28}
29
30impl fmt::Display for MediaTypeError {
31    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
32        match self {
33            Self::Empty => formatter.write_str("OCI media type cannot be empty"),
34            Self::MissingSlash => formatter.write_str("OCI media type must contain '/'"),
35            Self::InvalidCharacter => {
36                formatter.write_str("OCI media type contains invalid characters")
37            },
38        }
39    }
40}
41
42impl Error for MediaTypeError {}
43
44/// Known OCI media type labels.
45#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
46pub enum KnownMediaType {
47    ImageManifest,
48    ImageIndex,
49    ImageConfig,
50    ArtifactManifest,
51    LayerTar,
52    LayerTarGzip,
53    LayerTarZstd,
54}
55
56impl KnownMediaType {
57    /// Returns the stable media type string.
58    #[must_use]
59    pub const fn as_str(self) -> &'static str {
60        match self {
61            Self::ImageManifest => IMAGE_MANIFEST,
62            Self::ImageIndex => IMAGE_INDEX,
63            Self::ImageConfig => IMAGE_CONFIG,
64            Self::ArtifactManifest => ARTIFACT_MANIFEST,
65            Self::LayerTar => LAYER_TAR,
66            Self::LayerTarGzip => LAYER_TAR_GZIP,
67            Self::LayerTarZstd => LAYER_TAR_ZSTD,
68        }
69    }
70}
71
72impl fmt::Display for KnownMediaType {
73    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
74        formatter.write_str(self.as_str())
75    }
76}
77
78impl FromStr for KnownMediaType {
79    type Err = MediaTypeError;
80
81    fn from_str(value: &str) -> Result<Self, Self::Err> {
82        match value.trim() {
83            IMAGE_MANIFEST => Ok(Self::ImageManifest),
84            IMAGE_INDEX => Ok(Self::ImageIndex),
85            IMAGE_CONFIG => Ok(Self::ImageConfig),
86            ARTIFACT_MANIFEST => Ok(Self::ArtifactManifest),
87            LAYER_TAR => Ok(Self::LayerTar),
88            LAYER_TAR_GZIP => Ok(Self::LayerTarGzip),
89            LAYER_TAR_ZSTD => Ok(Self::LayerTarZstd),
90            "" => Err(MediaTypeError::Empty),
91            _ => Err(MediaTypeError::InvalidCharacter),
92        }
93    }
94}
95
96/// A known or custom OCI media type.
97#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
98pub enum OciMediaType {
99    Known(KnownMediaType),
100    Custom(String),
101}
102
103impl OciMediaType {
104    /// Creates a custom media type after conservative validation.
105    pub fn custom(value: impl AsRef<str>) -> Result<Self, MediaTypeError> {
106        validate_media_type(value.as_ref()).map(|value| Self::Custom(value.to_string()))
107    }
108
109    /// Returns the OCI image manifest media type.
110    #[must_use]
111    pub const fn image_manifest() -> Self {
112        Self::Known(KnownMediaType::ImageManifest)
113    }
114
115    /// Returns the OCI image index media type.
116    #[must_use]
117    pub const fn image_index() -> Self {
118        Self::Known(KnownMediaType::ImageIndex)
119    }
120
121    /// Returns the OCI image config media type.
122    #[must_use]
123    pub const fn image_config() -> Self {
124        Self::Known(KnownMediaType::ImageConfig)
125    }
126
127    /// Returns the OCI artifact manifest media type.
128    #[must_use]
129    pub const fn artifact_manifest() -> Self {
130        Self::Known(KnownMediaType::ArtifactManifest)
131    }
132
133    /// Returns media type text.
134    #[must_use]
135    pub fn as_str(&self) -> &str {
136        match self {
137            Self::Known(known) => known.as_str(),
138            Self::Custom(value) => value,
139        }
140    }
141
142    /// Returns true for layer media types.
143    #[must_use]
144    pub fn is_layer(&self) -> bool {
145        matches!(
146            self,
147            Self::Known(
148                KnownMediaType::LayerTar
149                    | KnownMediaType::LayerTarGzip
150                    | KnownMediaType::LayerTarZstd
151            )
152        )
153    }
154}
155
156impl AsRef<str> for OciMediaType {
157    fn as_ref(&self) -> &str {
158        self.as_str()
159    }
160}
161
162impl fmt::Display for OciMediaType {
163    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
164        formatter.write_str(self.as_str())
165    }
166}
167
168impl From<KnownMediaType> for OciMediaType {
169    fn from(value: KnownMediaType) -> Self {
170        Self::Known(value)
171    }
172}
173
174impl FromStr for OciMediaType {
175    type Err = MediaTypeError;
176
177    fn from_str(value: &str) -> Result<Self, Self::Err> {
178        let trimmed = validate_media_type(value)?;
179        KnownMediaType::from_str(trimmed).map_or_else(
180            |_| Ok(Self::Custom(trimmed.to_string())),
181            |known| Ok(Self::Known(known)),
182        )
183    }
184}
185
186impl TryFrom<&str> for OciMediaType {
187    type Error = MediaTypeError;
188
189    fn try_from(value: &str) -> Result<Self, Self::Error> {
190        Self::from_str(value)
191    }
192}
193
194fn validate_media_type(value: &str) -> Result<&str, MediaTypeError> {
195    let trimmed = value.trim();
196    if trimmed.is_empty() {
197        return Err(MediaTypeError::Empty);
198    }
199    if !trimmed.contains('/') {
200        return Err(MediaTypeError::MissingSlash);
201    }
202    if trimmed
203        .bytes()
204        .any(|byte| byte.is_ascii_control() || byte.is_ascii_whitespace())
205    {
206        return Err(MediaTypeError::InvalidCharacter);
207    }
208    Ok(trimmed)
209}
210
211#[cfg(test)]
212mod tests {
213    use super::{KnownMediaType, MediaTypeError, OciMediaType};
214
215    #[test]
216    fn parses_known_and_custom_media_types() -> Result<(), Box<dyn std::error::Error>> {
217        let manifest: OciMediaType = "application/vnd.oci.image.manifest.v1+json".parse()?;
218        let custom: OciMediaType = "application/vnd.example.artifact.v1+json".parse()?;
219
220        assert_eq!(manifest, OciMediaType::Known(KnownMediaType::ImageManifest));
221        assert_eq!(custom.as_str(), "application/vnd.example.artifact.v1+json");
222        assert_eq!(OciMediaType::image_index().to_string(), super::IMAGE_INDEX);
223        assert_eq!(
224            "plain".parse::<OciMediaType>(),
225            Err(MediaTypeError::MissingSlash)
226        );
227        Ok(())
228    }
229}