composefs_storage/storage.rs
1//! Storage access for container overlay filesystem.
2//!
3//! This module provides the main [`Storage`] struct for accessing containers-storage
4//! overlay driver data. All file access uses cap-std for fd-relative operations,
5//! providing security against path traversal attacks and TOCTOU race conditions.
6//!
7//! # Overview
8//!
9//! The `Storage` struct is the primary entry point for interacting with container
10//! storage. It holds a capability-based directory handle to the storage root.
11//!
12//! # Storage Structure
13//!
14//! Container storage on disk follows this layout:
15//! ```text
16//! /var/lib/containers/storage/
17//! +-- overlay/ # Layer data
18//! | +-- <layer-id>/ # Individual layer directories
19//! | | +-- diff/ # Layer file contents
20//! | | +-- link # Short link ID (26 chars)
21//! | | +-- lower # Parent layer references
22//! | +-- l/ # Short link directory (symlinks)
23//! +-- overlay-layers/ # Tar-split metadata
24//! | +-- <layer-id>.tar-split.gz
25//! +-- overlay-images/ # Image metadata
26//! +-- <image-id>/
27//! +-- manifest # OCI image manifest
28//! +-- =<key> # Base64-encoded metadata files
29//! ```
30//!
31//! # Security Model
32//!
33//! All file operations are performed via [`cap_std::fs::Dir`] handles, which provide:
34//! - Protection against path traversal attacks
35//! - Prevention of TOCTOU race conditions
36//! - Guarantee that all access stays within the storage directory tree
37
38use crate::error::{Result, StorageError};
39use cap_std::ambient_authority;
40use cap_std::fs::Dir;
41use std::env;
42use std::io::Read;
43use std::path::{Path, PathBuf};
44
45/// Main storage handle providing read-only access to container storage.
46///
47/// The Storage struct holds a `Dir` handle to the storage root for fd-relative
48/// file operations.
49#[derive(Debug)]
50pub struct Storage {
51 /// Directory handle for the storage root, used for all fd-relative operations.
52 root_dir: Dir,
53}
54
55impl Storage {
56 /// Open storage at the given root path.
57 ///
58 /// This validates that the path points to a valid container storage directory
59 /// by checking for required subdirectories and the database file.
60 ///
61 /// # Errors
62 ///
63 /// Returns an error if:
64 /// - The path does not exist or is not a directory
65 /// - Required subdirectories are missing
66 /// - The database file is missing or invalid
67 pub fn open<P: AsRef<Path>>(root: P) -> Result<Self> {
68 let root_path = root.as_ref();
69
70 // Open the directory handle for fd-relative operations
71 let root_dir = Dir::open_ambient_dir(root_path, ambient_authority()).map_err(|e| {
72 if e.kind() == std::io::ErrorKind::NotFound {
73 StorageError::RootNotFound(root_path.to_path_buf())
74 } else {
75 StorageError::Io(e)
76 }
77 })?;
78
79 // Validate storage structure
80 Self::validate_storage(&root_dir)?;
81
82 Ok(Self { root_dir })
83 }
84
85 /// Discover storage root from default locations.
86 ///
87 /// Searches for container storage in the following order:
88 /// 1. `$CONTAINERS_STORAGE_ROOT` environment variable
89 /// 2. Rootless storage: `$XDG_DATA_HOME/containers/storage` or `~/.local/share/containers/storage`
90 /// 3. Root storage: `/var/lib/containers/storage`
91 ///
92 /// # Errors
93 ///
94 /// Returns an error if no valid storage location is found.
95 pub fn discover() -> Result<Self> {
96 let search_paths = Self::default_search_paths();
97
98 for path in search_paths {
99 if path.exists() {
100 match Self::open(&path) {
101 Ok(storage) => return Ok(storage),
102 Err(_) => continue,
103 }
104 }
105 }
106
107 Err(StorageError::InvalidStorage(
108 "No valid storage location found. Searched default locations.".to_string(),
109 ))
110 }
111
112 /// Discover all storage locations: the primary store plus any additional
113 /// image stores from `$STORAGE_OPTS`.
114 ///
115 /// The `containers/storage` library supports
116 /// `STORAGE_OPTS=additionalimagestore=/path` to add read-only image stores
117 /// (used by e.g. `bcvk` to expose the host's containers-storage inside a VM).
118 ///
119 /// Returns a non-empty vec with the primary store first (if it exists),
120 /// followed by any additional stores. Returns an error only if no stores
121 /// are found at all.
122 pub fn discover_all() -> Result<Vec<Self>> {
123 let mut stores = Vec::new();
124 if let Ok(primary) = Self::discover() {
125 stores.push(primary);
126 }
127 stores.extend(Self::additional_image_stores_from_env());
128 if stores.is_empty() {
129 return Err(StorageError::InvalidStorage(
130 "No valid storage location found. Searched default locations and $STORAGE_OPTS."
131 .to_string(),
132 ));
133 }
134 Ok(stores)
135 }
136
137 /// Parse `$STORAGE_OPTS` for `additionalimagestore=<path>` entries and
138 /// open any that point to valid overlay storage.
139 ///
140 /// Invalid or inaccessible paths are silently skipped.
141 fn additional_image_stores_from_env() -> Vec<Self> {
142 let opts = match env::var("STORAGE_OPTS") {
143 Ok(v) => v,
144 Err(_) => return Vec::new(),
145 };
146
147 Self::parse_additional_image_stores(&opts)
148 }
149
150 /// Parse a `STORAGE_OPTS` value for `additionalimagestore=<path>` entries
151 /// and open any that point to valid overlay storage.
152 ///
153 /// This is separated from [`additional_image_stores_from_env()`] so the
154 /// parsing logic can be tested without mutating process-global environment
155 /// variables.
156 fn parse_additional_image_stores(opts: &str) -> Vec<Self> {
157 let mut stores = Vec::new();
158 // STORAGE_OPTS is comma-separated, e.g.
159 // "additionalimagestore=/run/host-container-storage,additionalimagestore=/other"
160 for item in opts.split(',') {
161 let item = item.trim();
162 if let Some(path) = item.strip_prefix("additionalimagestore=")
163 && let Ok(s) = Self::open(path)
164 {
165 stores.push(s);
166 }
167 }
168 stores
169 }
170
171 /// Get the default search paths for storage discovery.
172 fn default_search_paths() -> Vec<PathBuf> {
173 let mut paths = Vec::new();
174
175 // 1. Check CONTAINERS_STORAGE_ROOT environment variable
176 if let Ok(root) = env::var("CONTAINERS_STORAGE_ROOT") {
177 paths.push(PathBuf::from(root));
178 }
179
180 // 2. Check rootless locations
181 if let Ok(home) = env::var("HOME") {
182 let home_path = PathBuf::from(home);
183
184 // Try XDG_DATA_HOME first
185 if let Ok(xdg_data) = env::var("XDG_DATA_HOME") {
186 paths.push(PathBuf::from(xdg_data).join("containers/storage"));
187 }
188
189 // Fallback to ~/.local/share/containers/storage
190 paths.push(home_path.join(".local/share/containers/storage"));
191 }
192
193 // 3. Check root location
194 paths.push(PathBuf::from("/var/lib/containers/storage"));
195
196 paths
197 }
198
199 /// Validate that the directory structure is a valid overlay storage.
200 fn validate_storage(root_dir: &Dir) -> Result<()> {
201 // Check for required subdirectories
202 let required_dirs = ["overlay", "overlay-layers", "overlay-images"];
203
204 for dir_name in &required_dirs {
205 match root_dir.try_exists(dir_name) {
206 Ok(exists) if !exists => {
207 return Err(StorageError::InvalidStorage(format!(
208 "Missing required directory: {}",
209 dir_name
210 )));
211 }
212 Err(e) => return Err(StorageError::Io(e)),
213 _ => {}
214 }
215 }
216
217 Ok(())
218 }
219
220 /// Create storage from an existing root directory handle.
221 ///
222 /// # Errors
223 ///
224 /// Returns an error if the directory is not a valid container storage.
225 pub fn from_root_dir(root_dir: Dir) -> Result<Self> {
226 Self::validate_storage(&root_dir)?;
227 Ok(Self { root_dir })
228 }
229
230 /// Get a reference to the root directory handle.
231 pub fn root_dir(&self) -> &Dir {
232 &self.root_dir
233 }
234
235 /// Resolve a link ID to a layer ID using fd-relative symlink reading.
236 ///
237 /// # Errors
238 ///
239 /// Returns an error if the link doesn't exist or has an invalid format.
240 pub fn resolve_link(&self, link_id: &str) -> Result<String> {
241 // Open overlay directory from storage root
242 let overlay_dir = self.root_dir.open_dir("overlay")?;
243
244 // Open link directory
245 let link_dir = overlay_dir.open_dir("l")?;
246
247 // Read symlink target using fd-relative operation
248 let target = link_dir.read_link(link_id).map_err(|e| {
249 StorageError::LinkReadError(format!("Failed to read link {}: {}", link_id, e))
250 })?;
251
252 // Extract layer ID from symlink target
253 Self::extract_layer_id_from_link(&target)
254 }
255
256 /// Extract layer ID from symlink target path.
257 ///
258 /// Target format: ../<layer-id>/diff
259 fn extract_layer_id_from_link(target: &Path) -> Result<String> {
260 // Convert to string for processing
261 let target_str = target.to_str().ok_or_else(|| {
262 StorageError::LinkReadError("Invalid UTF-8 in link target".to_string())
263 })?;
264
265 // Split by '/' and find the layer ID component
266 let components: Vec<&str> = target_str.split('/').collect();
267
268 // Expected format: ../<layer-id>/diff
269 // So we need the second-to-last component
270 if components.len() >= 2 {
271 let layer_id = components[components.len() - 2];
272 if !layer_id.is_empty() && layer_id != ".." {
273 return Ok(layer_id.to_string());
274 }
275 }
276
277 Err(StorageError::LinkReadError(format!(
278 "Invalid link target format: {}",
279 target_str
280 )))
281 }
282
283 /// List all images in storage.
284 ///
285 /// # Errors
286 ///
287 /// Returns an error if the images directory cannot be read.
288 pub fn list_images(&self) -> Result<Vec<crate::image::Image>> {
289 use crate::image::Image;
290
291 let images_dir = self.root_dir.open_dir("overlay-images")?;
292 let mut images = Vec::new();
293
294 for entry in images_dir.entries()? {
295 let entry = entry?;
296 if entry.file_type()?.is_dir() {
297 let id = entry
298 .file_name()
299 .to_str()
300 .ok_or_else(|| {
301 StorageError::InvalidStorage(
302 "Invalid UTF-8 in image directory name".to_string(),
303 )
304 })?
305 .to_string();
306 images.push(Image::open(self, &id)?);
307 }
308 }
309 Ok(images)
310 }
311
312 /// Get an image by ID.
313 ///
314 /// # Errors
315 ///
316 /// Returns [`StorageError::ImageNotFound`] if the image doesn't exist.
317 pub fn get_image(&self, id: &str) -> Result<crate::image::Image> {
318 crate::image::Image::open(self, id)
319 }
320
321 /// Get layers for an image (in order from base to top).
322 ///
323 /// # Errors
324 ///
325 /// Returns an error if any layer cannot be opened.
326 pub fn get_image_layers(
327 &self,
328 image: &crate::image::Image,
329 ) -> Result<Vec<crate::layer::Layer>> {
330 use crate::layer::Layer;
331 // image.layers() returns diff_ids, which need to be mapped to storage layer IDs.
332 // Use the batch method to parse layers.json only once.
333 let diff_ids = image.layers()?;
334 let layer_ids: Vec<String> = self
335 .resolve_diff_ids(&diff_ids)?
336 .into_iter()
337 .enumerate()
338 .map(|(i, opt)| opt.ok_or_else(|| StorageError::LayerNotFound(diff_ids[i].clone())))
339 .collect::<Result<_>>()?;
340 layer_ids
341 .iter()
342 .map(|layer_id| Layer::open(self, layer_id))
343 .collect()
344 }
345
346 /// Find an image by name.
347 ///
348 /// # Errors
349 ///
350 /// Returns [`StorageError::ImageNotFound`] if no image with the given name is found.
351 pub fn find_image_by_name(&self, name: &str) -> Result<crate::image::Image> {
352 // Read images.json from overlay-images/
353 let images_dir = self.root_dir.open_dir("overlay-images")?;
354 let mut file = images_dir.open("images.json")?;
355 let mut contents = String::new();
356 file.read_to_string(&mut contents)?;
357
358 // Parse the JSON array
359 let entries: Vec<ImageJsonEntry> = serde_json::from_str(&contents)
360 .map_err(|e| StorageError::InvalidStorage(format!("Invalid images.json: {}", e)))?;
361
362 // Search for matching name
363 for entry in &entries {
364 if let Some(names) = &entry.names {
365 for image_name in names {
366 if image_name == name {
367 return self.get_image(&entry.id);
368 }
369 }
370 }
371 }
372
373 // Try partial matching (e.g., "alpine:latest" matches "docker.io/library/alpine:latest")
374 for entry in &entries {
375 if let Some(names) = &entry.names {
376 for image_name in names {
377 // Check if name is a suffix (after removing registry/namespace prefix)
378 if let Some(prefix) = image_name.strip_suffix(name) {
379 // Verify it's a proper boundary (preceded by '/')
380 if prefix.is_empty() || prefix.ends_with('/') {
381 return self.get_image(&entry.id);
382 }
383 }
384 }
385 }
386 }
387
388 // Try matching short name without tag (e.g., "busybox" matches "docker.io/library/busybox:latest")
389 // This handles the common case of just specifying the image name
390 let name_with_tag = if name.contains(':') {
391 name.to_string()
392 } else {
393 format!("{}:latest", name)
394 };
395
396 for entry in &entries {
397 if let Some(names) = &entry.names {
398 for image_name in names {
399 // Check if image_name ends with /name:tag pattern
400 if let Some(prefix) = image_name.strip_suffix(&name_with_tag)
401 && (prefix.is_empty() || prefix.ends_with('/'))
402 {
403 return self.get_image(&entry.id);
404 }
405 }
406 }
407 }
408
409 Err(StorageError::ImageNotFound(name.to_string()))
410 }
411
412 /// Parse layers.json and return all entries.
413 ///
414 /// This is used internally to avoid re-parsing on every lookup.
415 fn read_layer_entries(&self) -> Result<Vec<LayerEntry>> {
416 let layers_dir = self.root_dir.open_dir("overlay-layers").map_err(|e| {
417 StorageError::Io(std::io::Error::new(
418 e.kind(),
419 format!("opening overlay-layers/: {e}"),
420 ))
421 })?;
422 let mut file = layers_dir.open("layers.json").map_err(|e| {
423 StorageError::Io(std::io::Error::new(
424 e.kind(),
425 format!("opening overlay-layers/layers.json: {e}"),
426 ))
427 })?;
428 let mut contents = String::new();
429 file.read_to_string(&mut contents)?;
430
431 serde_json::from_str(&contents)
432 .map_err(|e| StorageError::InvalidStorage(format!("Invalid layers.json: {}", e)))
433 }
434
435 /// Resolve multiple diff-digests to storage layer IDs in a single pass.
436 ///
437 /// Parses `layers.json` once and looks up all diff_ids, avoiding the O(N×M)
438 /// overhead of calling [`resolve_diff_id()`] in a loop.
439 ///
440 /// Returns a `Vec<Option<String>>` with the same length as `diff_digests`,
441 /// where `Some(id)` means the diff-digest was found and `None` means it was not.
442 /// This allows callers to merge results across multiple stores without
443 /// short-circuiting on the first miss.
444 ///
445 /// # Errors
446 ///
447 /// Returns an error only if `layers.json` cannot be read or parsed.
448 pub fn resolve_diff_ids(&self, diff_digests: &[String]) -> Result<Vec<Option<String>>> {
449 let entries = self.read_layer_entries()?;
450
451 // Build a map from normalized diff-digest -> layer ID
452 let mut digest_to_id = std::collections::HashMap::with_capacity(entries.len());
453 for entry in &entries {
454 if let Some(digest) = &entry.diff_digest {
455 digest_to_id.insert(digest.as_str(), entry.id.as_str());
456 }
457 }
458
459 Ok(diff_digests
460 .iter()
461 .map(|diff_digest| {
462 let normalized = if diff_digest.starts_with("sha256:") {
463 diff_digest.clone()
464 } else {
465 format!("sha256:{}", diff_digest)
466 };
467 digest_to_id
468 .get(normalized.as_str())
469 .map(|id| id.to_string())
470 })
471 .collect())
472 }
473
474 /// Resolve a diff-digest to a storage layer ID.
475 ///
476 /// # Errors
477 ///
478 /// Returns [`StorageError::LayerNotFound`] if no layer with the given diff-digest exists.
479 pub fn resolve_diff_id(&self, diff_digest: &str) -> Result<String> {
480 self.resolve_diff_ids(&[diff_digest.to_string()])?
481 .into_iter()
482 .next()
483 .flatten()
484 .ok_or_else(|| StorageError::LayerNotFound(diff_digest.to_string()))
485 }
486
487 /// Get layer metadata including size information.
488 ///
489 /// # Errors
490 ///
491 /// Returns an error if the layer is not found.
492 pub fn get_layer_metadata(&self, layer_id: &str) -> Result<LayerMetadata> {
493 let entries = self.read_layer_entries()?;
494
495 for entry in entries {
496 if entry.id == layer_id {
497 return Ok(LayerMetadata {
498 id: entry.id,
499 parent: entry.parent,
500 diff_size: entry.diff_size,
501 compressed_size: entry.compressed_size,
502 });
503 }
504 }
505
506 Err(StorageError::LayerNotFound(layer_id.to_string()))
507 }
508
509 /// Calculate the total uncompressed size of an image.
510 ///
511 /// # Errors
512 ///
513 /// Returns an error if any layer metadata cannot be read.
514 pub fn calculate_image_size(&self, image: &crate::image::Image) -> Result<u64> {
515 let layers = self.get_image_layers(image)?;
516 let mut total_size: u64 = 0;
517
518 for layer in &layers {
519 let metadata = self.get_layer_metadata(layer.id())?;
520 if let Some(size) = metadata.diff_size {
521 total_size = total_size.saturating_add(size);
522 }
523 }
524
525 Ok(total_size)
526 }
527}
528
529use crate::image::ImageJsonEntry;
530
531/// Entry in layers.json for layer ID lookups.
532#[derive(Debug, serde::Deserialize)]
533#[serde(rename_all = "kebab-case")]
534struct LayerEntry {
535 id: String,
536 parent: Option<String>,
537 diff_digest: Option<String>,
538 diff_size: Option<u64>,
539 compressed_size: Option<u64>,
540}
541
542/// Metadata about a layer from layers.json.
543#[derive(Debug, Clone)]
544pub struct LayerMetadata {
545 /// Layer storage ID.
546 pub id: String,
547 /// Parent layer ID (if not base layer).
548 pub parent: Option<String>,
549 /// Uncompressed diff size in bytes.
550 pub diff_size: Option<u64>,
551 /// Compressed size in bytes.
552 pub compressed_size: Option<u64>,
553}
554
555#[cfg(test)]
556mod tests {
557 use super::*;
558
559 #[test]
560 fn test_default_search_paths() {
561 let paths = Storage::default_search_paths();
562 assert!(!paths.is_empty(), "Should have at least one search path");
563 }
564
565 #[test]
566 fn test_storage_validation() {
567 // Create a mock storage directory structure for testing
568 let dir = tempfile::tempdir().unwrap();
569 let storage_path = dir.path();
570
571 // Create required directories
572 std::fs::create_dir_all(storage_path.join("overlay")).unwrap();
573 std::fs::create_dir_all(storage_path.join("overlay-layers")).unwrap();
574 std::fs::create_dir_all(storage_path.join("overlay-images")).unwrap();
575
576 let storage = Storage::open(storage_path).unwrap();
577 assert!(storage.root_dir().try_exists("overlay").unwrap());
578 }
579
580 /// Helper: create a mock overlay storage directory.
581 fn create_mock_storage(path: &Path) {
582 for d in ["overlay", "overlay-layers", "overlay-images"] {
583 std::fs::create_dir_all(path.join(d)).unwrap();
584 }
585 }
586
587 #[test]
588 fn test_parse_additional_image_stores() {
589 let dir = tempfile::tempdir().unwrap();
590 let store_a = dir.path().join("a");
591 let store_b = dir.path().join("b");
592 create_mock_storage(&store_a);
593 create_mock_storage(&store_b);
594
595 // Empty string returns empty
596 assert!(Storage::parse_additional_image_stores("").is_empty());
597
598 // Single store
599 let opts = format!("additionalimagestore={}", store_a.display());
600 let stores = Storage::parse_additional_image_stores(&opts);
601 assert_eq!(stores.len(), 1);
602
603 // Multiple stores (comma-separated)
604 let opts = format!(
605 "additionalimagestore={},additionalimagestore={}",
606 store_a.display(),
607 store_b.display()
608 );
609 let stores = Storage::parse_additional_image_stores(&opts);
610 assert_eq!(stores.len(), 2);
611
612 // Non-existent path is silently skipped
613 assert!(
614 Storage::parse_additional_image_stores("additionalimagestore=/no/such/path").is_empty()
615 );
616
617 // Unrelated options are ignored
618 assert!(
619 Storage::parse_additional_image_stores("overlay.mount_program=/usr/bin/fuse-overlayfs")
620 .is_empty()
621 );
622 }
623}