Skip to main content

use_oci_image/
lib.rs

1#![forbid(unsafe_code)]
2#![doc = include_str!("../README.md")]
3
4use core::{fmt, str::FromStr};
5use std::error::Error;
6
7use use_oci_annotation::Annotation;
8use use_oci_descriptor::OciDescriptor;
9use use_oci_digest::OciDigest;
10use use_oci_media_type::OciMediaType;
11use use_oci_platform::OciPlatform;
12use use_oci_reference::ImageReference;
13
14/// Errors returned when image metadata is invalid.
15#[derive(Clone, Copy, Debug, Eq, PartialEq)]
16pub enum ImageError {
17    Empty,
18    InvalidName,
19}
20
21impl fmt::Display for ImageError {
22    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
23        match self {
24            Self::Empty => formatter.write_str("OCI image value cannot be empty"),
25            Self::InvalidName => formatter.write_str("invalid OCI image name"),
26        }
27    }
28}
29
30impl Error for ImageError {}
31
32/// A lightweight image name label.
33#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
34pub struct ImageName(String);
35
36impl ImageName {
37    /// Creates an image name label.
38    pub fn new(value: impl AsRef<str>) -> Result<Self, ImageError> {
39        let trimmed = value.as_ref().trim().to_ascii_lowercase();
40        if trimmed.is_empty() {
41            return Err(ImageError::Empty);
42        }
43        if trimmed.chars().any(char::is_whitespace)
44            || trimmed.contains('@')
45            || trimmed.contains(':')
46        {
47            return Err(ImageError::InvalidName);
48        }
49        Ok(Self(trimmed))
50    }
51
52    /// Returns the image name text.
53    #[must_use]
54    pub fn as_str(&self) -> &str {
55        &self.0
56    }
57}
58
59impl AsRef<str> for ImageName {
60    fn as_ref(&self) -> &str {
61        self.as_str()
62    }
63}
64
65impl fmt::Display for ImageName {
66    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
67        formatter.write_str(self.as_str())
68    }
69}
70
71impl FromStr for ImageName {
72    type Err = ImageError;
73
74    fn from_str(value: &str) -> Result<Self, Self::Err> {
75        Self::new(value)
76    }
77}
78
79/// OCI image ID metadata, represented by a digest.
80#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
81pub struct ImageId(OciDigest);
82
83impl ImageId {
84    /// Creates an image ID from a digest.
85    #[must_use]
86    pub const fn new(digest: OciDigest) -> Self {
87        Self(digest)
88    }
89
90    /// Returns the digest.
91    #[must_use]
92    pub const fn digest(&self) -> &OciDigest {
93        &self.0
94    }
95}
96
97impl fmt::Display for ImageId {
98    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
99        self.0.fmt(formatter)
100    }
101}
102
103/// OCI image kind labels.
104#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
105pub enum ImageKind {
106    Image,
107    Artifact,
108    Unknown,
109}
110
111impl Default for ImageKind {
112    fn default() -> Self {
113        Self::Image
114    }
115}
116
117impl ImageKind {
118    /// Returns the stable image kind label.
119    #[must_use]
120    pub const fn as_str(self) -> &'static str {
121        match self {
122            Self::Image => "image",
123            Self::Artifact => "artifact",
124            Self::Unknown => "unknown",
125        }
126    }
127}
128
129impl fmt::Display for ImageKind {
130    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
131        formatter.write_str(self.as_str())
132    }
133}
134
135/// OCI image metadata composed from focused primitive crates.
136#[derive(Clone, Debug, Eq, PartialEq)]
137pub struct ImageMetadata {
138    name: ImageName,
139    reference: Option<ImageReference>,
140    id: Option<ImageId>,
141    kind: ImageKind,
142    descriptors: Vec<OciDescriptor>,
143    platforms: Vec<OciPlatform>,
144    media_types: Vec<OciMediaType>,
145    annotations: Vec<Annotation>,
146}
147
148impl ImageMetadata {
149    /// Creates image metadata from a name.
150    #[must_use]
151    pub fn new(name: ImageName) -> Self {
152        Self {
153            name,
154            reference: None,
155            id: None,
156            kind: ImageKind::Image,
157            descriptors: Vec::new(),
158            platforms: Vec::new(),
159            media_types: Vec::new(),
160            annotations: Vec::new(),
161        }
162    }
163
164    /// Adds an image reference.
165    #[must_use]
166    pub fn with_reference(mut self, reference: ImageReference) -> Self {
167        self.reference = Some(reference);
168        self
169    }
170
171    /// Adds an image ID.
172    #[must_use]
173    pub fn with_id(mut self, id: ImageId) -> Self {
174        self.id = Some(id);
175        self
176    }
177
178    /// Adds an image kind.
179    #[must_use]
180    pub const fn with_kind(mut self, kind: ImageKind) -> Self {
181        self.kind = kind;
182        self
183    }
184
185    /// Adds a descriptor.
186    #[must_use]
187    pub fn with_descriptor(mut self, descriptor: OciDescriptor) -> Self {
188        self.descriptors.push(descriptor);
189        self
190    }
191
192    /// Adds platform metadata.
193    #[must_use]
194    pub fn with_platform(mut self, platform: OciPlatform) -> Self {
195        self.platforms.push(platform);
196        self
197    }
198
199    /// Adds a media type marker.
200    #[must_use]
201    pub fn with_media_type(mut self, media_type: OciMediaType) -> Self {
202        self.media_types.push(media_type);
203        self
204    }
205
206    /// Adds an annotation.
207    #[must_use]
208    pub fn with_annotation(mut self, annotation: Annotation) -> Self {
209        self.annotations.push(annotation);
210        self
211    }
212
213    /// Returns the image name.
214    #[must_use]
215    pub const fn name(&self) -> &ImageName {
216        &self.name
217    }
218
219    /// Returns the optional reference.
220    #[must_use]
221    pub const fn reference(&self) -> Option<&ImageReference> {
222        self.reference.as_ref()
223    }
224
225    /// Returns the image kind.
226    #[must_use]
227    pub const fn kind(&self) -> ImageKind {
228        self.kind
229    }
230
231    /// Returns descriptors.
232    #[must_use]
233    pub fn descriptors(&self) -> &[OciDescriptor] {
234        &self.descriptors
235    }
236
237    /// Returns annotations.
238    #[must_use]
239    pub fn annotations(&self) -> &[Annotation] {
240        &self.annotations
241    }
242}
243
244#[cfg(test)]
245mod tests {
246    use super::{ImageError, ImageId, ImageKind, ImageMetadata, ImageName};
247    use use_oci_descriptor::{DescriptorSize, OciDescriptor};
248    use use_oci_digest::OciDigest;
249    use use_oci_media_type::OciMediaType;
250
251    const SHA: &str = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa";
252
253    #[test]
254    fn composes_image_metadata() -> Result<(), Box<dyn std::error::Error>> {
255        let digest: OciDigest = format!("sha256:{SHA}").parse()?;
256        let descriptor = OciDescriptor::new(
257            OciMediaType::image_manifest(),
258            digest.clone(),
259            DescriptorSize::new(10),
260        );
261        let image = ImageMetadata::new(ImageName::new("rustuse/app")?)
262            .with_id(ImageId::new(digest))
263            .with_kind(ImageKind::Artifact)
264            .with_descriptor(descriptor);
265
266        assert_eq!(image.name().as_str(), "rustuse/app");
267        assert_eq!(image.kind(), ImageKind::Artifact);
268        assert_eq!(image.descriptors().len(), 1);
269        assert_eq!(ImageName::new("bad name"), Err(ImageError::InvalidName));
270        Ok(())
271    }
272}