use_oci_descriptor/
lib.rs1#![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#[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#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
34pub struct DescriptorSize(u64);
35
36impl DescriptorSize {
37 #[must_use]
39 pub const fn new(value: u64) -> Self {
40 Self(value)
41 }
42
43 #[must_use]
45 pub const fn as_u64(self) -> u64 {
46 self.0
47 }
48}
49
50#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
52pub struct DescriptorUrl(String);
53
54impl DescriptorUrl {
55 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 #[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#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
82pub struct ArtifactType(String);
83
84impl ArtifactType {
85 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 #[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#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
106pub struct DescriptorData(String);
107
108impl DescriptorData {
109 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 #[must_use]
120 pub fn as_str(&self) -> &str {
121 &self.0
122 }
123}
124
125#[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 #[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 #[must_use]
156 pub fn with_url(mut self, url: DescriptorUrl) -> Self {
157 self.urls.push(url);
158 self
159 }
160
161 #[must_use]
163 pub fn with_annotation(mut self, annotation: Annotation) -> Self {
164 self.annotations.push(annotation);
165 self
166 }
167
168 #[must_use]
170 pub fn with_data(mut self, data: DescriptorData) -> Self {
171 self.data = Some(data);
172 self
173 }
174
175 #[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 #[must_use]
184 pub fn with_platform(mut self, platform: OciPlatform) -> Self {
185 self.platform = Some(platform);
186 self
187 }
188
189 #[must_use]
191 pub const fn media_type(&self) -> &OciMediaType {
192 &self.media_type
193 }
194
195 #[must_use]
197 pub const fn digest(&self) -> &OciDigest {
198 &self.digest
199 }
200
201 #[must_use]
203 pub const fn size(&self) -> DescriptorSize {
204 self.size
205 }
206
207 #[must_use]
209 pub fn urls(&self) -> &[DescriptorUrl] {
210 &self.urls
211 }
212
213 #[must_use]
215 pub fn annotations(&self) -> &[Annotation] {
216 &self.annotations
217 }
218
219 #[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}