Skip to main content

use_oci_descriptor/
lib.rs

1#![forbid(unsafe_code)]
2#![doc = include_str!("../README.md")]
3
4use core::fmt;
5use std::error::Error;
6
7use use_oci_annotation::Annotation;
8use use_oci_digest::OciDigest;
9use use_oci_media_type::OciMediaType;
10use use_oci_platform::OciPlatform;
11
12/// Errors returned when descriptor metadata is invalid.
13#[derive(Clone, Copy, Debug, Eq, PartialEq)]
14pub enum DescriptorError {
15    Empty,
16    InvalidUrl,
17    InvalidData,
18}
19
20impl fmt::Display for DescriptorError {
21    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
22        match self {
23            Self::Empty => formatter.write_str("OCI descriptor value cannot be empty"),
24            Self::InvalidUrl => formatter.write_str("invalid OCI descriptor URL"),
25            Self::InvalidData => formatter.write_str("invalid OCI descriptor data marker"),
26        }
27    }
28}
29
30impl Error for DescriptorError {}
31
32/// Descriptor size in bytes.
33#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
34pub struct DescriptorSize(u64);
35
36impl DescriptorSize {
37    /// Creates a descriptor size.
38    #[must_use]
39    pub const fn new(value: u64) -> Self {
40        Self(value)
41    }
42
43    /// Returns the size in bytes.
44    #[must_use]
45    pub const fn as_u64(self) -> u64 {
46        self.0
47    }
48}
49
50/// A descriptor URL string. This type does not fetch anything.
51#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
52pub struct DescriptorUrl(String);
53
54impl DescriptorUrl {
55    /// Creates a descriptor URL marker.
56    pub fn new(value: impl AsRef<str>) -> Result<Self, DescriptorError> {
57        let trimmed = value.as_ref().trim();
58        if trimmed.is_empty() {
59            return Err(DescriptorError::Empty);
60        }
61        if trimmed.chars().any(char::is_whitespace) || !trimmed.contains("://") {
62            return Err(DescriptorError::InvalidUrl);
63        }
64        Ok(Self(trimmed.to_string()))
65    }
66
67    /// Returns the URL text.
68    #[must_use]
69    pub fn as_str(&self) -> &str {
70        &self.0
71    }
72}
73
74impl fmt::Display for DescriptorUrl {
75    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
76        formatter.write_str(self.as_str())
77    }
78}
79
80/// A descriptor artifact type marker.
81#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
82pub struct ArtifactType(String);
83
84impl ArtifactType {
85    /// Creates an artifact type marker.
86    pub fn new(value: impl AsRef<str>) -> Result<Self, DescriptorError> {
87        let media_type = OciMediaType::custom(value).map_err(|_| DescriptorError::Empty)?;
88        Ok(Self(media_type.to_string()))
89    }
90
91    /// Returns the artifact type text.
92    #[must_use]
93    pub fn as_str(&self) -> &str {
94        &self.0
95    }
96}
97
98impl fmt::Display for ArtifactType {
99    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
100        formatter.write_str(self.as_str())
101    }
102}
103
104/// Embedded descriptor data marker.
105#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
106pub struct DescriptorData(String);
107
108impl DescriptorData {
109    /// Creates an embedded data marker.
110    pub fn new(value: impl AsRef<str>) -> Result<Self, DescriptorError> {
111        let value = value.as_ref();
112        if value.contains('\0') {
113            return Err(DescriptorError::InvalidData);
114        }
115        Ok(Self(value.to_string()))
116    }
117
118    /// Returns the data marker text.
119    #[must_use]
120    pub fn as_str(&self) -> &str {
121        &self.0
122    }
123}
124
125/// OCI descriptor metadata.
126#[derive(Clone, Debug, Eq, PartialEq)]
127pub struct OciDescriptor {
128    media_type: OciMediaType,
129    digest: OciDigest,
130    size: DescriptorSize,
131    urls: Vec<DescriptorUrl>,
132    annotations: Vec<Annotation>,
133    data: Option<DescriptorData>,
134    artifact_type: Option<ArtifactType>,
135    platform: Option<OciPlatform>,
136}
137
138impl OciDescriptor {
139    /// Creates descriptor metadata from required fields.
140    #[must_use]
141    pub fn new(media_type: OciMediaType, digest: OciDigest, size: DescriptorSize) -> Self {
142        Self {
143            media_type,
144            digest,
145            size,
146            urls: Vec::new(),
147            annotations: Vec::new(),
148            data: None,
149            artifact_type: None,
150            platform: None,
151        }
152    }
153
154    /// Adds a URL marker.
155    #[must_use]
156    pub fn with_url(mut self, url: DescriptorUrl) -> Self {
157        self.urls.push(url);
158        self
159    }
160
161    /// Adds an annotation.
162    #[must_use]
163    pub fn with_annotation(mut self, annotation: Annotation) -> Self {
164        self.annotations.push(annotation);
165        self
166    }
167
168    /// Adds embedded data marker text.
169    #[must_use]
170    pub fn with_data(mut self, data: DescriptorData) -> Self {
171        self.data = Some(data);
172        self
173    }
174
175    /// Adds an artifact type.
176    #[must_use]
177    pub fn with_artifact_type(mut self, artifact_type: ArtifactType) -> Self {
178        self.artifact_type = Some(artifact_type);
179        self
180    }
181
182    /// Adds a platform.
183    #[must_use]
184    pub fn with_platform(mut self, platform: OciPlatform) -> Self {
185        self.platform = Some(platform);
186        self
187    }
188
189    /// Returns the media type.
190    #[must_use]
191    pub const fn media_type(&self) -> &OciMediaType {
192        &self.media_type
193    }
194
195    /// Returns the digest.
196    #[must_use]
197    pub const fn digest(&self) -> &OciDigest {
198        &self.digest
199    }
200
201    /// Returns the size.
202    #[must_use]
203    pub const fn size(&self) -> DescriptorSize {
204        self.size
205    }
206
207    /// Returns URL markers.
208    #[must_use]
209    pub fn urls(&self) -> &[DescriptorUrl] {
210        &self.urls
211    }
212
213    /// Returns annotations.
214    #[must_use]
215    pub fn annotations(&self) -> &[Annotation] {
216        &self.annotations
217    }
218
219    /// Returns the optional platform.
220    #[must_use]
221    pub const fn platform(&self) -> Option<&OciPlatform> {
222        self.platform.as_ref()
223    }
224}
225
226#[cfg(test)]
227mod tests {
228    use super::{DescriptorSize, DescriptorUrl, OciDescriptor};
229    use use_oci_annotation::Annotation;
230    use use_oci_digest::OciDigest;
231    use use_oci_media_type::OciMediaType;
232
233    const SHA: &str = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa";
234
235    #[test]
236    fn builds_descriptor_metadata() -> Result<(), Box<dyn std::error::Error>> {
237        let digest: OciDigest = format!("sha256:{SHA}").parse()?;
238        let descriptor = OciDescriptor::new(
239            OciMediaType::image_manifest(),
240            digest,
241            DescriptorSize::new(12),
242        )
243        .with_url(DescriptorUrl::new("https://example.com/blob")?)
244        .with_annotation(Annotation::title("Example")?);
245
246        assert_eq!(descriptor.size().as_u64(), 12);
247        assert_eq!(descriptor.urls().len(), 1);
248        assert_eq!(descriptor.annotations().len(), 1);
249        Ok(())
250    }
251}