oci_spec/image/index.rs
1use super::{Descriptor, MediaType};
2use crate::{
3 error::{OciSpecError, Result},
4 from_file, from_reader, to_file, to_string, to_writer,
5};
6use derive_builder::Builder;
7use getset::{CopyGetters, Getters, Setters};
8use serde::{Deserialize, Serialize};
9use std::{
10 collections::HashMap,
11 fmt::Display,
12 io::{Read, Write},
13 path::Path,
14};
15
16/// The expected schema version; equals 2 for compatibility with older versions of Docker.
17pub const SCHEMA_VERSION: u32 = 2;
18
19#[derive(
20 Builder, Clone, CopyGetters, Debug, Deserialize, Eq, Getters, Setters, PartialEq, Serialize,
21)]
22#[serde(rename_all = "camelCase")]
23#[builder(
24 pattern = "owned",
25 setter(into, strip_option),
26 build_fn(error = "OciSpecError")
27)]
28/// The image index is a higher-level manifest which points to specific
29/// image manifests, ideal for one or more platforms. While the use of
30/// an image index is OPTIONAL for image providers, image consumers
31/// SHOULD be prepared to process them.
32pub struct ImageIndex {
33 /// This REQUIRED property specifies the image manifest schema version.
34 /// For this version of the specification, this MUST be 2 to ensure
35 /// backward compatibility with older versions of Docker. The
36 /// value of this field will not change. This field MAY be
37 /// removed in a future version of the specification.
38 #[getset(get_copy = "pub", set = "pub")]
39 schema_version: u32,
40 /// This property is reserved for use, to maintain compatibility. When
41 /// used, this field contains the media type of this document,
42 /// which differs from the descriptor use of mediaType.
43 #[serde(skip_serializing_if = "Option::is_none")]
44 #[getset(get = "pub", set = "pub")]
45 #[builder(default)]
46 media_type: Option<MediaType>,
47 /// This OPTIONAL property contains the type of an artifact when the manifest is used for an
48 /// artifact. If defined, the value MUST comply with RFC 6838, including the naming
49 /// requirements in its section 4.2, and MAY be registered with IANA.
50 #[serde(skip_serializing_if = "Option::is_none")]
51 #[getset(get = "pub", set = "pub")]
52 #[builder(default)]
53 artifact_type: Option<MediaType>,
54 /// This REQUIRED property contains a list of manifests for specific
55 /// platforms. While this property MUST be present, the size of
56 /// the array MAY be zero.
57 #[getset(get_mut = "pub", get = "pub", set = "pub")]
58 manifests: Vec<Descriptor>,
59 /// This OPTIONAL property specifies a descriptor of another manifest. This value, used by the
60 /// referrers API, indicates a relationship to the specified manifest.
61 #[serde(skip_serializing_if = "Option::is_none")]
62 #[getset(get = "pub", set = "pub")]
63 #[builder(default)]
64 subject: Option<Descriptor>,
65 /// This OPTIONAL property contains arbitrary metadata for the image
66 /// index. This OPTIONAL property MUST use the annotation rules.
67 #[serde(skip_serializing_if = "Option::is_none")]
68 #[getset(get_mut = "pub", get = "pub", set = "pub")]
69 #[builder(default)]
70 annotations: Option<HashMap<String, String>>,
71}
72
73impl ImageIndex {
74 /// Attempts to load an image index from a file.
75 /// # Errors
76 /// This function will return an [OciSpecError::Io](crate::OciSpecError::Io)
77 /// if the file does not exist or an
78 /// [OciSpecError::SerDe](crate::OciSpecError::SerDe) if the image index
79 /// cannot be deserialized.
80 /// # Example
81 /// ``` no_run
82 /// use oci_spec::image::ImageIndex;
83 ///
84 /// let image_index = ImageIndex::from_file("index.json").unwrap();
85 /// ```
86 pub fn from_file<P: AsRef<Path>>(path: P) -> Result<ImageIndex> {
87 from_file(path)
88 }
89
90 /// Attempts to load an image index from a stream.
91 /// # Errors
92 /// This function will return an [OciSpecError::SerDe](crate::OciSpecError::SerDe)
93 /// if the index cannot be deserialized.
94 /// # Example
95 /// ``` no_run
96 /// use oci_spec::image::ImageIndex;
97 /// use std::fs::File;
98 ///
99 /// let reader = File::open("index.json").unwrap();
100 /// let image_index = ImageIndex::from_reader(reader).unwrap();
101 /// ```
102 pub fn from_reader<R: Read>(reader: R) -> Result<ImageIndex> {
103 from_reader(reader)
104 }
105
106 /// Attempts to write an image index to a file as JSON. If the file already exists, it
107 /// will be overwritten.
108 /// # Errors
109 /// This function will return an [OciSpecError::SerDe](crate::OciSpecError::SerDe) if
110 /// the image index cannot be serialized.
111 /// # Example
112 /// ``` no_run
113 /// use oci_spec::image::ImageIndex;
114 ///
115 /// let image_index = ImageIndex::from_file("index.json").unwrap();
116 /// image_index.to_file("my-index.json").unwrap();
117 /// ```
118 pub fn to_file<P: AsRef<Path>>(&self, path: P) -> Result<()> {
119 to_file(&self, path, false)
120 }
121
122 /// Attempts to write an image index to a file as pretty printed JSON. If the file
123 /// already exists, it will be overwritten.
124 /// # Errors
125 /// This function will return an [OciSpecError::SerDe](crate::OciSpecError::SerDe) if
126 /// the image index cannot be serialized.
127 /// # Example
128 /// ``` no_run
129 /// use oci_spec::image::ImageIndex;
130 ///
131 /// let image_index = ImageIndex::from_file("index.json").unwrap();
132 /// image_index.to_file_pretty("my-index.json").unwrap();
133 /// ```
134 pub fn to_file_pretty<P: AsRef<Path>>(&self, path: P) -> Result<()> {
135 to_file(&self, path, true)
136 }
137
138 /// Attempts to write an image index to a stream as JSON.
139 /// # Errors
140 /// This function will return an [OciSpecError::SerDe](crate::OciSpecError::SerDe) if
141 /// the image index cannot be serialized.
142 /// # Example
143 /// ``` no_run
144 /// use oci_spec::image::ImageIndex;
145 ///
146 /// let image_index = ImageIndex::from_file("index.json").unwrap();
147 /// let mut writer = Vec::new();
148 /// image_index.to_writer(&mut writer);
149 /// ```
150 pub fn to_writer<W: Write>(&self, writer: &mut W) -> Result<()> {
151 to_writer(&self, writer, false)
152 }
153
154 /// Attempts to write an image index to a stream as pretty printed JSON.
155 /// # Errors
156 /// This function will return an [OciSpecError::SerDe](crate::OciSpecError::SerDe) if
157 /// the image index cannot be serialized.
158 /// # Example
159 /// ``` no_run
160 /// use oci_spec::image::ImageIndex;
161 ///
162 /// let image_index = ImageIndex::from_file("index.json").unwrap();
163 /// let mut writer = Vec::new();
164 /// image_index.to_writer_pretty(&mut writer);
165 /// ```
166 pub fn to_writer_pretty<W: Write>(&self, writer: &mut W) -> Result<()> {
167 to_writer(&self, writer, true)
168 }
169
170 /// Attempts to write an image index to a string as JSON.
171 /// # Errors
172 /// This function will return an [OciSpecError::SerDe](crate::OciSpecError::SerDe) if
173 /// the image configuration cannot be serialized.
174 /// # Example
175 /// ``` no_run
176 /// use oci_spec::image::ImageIndex;
177 ///
178 /// let image_index = ImageIndex::from_file("index.json").unwrap();
179 /// let json_str = image_index.to_string().unwrap();
180 /// ```
181 pub fn to_string(&self) -> Result<String> {
182 to_string(&self, false)
183 }
184
185 /// Attempts to write an image index to a string as pretty printed JSON.
186 /// # Errors
187 /// This function will return an [OciSpecError::SerDe](crate::OciSpecError::SerDe) if
188 /// the image configuration cannot be serialized.
189 /// # Example
190 /// ``` no_run
191 /// use oci_spec::image::ImageIndex;
192 ///
193 /// let image_index = ImageIndex::from_file("index.json").unwrap();
194 /// let json_str = image_index.to_string_pretty().unwrap();
195 /// ```
196 pub fn to_string_pretty(&self) -> Result<String> {
197 to_string(&self, true)
198 }
199}
200
201impl Default for ImageIndex {
202 fn default() -> Self {
203 Self {
204 schema_version: SCHEMA_VERSION,
205 media_type: Default::default(),
206 manifests: Default::default(),
207 annotations: Default::default(),
208 artifact_type: Default::default(),
209 subject: Default::default(),
210 }
211 }
212}
213
214/// This ToString trait is automatically implemented for any type which implements the Display trait.
215/// As such, ToString shouldn’t be implemented directly: Display should be implemented instead,
216/// and you get the ToString implementation for free.
217impl Display for ImageIndex {
218 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
219 // Serde seralization never fails since this is
220 // a combination of String and enums.
221 write!(
222 f,
223 "{}",
224 self.to_string_pretty()
225 .expect("ImageIndex to JSON convertion failed")
226 )
227 }
228}
229
230#[cfg(test)]
231mod tests {
232 use std::str::FromStr;
233 use std::{fs, path::PathBuf};
234
235 use super::*;
236 use crate::image::{Arch, Os, Sha256Digest};
237 use crate::image::{DescriptorBuilder, PlatformBuilder};
238
239 fn create_index() -> ImageIndex {
240 let ppc_manifest = DescriptorBuilder::default()
241 .media_type(MediaType::ImageManifest)
242 .digest(
243 Sha256Digest::from_str(
244 "e692418e4cbaf90ca69d05a66403747baa33ee08806650b51fab815ad7fc331f",
245 )
246 .unwrap(),
247 )
248 .size(7143u64)
249 .platform(
250 PlatformBuilder::default()
251 .architecture(Arch::PowerPC64le)
252 .os(Os::Linux)
253 .build()
254 .expect("build ppc64le platform"),
255 )
256 .build()
257 .expect("build ppc manifest descriptor");
258
259 let amd64_manifest = DescriptorBuilder::default()
260 .media_type(MediaType::ImageManifest)
261 .digest(
262 Sha256Digest::from_str(
263 "5b0bcabd1ed22e9fb1310cf6c2dec7cdef19f0ad69efa1f392e94a4333501270",
264 )
265 .unwrap(),
266 )
267 .size(7682u64)
268 .platform(
269 PlatformBuilder::default()
270 .architecture(Arch::Amd64)
271 .os(Os::Linux)
272 .build()
273 .expect("build amd64 platform"),
274 )
275 .build()
276 .expect("build amd64 manifest descriptor");
277
278 ImageIndexBuilder::default()
279 .schema_version(SCHEMA_VERSION)
280 .manifests(vec![ppc_manifest, amd64_manifest])
281 .build()
282 .expect("build image index")
283 }
284
285 fn get_index_path() -> PathBuf {
286 PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("test/data/index.json")
287 }
288
289 #[test]
290 fn load_index_from_file() {
291 // arrange
292 let index_path = get_index_path();
293
294 // act
295 let actual = ImageIndex::from_file(index_path).expect("from file");
296
297 // assert
298 let expected = create_index();
299 assert_eq!(actual, expected);
300 }
301
302 #[test]
303 fn load_index_from_reader() {
304 // arrange
305 let reader = fs::read(get_index_path()).expect("read index");
306
307 // act
308 let actual = ImageIndex::from_reader(&*reader).expect("from reader");
309
310 // assert
311 let expected = create_index();
312 assert_eq!(actual, expected);
313 }
314
315 #[test]
316 fn save_index_to_file() {
317 // arrange
318 let tmp = std::env::temp_dir().join("save_index_to_file");
319 fs::create_dir_all(&tmp).expect("create test directory");
320 let index = create_index();
321 let index_path = tmp.join("index.json");
322
323 // act
324 index
325 .to_file_pretty(&index_path)
326 .expect("write index to file");
327
328 // assert
329 let actual = fs::read_to_string(index_path).expect("read actual");
330 let expected = fs::read_to_string(get_index_path()).expect("read expected");
331 assert_eq!(actual, expected);
332 }
333
334 #[test]
335 fn save_index_to_writer() {
336 // arrange
337 let mut actual = Vec::new();
338 let index = create_index();
339
340 // act
341 index.to_writer_pretty(&mut actual).expect("to writer");
342
343 // assert
344 let expected = fs::read(get_index_path()).expect("read expected");
345 assert_eq!(actual, expected);
346 }
347
348 #[test]
349 fn save_index_to_string() {
350 // arrange
351 let index = create_index();
352
353 // act
354 let actual = index.to_string_pretty().expect("to string");
355
356 // assert
357 let expected = fs::read_to_string(get_index_path()).expect("read expected");
358 assert_eq!(actual, expected);
359 }
360}