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}