Skip to main content

composefs_storage/
image.rs

1//! Image reading and manifest parsing.
2//!
3//! This module provides access to OCI image manifests and metadata stored in
4//! the `overlay-images/` directory. All operations use fd-relative access via
5//! cap-std Dir handles.
6//!
7//! # Overview
8//!
9//! The [`Image`] struct represents a container image stored in the overlay driver.
10//! It provides access to:
11//! - OCI image manifests ([`oci_spec::image::ImageManifest`])
12//! - OCI image configurations ([`oci_spec::image::ImageConfiguration`])
13//! - Layer information (diff_ids that map to storage layer IDs)
14//! - Additional metadata stored in base64-encoded files
15//!
16//! # Image Directory Structure
17//!
18//! Each image is stored in `overlay-images/<image-id>/`:
19//! ```text
20//! overlay-images/<image-id>/
21//! +-- manifest              # OCI image manifest (JSON)
22//! +-- =<base64-key>         # Additional metadata files
23//! ```
24
25use base64::{Engine, engine::general_purpose::STANDARD};
26use cap_std::fs::Dir;
27use oci_spec::image::{ImageConfiguration, ImageManifest};
28use std::io::Read;
29
30use crate::error::{Result, StorageError};
31use crate::storage::Storage;
32
33/// Filename for OCI image manifest in the image directory.
34const MANIFEST_FILENAME: &str = "manifest";
35
36/// Represents an OCI image with its metadata and manifest.
37#[derive(Debug)]
38pub struct Image {
39    /// Image ID (typically a 64-character hex digest).
40    id: String,
41
42    /// Directory handle for overlay-images/\<image-id\>/.
43    image_dir: Dir,
44}
45
46impl Image {
47    /// Open an image by ID using fd-relative operations.
48    ///
49    /// The ID can be provided with or without a `sha256:` prefix - the prefix
50    /// will be stripped if present, since containers-storage directories use
51    /// just the hex digest.
52    ///
53    /// # Errors
54    ///
55    /// Returns an error if the image directory doesn't exist or cannot be opened.
56    pub fn open(storage: &Storage, id: &str) -> Result<Self> {
57        // Strip the sha256: prefix if present - containers-storage directories
58        // use just the hex digest, but image IDs from podman (e.g. via --iidfile)
59        // include the prefix. See https://github.com/containers/skopeo/issues/2750
60        let id = id.strip_prefix("sha256:").unwrap_or(id);
61
62        // Open overlay-images directory from storage root
63        let images_dir = storage.root_dir().open_dir("overlay-images")?;
64
65        // Open specific image directory
66        let image_dir = images_dir
67            .open_dir(id)
68            .map_err(|_| StorageError::ImageNotFound(id.to_string()))?;
69
70        Ok(Self {
71            id: id.to_string(),
72            image_dir,
73        })
74    }
75
76    /// Get the image ID.
77    pub fn id(&self) -> &str {
78        &self.id
79    }
80
81    /// Read the raw manifest JSON bytes.
82    ///
83    /// Returns the original manifest bytes as stored on disk, preserving
84    /// whitespace and field ordering for content-addressed hashing.
85    ///
86    /// # Errors
87    ///
88    /// Returns an error if the manifest file cannot be read.
89    pub fn read_manifest_raw(&self) -> Result<Vec<u8>> {
90        let mut file = self.image_dir.open(MANIFEST_FILENAME)?;
91        let mut data = Vec::new();
92        file.read_to_end(&mut data)?;
93        Ok(data)
94    }
95
96    /// Read and parse the image manifest.
97    ///
98    /// The manifest is stored as a JSON file named "manifest" in the image directory.
99    ///
100    /// # Errors
101    ///
102    /// Returns an error if the manifest file cannot be read or parsed.
103    pub fn manifest(&self) -> Result<ImageManifest> {
104        let file = self.image_dir.open(MANIFEST_FILENAME)?;
105        serde_json::from_reader(file)
106            .map_err(|e| StorageError::InvalidStorage(format!("Invalid manifest JSON: {}", e)))
107    }
108
109    /// Read and parse the image configuration.
110    ///
111    /// The image config is stored with a base64-encoded key based on the image digest.
112    ///
113    /// # Errors
114    ///
115    /// Returns an error if the config file cannot be read or parsed.
116    pub fn config(&self) -> Result<ImageConfiguration> {
117        // The config is stored with key: sha256:<image-id>
118        // Base64 encode: "sha256:<id>"
119        let key = format!("sha256:{}", self.id);
120        let encoded_key = STANDARD.encode(key.as_bytes());
121
122        let config_data = self.read_metadata(&encoded_key).map_err(|e| {
123            StorageError::Io(std::io::Error::other(format!(
124                "reading config metadata ={} for image {}: {}",
125                encoded_key, self.id, e
126            )))
127        })?;
128        serde_json::from_slice(&config_data)
129            .map_err(|e| StorageError::InvalidStorage(format!("Invalid config JSON: {}", e)))
130    }
131
132    /// Get the OCI diff_ids for this image in order (base to top).
133    ///
134    /// This returns the diff_ids from the image config, which are the uncompressed
135    /// tar digests. Note that these are **not** the same as the storage layer IDs!
136    /// To get the actual storage layer IDs, use [`storage_layer_ids()`](Self::storage_layer_ids).
137    ///
138    /// # Errors
139    ///
140    /// Returns an error if the config cannot be read or parsed.
141    pub fn layers(&self) -> Result<Vec<String>> {
142        let config = self.config()?;
143
144        // Extract diff_ids from config - these are NOT the storage layer IDs
145        let diff_ids: Vec<String> = config
146            .rootfs()
147            .diff_ids()
148            .iter()
149            .map(|digest| {
150                // Remove the "sha256:" prefix if present
151                let diff_id = digest.to_string();
152                diff_id
153                    .strip_prefix("sha256:")
154                    .unwrap_or(&diff_id)
155                    .to_string()
156            })
157            .collect();
158
159        Ok(diff_ids)
160    }
161
162    /// Get the storage layer IDs for this image in order (base to top).
163    ///
164    /// Unlike [`layers()`](Self::layers) which returns OCI diff_ids, this method
165    /// returns the actual storage layer directory names by resolving diff_ids
166    /// through the `layers.json` mapping file.
167    ///
168    /// # Errors
169    ///
170    /// Returns an error if the config cannot be read, parsed, or if any layer
171    /// cannot be resolved.
172    pub fn storage_layer_ids(&self, stores: &[Storage]) -> Result<Vec<String>> {
173        let diff_ids = self.layers()?;
174
175        // Try to resolve all diff_ids from each store (batch parse of layers.json).
176        // Layers may span stores (e.g. base layers in an additional image store,
177        // new layers in the primary), so we merge results across stores.
178        let mut resolved: Vec<Option<String>> = vec![None; diff_ids.len()];
179        for store in stores {
180            if resolved.iter().all(|r| r.is_some()) {
181                break;
182            }
183            // resolve_diff_ids parses layers.json once for all diff_ids
184            if let Ok(found) = store.resolve_diff_ids(&diff_ids) {
185                for (i, id) in found.into_iter().enumerate() {
186                    if resolved[i].is_none() {
187                        resolved[i] = id;
188                    }
189                }
190            }
191        }
192
193        resolved
194            .into_iter()
195            .enumerate()
196            .map(|(i, opt)| opt.ok_or_else(|| StorageError::LayerNotFound(diff_ids[i].clone())))
197            .collect()
198    }
199
200    /// Read additional metadata files.
201    ///
202    /// Metadata files are stored with base64-encoded keys as filenames,
203    /// prefixed with '='.
204    ///
205    /// # Errors
206    ///
207    /// Returns an error if the metadata file doesn't exist or cannot be read.
208    pub fn read_metadata(&self, key: &str) -> Result<Vec<u8>> {
209        let filename = format!("={}", key);
210        let mut file = self.image_dir.open(&filename)?;
211        let mut data = Vec::new();
212        file.read_to_end(&mut data)?;
213        Ok(data)
214    }
215
216    /// Get a reference to the image directory handle.
217    pub fn image_dir(&self) -> &Dir {
218        &self.image_dir
219    }
220
221    /// Get the repository names/tags for this image.
222    ///
223    /// Reads from the `overlay-images/images.json` index file to find the
224    /// names associated with this image.
225    ///
226    /// # Errors
227    ///
228    /// Returns an error if the images.json file cannot be read or parsed.
229    pub fn names(&self, storage: &Storage) -> Result<Vec<String>> {
230        let images_dir = storage.root_dir().open_dir("overlay-images")?;
231        let mut file = images_dir.open("images.json")?;
232        let mut contents = String::new();
233        file.read_to_string(&mut contents)?;
234
235        let entries: Vec<ImageJsonEntry> = serde_json::from_str(&contents)
236            .map_err(|e| StorageError::InvalidStorage(format!("Invalid images.json: {}", e)))?;
237
238        for entry in entries {
239            if entry.id == self.id {
240                return Ok(entry.names.unwrap_or_default());
241            }
242        }
243
244        // Image not found in images.json - return empty names
245        Ok(Vec::new())
246    }
247}
248
249/// Entry in images.json for image name lookups.
250#[derive(Debug, serde::Deserialize)]
251pub(crate) struct ImageJsonEntry {
252    pub(crate) id: String,
253    pub(crate) names: Option<Vec<String>>,
254}
255
256#[cfg(test)]
257mod tests {
258    use super::*;
259
260    #[test]
261    fn test_manifest_parsing() {
262        let manifest_json = r#"{
263            "schemaVersion": 2,
264            "mediaType": "application/vnd.oci.image.manifest.v1+json",
265            "config": {
266                "mediaType": "application/vnd.oci.image.config.v1+json",
267                "digest": "sha256:0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef",
268                "size": 1234
269            },
270            "layers": [
271                {
272                    "mediaType": "application/vnd.oci.image.layer.v1.tar+gzip",
273                    "digest": "sha256:1111111111111111111111111111111111111111111111111111111111111111",
274                    "size": 5678
275                },
276                {
277                    "mediaType": "application/vnd.oci.image.layer.v1.tar+gzip",
278                    "digest": "sha256:2222222222222222222222222222222222222222222222222222222222222222",
279                    "size": 9012
280                }
281            ]
282        }"#;
283
284        let manifest: ImageManifest = serde_json::from_str(manifest_json).unwrap();
285        assert_eq!(manifest.schema_version(), 2);
286        assert_eq!(manifest.layers().len(), 2);
287    }
288}