#![forbid(unsafe_code)]
#![doc = include_str!("../README.md")]
use core::{fmt, str::FromStr};
use std::error::Error;
pub const IMAGE_MANIFEST: &str = "application/vnd.oci.image.manifest.v1+json";
pub const IMAGE_INDEX: &str = "application/vnd.oci.image.index.v1+json";
pub const IMAGE_CONFIG: &str = "application/vnd.oci.image.config.v1+json";
pub const ARTIFACT_MANIFEST: &str = "application/vnd.oci.artifact.manifest.v1+json";
pub const LAYER_TAR: &str = "application/vnd.oci.image.layer.v1.tar";
pub const LAYER_TAR_GZIP: &str = "application/vnd.oci.image.layer.v1.tar+gzip";
pub const LAYER_TAR_ZSTD: &str = "application/vnd.oci.image.layer.v1.tar+zstd";
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum MediaTypeError {
Empty,
MissingSlash,
InvalidCharacter,
}
impl fmt::Display for MediaTypeError {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Empty => formatter.write_str("OCI media type cannot be empty"),
Self::MissingSlash => formatter.write_str("OCI media type must contain '/'"),
Self::InvalidCharacter => {
formatter.write_str("OCI media type contains invalid characters")
},
}
}
}
impl Error for MediaTypeError {}
#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
pub enum KnownMediaType {
ImageManifest,
ImageIndex,
ImageConfig,
ArtifactManifest,
LayerTar,
LayerTarGzip,
LayerTarZstd,
}
impl KnownMediaType {
#[must_use]
pub const fn as_str(self) -> &'static str {
match self {
Self::ImageManifest => IMAGE_MANIFEST,
Self::ImageIndex => IMAGE_INDEX,
Self::ImageConfig => IMAGE_CONFIG,
Self::ArtifactManifest => ARTIFACT_MANIFEST,
Self::LayerTar => LAYER_TAR,
Self::LayerTarGzip => LAYER_TAR_GZIP,
Self::LayerTarZstd => LAYER_TAR_ZSTD,
}
}
}
impl fmt::Display for KnownMediaType {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter.write_str(self.as_str())
}
}
impl FromStr for KnownMediaType {
type Err = MediaTypeError;
fn from_str(value: &str) -> Result<Self, Self::Err> {
match value.trim() {
IMAGE_MANIFEST => Ok(Self::ImageManifest),
IMAGE_INDEX => Ok(Self::ImageIndex),
IMAGE_CONFIG => Ok(Self::ImageConfig),
ARTIFACT_MANIFEST => Ok(Self::ArtifactManifest),
LAYER_TAR => Ok(Self::LayerTar),
LAYER_TAR_GZIP => Ok(Self::LayerTarGzip),
LAYER_TAR_ZSTD => Ok(Self::LayerTarZstd),
"" => Err(MediaTypeError::Empty),
_ => Err(MediaTypeError::InvalidCharacter),
}
}
}
#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
pub enum OciMediaType {
Known(KnownMediaType),
Custom(String),
}
impl OciMediaType {
pub fn custom(value: impl AsRef<str>) -> Result<Self, MediaTypeError> {
validate_media_type(value.as_ref()).map(|value| Self::Custom(value.to_string()))
}
#[must_use]
pub const fn image_manifest() -> Self {
Self::Known(KnownMediaType::ImageManifest)
}
#[must_use]
pub const fn image_index() -> Self {
Self::Known(KnownMediaType::ImageIndex)
}
#[must_use]
pub const fn image_config() -> Self {
Self::Known(KnownMediaType::ImageConfig)
}
#[must_use]
pub const fn artifact_manifest() -> Self {
Self::Known(KnownMediaType::ArtifactManifest)
}
#[must_use]
pub fn as_str(&self) -> &str {
match self {
Self::Known(known) => known.as_str(),
Self::Custom(value) => value,
}
}
#[must_use]
pub fn is_layer(&self) -> bool {
matches!(
self,
Self::Known(
KnownMediaType::LayerTar
| KnownMediaType::LayerTarGzip
| KnownMediaType::LayerTarZstd
)
)
}
}
impl AsRef<str> for OciMediaType {
fn as_ref(&self) -> &str {
self.as_str()
}
}
impl fmt::Display for OciMediaType {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter.write_str(self.as_str())
}
}
impl From<KnownMediaType> for OciMediaType {
fn from(value: KnownMediaType) -> Self {
Self::Known(value)
}
}
impl FromStr for OciMediaType {
type Err = MediaTypeError;
fn from_str(value: &str) -> Result<Self, Self::Err> {
let trimmed = validate_media_type(value)?;
KnownMediaType::from_str(trimmed).map_or_else(
|_| Ok(Self::Custom(trimmed.to_string())),
|known| Ok(Self::Known(known)),
)
}
}
impl TryFrom<&str> for OciMediaType {
type Error = MediaTypeError;
fn try_from(value: &str) -> Result<Self, Self::Error> {
Self::from_str(value)
}
}
fn validate_media_type(value: &str) -> Result<&str, MediaTypeError> {
let trimmed = value.trim();
if trimmed.is_empty() {
return Err(MediaTypeError::Empty);
}
if !trimmed.contains('/') {
return Err(MediaTypeError::MissingSlash);
}
if trimmed
.bytes()
.any(|byte| byte.is_ascii_control() || byte.is_ascii_whitespace())
{
return Err(MediaTypeError::InvalidCharacter);
}
Ok(trimmed)
}
#[cfg(test)]
mod tests {
use super::{KnownMediaType, MediaTypeError, OciMediaType};
#[test]
fn parses_known_and_custom_media_types() -> Result<(), Box<dyn std::error::Error>> {
let manifest: OciMediaType = "application/vnd.oci.image.manifest.v1+json".parse()?;
let custom: OciMediaType = "application/vnd.example.artifact.v1+json".parse()?;
assert_eq!(manifest, OciMediaType::Known(KnownMediaType::ImageManifest));
assert_eq!(custom.as_str(), "application/vnd.example.artifact.v1+json");
assert_eq!(OciMediaType::image_index().to_string(), super::IMAGE_INDEX);
assert_eq!(
"plain".parse::<OciMediaType>(),
Err(MediaTypeError::MissingSlash)
);
Ok(())
}
}