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}