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#[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#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
34pub struct ImageName(String);
35
36impl ImageName {
37 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 #[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#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
81pub struct ImageId(OciDigest);
82
83impl ImageId {
84 #[must_use]
86 pub const fn new(digest: OciDigest) -> Self {
87 Self(digest)
88 }
89
90 #[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#[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 #[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#[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 #[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 #[must_use]
166 pub fn with_reference(mut self, reference: ImageReference) -> Self {
167 self.reference = Some(reference);
168 self
169 }
170
171 #[must_use]
173 pub fn with_id(mut self, id: ImageId) -> Self {
174 self.id = Some(id);
175 self
176 }
177
178 #[must_use]
180 pub const fn with_kind(mut self, kind: ImageKind) -> Self {
181 self.kind = kind;
182 self
183 }
184
185 #[must_use]
187 pub fn with_descriptor(mut self, descriptor: OciDescriptor) -> Self {
188 self.descriptors.push(descriptor);
189 self
190 }
191
192 #[must_use]
194 pub fn with_platform(mut self, platform: OciPlatform) -> Self {
195 self.platforms.push(platform);
196 self
197 }
198
199 #[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 #[must_use]
208 pub fn with_annotation(mut self, annotation: Annotation) -> Self {
209 self.annotations.push(annotation);
210 self
211 }
212
213 #[must_use]
215 pub const fn name(&self) -> &ImageName {
216 &self.name
217 }
218
219 #[must_use]
221 pub const fn reference(&self) -> Option<&ImageReference> {
222 self.reference.as_ref()
223 }
224
225 #[must_use]
227 pub const fn kind(&self) -> ImageKind {
228 self.kind
229 }
230
231 #[must_use]
233 pub fn descriptors(&self) -> &[OciDescriptor] {
234 &self.descriptors
235 }
236
237 #[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}