use_oci_media_type/
lib.rs1#![forbid(unsafe_code)]
2#![doc = include_str!("../README.md")]
3
4use core::{fmt, str::FromStr};
5use std::error::Error;
6
7pub const IMAGE_MANIFEST: &str = "application/vnd.oci.image.manifest.v1+json";
9pub const IMAGE_INDEX: &str = "application/vnd.oci.image.index.v1+json";
11pub const IMAGE_CONFIG: &str = "application/vnd.oci.image.config.v1+json";
13pub const ARTIFACT_MANIFEST: &str = "application/vnd.oci.artifact.manifest.v1+json";
15pub const LAYER_TAR: &str = "application/vnd.oci.image.layer.v1.tar";
17pub const LAYER_TAR_GZIP: &str = "application/vnd.oci.image.layer.v1.tar+gzip";
19pub const LAYER_TAR_ZSTD: &str = "application/vnd.oci.image.layer.v1.tar+zstd";
21
22#[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#[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 #[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#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
98pub enum OciMediaType {
99 Known(KnownMediaType),
100 Custom(String),
101}
102
103impl OciMediaType {
104 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 #[must_use]
111 pub const fn image_manifest() -> Self {
112 Self::Known(KnownMediaType::ImageManifest)
113 }
114
115 #[must_use]
117 pub const fn image_index() -> Self {
118 Self::Known(KnownMediaType::ImageIndex)
119 }
120
121 #[must_use]
123 pub const fn image_config() -> Self {
124 Self::Known(KnownMediaType::ImageConfig)
125 }
126
127 #[must_use]
129 pub const fn artifact_manifest() -> Self {
130 Self::Known(KnownMediaType::ArtifactManifest)
131 }
132
133 #[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 #[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}