Skip to main content

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}