Skip to main content

composefs_storage/
layer.rs

1//! Layer reading and metadata handling.
2//!
3//! This module provides access to individual overlay layers and their metadata.
4//! Layers are the fundamental storage units in the overlay driver, representing
5//! filesystem changes that are stacked to form complete container images.
6//!
7//! # Overview
8//!
9//! The [`Layer`] struct represents a single layer in the overlay filesystem.
10//! Each layer contains:
11//! - A `diff/` directory with the actual file contents
12//! - A `link` file containing a short 26-character identifier
13//! - A `lower` file listing parent layers (if not a base layer)
14//! - Metadata for whiteouts and opaque directories
15//!
16//! # Layer Structure
17//!
18//! Each layer is stored in `overlay/<layer-id>/`:
19//! ```text
20//! overlay/<layer-id>/
21//! +-- diff/                 # Layer file contents
22//! |   +-- etc/
23//! |   |   +-- hosts
24//! |   +-- usr/
25//! |       +-- bin/
26//! +-- link                  # Short link ID (26 chars)
27//! +-- lower                 # Parent references: "l/<link-id>:l/<link-id>:..."
28//! ```
29//!
30//! # Whiteouts and Opaque Directories
31//!
32//! The overlay driver uses special markers to indicate file deletions:
33//! - `.wh.<filename>` - Whiteout file (marks `<filename>` as deleted)
34//! - `.wh..wh..opq` - Opaque directory marker (hides lower layer contents)
35
36use crate::error::{Result, StorageError};
37use crate::storage::Storage;
38use cap_std::fs::Dir;
39
40/// Represents an overlay layer with its metadata and content.
41#[derive(Debug)]
42pub struct Layer {
43    /// Layer ID (typically a 64-character hex digest).
44    id: String,
45
46    /// Directory handle for the layer directory (overlay/\<layer-id\>/).
47    layer_dir: Dir,
48
49    /// Directory handle for the diff/ subdirectory containing layer content.
50    diff_dir: Dir,
51
52    /// Short link identifier from the link file (26 characters).
53    link_id: String,
54
55    /// Parent layer link IDs from the lower file.
56    parent_links: Vec<String>,
57}
58
59impl Layer {
60    /// Open a layer by ID using fd-relative operations.
61    ///
62    /// # Errors
63    ///
64    /// Returns an error if the layer directory doesn't exist or cannot be opened.
65    pub fn open(storage: &Storage, id: &str) -> Result<Self> {
66        // Open overlay directory from storage root
67        let overlay_dir = storage.root_dir().open_dir("overlay")?;
68
69        // Open layer directory relative to overlay
70        let layer_dir = overlay_dir
71            .open_dir(id)
72            .map_err(|_| StorageError::LayerNotFound(id.to_string()))?;
73
74        // Open diff directory for content access
75        let diff_dir = layer_dir.open_dir("diff")?;
76
77        // Read metadata files using fd-relative operations
78        let link_id = Self::read_link(&layer_dir)?;
79        let parent_links = Self::read_lower(&layer_dir)?;
80
81        Ok(Self {
82            id: id.to_string(),
83            layer_dir,
84            diff_dir,
85            link_id,
86            parent_links,
87        })
88    }
89
90    /// Get the layer ID.
91    pub fn id(&self) -> &str {
92        &self.id
93    }
94
95    /// Read the link file (26-char identifier) via Dir handle.
96    fn read_link(layer_dir: &Dir) -> Result<String> {
97        let content = layer_dir.read_to_string("link")?;
98        Ok(content.trim().to_string())
99    }
100
101    /// Read the lower file (colon-separated parent links) via Dir handle.
102    fn read_lower(layer_dir: &Dir) -> Result<Vec<String>> {
103        match layer_dir.read_to_string("lower") {
104            Ok(content) => {
105                // Format is "l/<link-id>:l/<link-id>:..."
106                let links: Vec<String> = content
107                    .trim()
108                    .split(':')
109                    .filter_map(|s| s.strip_prefix("l/"))
110                    .map(|s| s.to_string())
111                    .collect();
112                Ok(links)
113            }
114            Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(Vec::new()), // Base layer has no lower file
115            Err(e) => Err(StorageError::Io(e)),
116        }
117    }
118
119    /// Get the short link ID for this layer.
120    pub fn link_id(&self) -> &str {
121        &self.link_id
122    }
123
124    /// Get the parent link IDs for this layer.
125    pub fn parent_links(&self) -> &[String] {
126        &self.parent_links
127    }
128
129    /// Get parent layer IDs (resolved from link IDs).
130    ///
131    /// This resolves the short link IDs from the `lower` file to full layer IDs
132    /// by reading the symlinks in the `overlay/l/` directory.
133    ///
134    /// # Errors
135    ///
136    /// Returns an error if any link cannot be resolved.
137    pub fn parents(&self, storage: &Storage) -> Result<Vec<String>> {
138        self.parent_links
139            .iter()
140            .map(|link_id| storage.resolve_link(link_id))
141            .collect()
142    }
143
144    /// Get a reference to the layer directory handle.
145    pub fn layer_dir(&self) -> &Dir {
146        &self.layer_dir
147    }
148
149    /// Get a reference to the diff directory handle.
150    pub fn diff_dir(&self) -> &Dir {
151        &self.diff_dir
152    }
153
154    /// Get the complete chain of layers from this layer to the base.
155    ///
156    /// Returns layers in order: [self, parent, grandparent, ..., base]
157    ///
158    /// # Errors
159    ///
160    /// Returns an error if the layer chain exceeds the maximum depth of 500 layers.
161    pub fn layer_chain(self, storage: &Storage) -> Result<Vec<Layer>> {
162        let mut chain = vec![self];
163        let mut current_idx = 0;
164
165        // Maximum depth to prevent infinite loops
166        const MAX_DEPTH: usize = 500;
167
168        while current_idx < chain.len() && chain.len() < MAX_DEPTH {
169            let parent_ids = chain[current_idx].parents(storage)?;
170
171            // Add all parents to the chain
172            for parent_id in parent_ids {
173                chain.push(Layer::open(storage, &parent_id)?);
174            }
175
176            current_idx += 1;
177        }
178
179        if chain.len() >= MAX_DEPTH {
180            return Err(StorageError::InvalidStorage(
181                "Layer chain exceeds maximum depth of 500".to_string(),
182            ));
183        }
184
185        Ok(chain)
186    }
187
188    /// Open a file in the layer's diff directory using fd-relative operations.
189    ///
190    /// # Errors
191    ///
192    /// Returns an error if the file doesn't exist or cannot be opened.
193    pub fn open_file(&self, path: impl AsRef<std::path::Path>) -> Result<cap_std::fs::File> {
194        self.diff_dir.open(path).map_err(StorageError::Io)
195    }
196
197    /// Open a file and return a standard library File.
198    ///
199    /// # Errors
200    ///
201    /// Returns an error if the file doesn't exist or cannot be opened.
202    pub fn open_file_std(&self, path: impl AsRef<std::path::Path>) -> Result<std::fs::File> {
203        let file = self.diff_dir.open(path).map_err(StorageError::Io)?;
204        Ok(file.into_std())
205    }
206
207    /// Get metadata for a file in the layer's diff directory.
208    ///
209    /// # Errors
210    ///
211    /// Returns an error if the file doesn't exist.
212    pub fn metadata(&self, path: impl AsRef<std::path::Path>) -> Result<cap_std::fs::Metadata> {
213        self.diff_dir.metadata(path).map_err(StorageError::Io)
214    }
215
216    /// Read directory entries using Dir handle.
217    ///
218    /// # Errors
219    ///
220    /// Returns an error if the directory doesn't exist.
221    pub fn read_dir(&self, path: impl AsRef<std::path::Path>) -> Result<cap_std::fs::ReadDir> {
222        self.diff_dir.read_dir(path).map_err(StorageError::Io)
223    }
224
225    /// Check if a whiteout file exists for the given filename.
226    ///
227    /// Whiteout format: `.wh.<filename>`
228    ///
229    /// # Arguments
230    ///
231    /// * `parent_path` - The directory path containing the file (empty string or "." for root)
232    /// * `filename` - The name of the file to check for whiteout
233    ///
234    /// # Errors
235    ///
236    /// Returns an error if the directory cannot be accessed.
237    pub fn has_whiteout(&self, parent_path: &str, filename: &str) -> Result<bool> {
238        let whiteout_name = format!(".wh.{}", filename);
239
240        // Handle root directory case
241        if parent_path.is_empty() || parent_path == "." {
242            Ok(self.diff_dir.try_exists(&whiteout_name)?)
243        } else {
244            match self.diff_dir.open_dir(parent_path) {
245                Ok(parent_dir) => Ok(parent_dir.try_exists(&whiteout_name)?),
246                Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(false),
247                Err(e) => Err(StorageError::Io(e)),
248            }
249        }
250    }
251
252    /// Check if a directory is marked as opaque (hides lower layers).
253    ///
254    /// Opaque marker: `.wh..wh..opq`
255    ///
256    /// # Errors
257    ///
258    /// Returns an error if the directory cannot be accessed.
259    pub fn is_opaque_dir(&self, path: &str) -> Result<bool> {
260        const OPAQUE_MARKER: &str = ".wh..wh..opq";
261
262        if path.is_empty() || path == "." {
263            Ok(self.diff_dir.try_exists(OPAQUE_MARKER)?)
264        } else {
265            match self.diff_dir.open_dir(path) {
266                Ok(dir) => Ok(dir.try_exists(OPAQUE_MARKER)?),
267                Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(false),
268                Err(e) => Err(StorageError::Io(e)),
269            }
270        }
271    }
272}
273
274#[cfg(test)]
275mod tests {
276    use super::*;
277    use std::path::Path;
278
279    #[test]
280    fn test_parse_lower_format() {
281        // Test that we correctly parse the lower file format
282        let content = "l/ABCDEFGHIJKLMNOPQRSTUVWXY:l/BCDEFGHIJKLMNOPQRSTUVWXYZ";
283        let links: Vec<String> = content
284            .trim()
285            .split(':')
286            .filter_map(|s| s.strip_prefix("l/"))
287            .map(|s| s.to_string())
288            .collect();
289
290        assert_eq!(links.len(), 2);
291        assert_eq!(links[0], "ABCDEFGHIJKLMNOPQRSTUVWXY");
292        assert_eq!(links[1], "BCDEFGHIJKLMNOPQRSTUVWXYZ");
293    }
294
295    /// Create a minimal mock storage + layer on disk so that `Layer::open()` succeeds.
296    /// Returns the opened `Layer`.
297    fn create_mock_layer(root: &Path) -> Layer {
298        // Storage validation requires these three directories
299        for d in ["overlay", "overlay-layers", "overlay-images"] {
300            std::fs::create_dir_all(root.join(d)).unwrap();
301        }
302
303        let layer_id = "test-layer-001";
304        let layer_dir = root.join("overlay").join(layer_id);
305        std::fs::create_dir_all(layer_dir.join("diff")).unwrap();
306        std::fs::write(layer_dir.join("link"), "ABCDEFGHIJKLMNOPQRSTUVWXYZ").unwrap();
307
308        let storage = Storage::open(root).unwrap();
309        Layer::open(&storage, layer_id).unwrap()
310    }
311
312    // --- has_whiteout tests ---
313
314    #[test]
315    fn test_has_whiteout_in_root() {
316        let dir = tempfile::tempdir().unwrap();
317        let layer = create_mock_layer(dir.path());
318
319        // No whiteout yet
320        assert!(!layer.has_whiteout("", "somefile").unwrap());
321        assert!(!layer.has_whiteout(".", "somefile").unwrap());
322
323        // Create whiteout marker (regular file — has_whiteout uses try_exists)
324        std::fs::write(
325            dir.path().join("overlay/test-layer-001/diff/.wh.somefile"),
326            "",
327        )
328        .unwrap();
329
330        assert!(layer.has_whiteout("", "somefile").unwrap());
331        assert!(layer.has_whiteout(".", "somefile").unwrap());
332        // Different name still returns false
333        assert!(!layer.has_whiteout("", "otherfile").unwrap());
334    }
335
336    #[test]
337    fn test_has_whiteout_in_subdirectory() {
338        let dir = tempfile::tempdir().unwrap();
339        let layer = create_mock_layer(dir.path());
340        let diff = dir.path().join("overlay/test-layer-001/diff");
341
342        std::fs::create_dir_all(diff.join("etc")).unwrap();
343        std::fs::write(diff.join("etc/.wh.hosts"), "").unwrap();
344
345        assert!(layer.has_whiteout("etc", "hosts").unwrap());
346        // Root doesn't have this whiteout
347        assert!(!layer.has_whiteout("", "hosts").unwrap());
348    }
349
350    #[test]
351    fn test_has_whiteout_in_nested_subdirectory() {
352        let dir = tempfile::tempdir().unwrap();
353        let layer = create_mock_layer(dir.path());
354        let diff = dir.path().join("overlay/test-layer-001/diff");
355
356        std::fs::create_dir_all(diff.join("usr/local/bin")).unwrap();
357        std::fs::write(diff.join("usr/local/bin/.wh.myapp"), "").unwrap();
358
359        assert!(layer.has_whiteout("usr/local/bin", "myapp").unwrap());
360        assert!(!layer.has_whiteout("usr/local", "myapp").unwrap());
361        assert!(!layer.has_whiteout("usr", "myapp").unwrap());
362    }
363
364    #[test]
365    fn test_has_whiteout_nonexistent_parent() {
366        let dir = tempfile::tempdir().unwrap();
367        let layer = create_mock_layer(dir.path());
368
369        // Parent directory doesn't exist — should return false, not error
370        assert!(!layer.has_whiteout("no/such/dir", "file").unwrap());
371    }
372
373    #[test]
374    fn test_has_whiteout_multiple() {
375        let dir = tempfile::tempdir().unwrap();
376        let layer = create_mock_layer(dir.path());
377        let diff = dir.path().join("overlay/test-layer-001/diff");
378
379        std::fs::write(diff.join(".wh.file_a"), "").unwrap();
380        std::fs::write(diff.join(".wh.file_b"), "").unwrap();
381
382        assert!(layer.has_whiteout("", "file_a").unwrap());
383        assert!(layer.has_whiteout("", "file_b").unwrap());
384        assert!(!layer.has_whiteout("", "file_c").unwrap());
385    }
386
387    // --- is_opaque_dir tests ---
388
389    #[test]
390    fn test_is_opaque_dir_in_root() {
391        let dir = tempfile::tempdir().unwrap();
392        let layer = create_mock_layer(dir.path());
393        let diff = dir.path().join("overlay/test-layer-001/diff");
394
395        assert!(!layer.is_opaque_dir("").unwrap());
396        assert!(!layer.is_opaque_dir(".").unwrap());
397
398        std::fs::write(diff.join(".wh..wh..opq"), "").unwrap();
399
400        assert!(layer.is_opaque_dir("").unwrap());
401        assert!(layer.is_opaque_dir(".").unwrap());
402    }
403
404    #[test]
405    fn test_is_opaque_dir_in_subdirectory() {
406        let dir = tempfile::tempdir().unwrap();
407        let layer = create_mock_layer(dir.path());
408        let diff = dir.path().join("overlay/test-layer-001/diff");
409
410        std::fs::create_dir_all(diff.join("etc")).unwrap();
411        std::fs::write(diff.join("etc/.wh..wh..opq"), "").unwrap();
412
413        assert!(layer.is_opaque_dir("etc").unwrap());
414        // Root is not opaque
415        assert!(!layer.is_opaque_dir("").unwrap());
416    }
417
418    #[test]
419    fn test_is_opaque_dir_false_for_normal_dir() {
420        let dir = tempfile::tempdir().unwrap();
421        let layer = create_mock_layer(dir.path());
422        let diff = dir.path().join("overlay/test-layer-001/diff");
423
424        // Create a subdirectory with a regular file, but no opaque marker
425        std::fs::create_dir_all(diff.join("var/log")).unwrap();
426        std::fs::write(diff.join("var/log/syslog"), "log data").unwrap();
427
428        assert!(!layer.is_opaque_dir("var").unwrap());
429        assert!(!layer.is_opaque_dir("var/log").unwrap());
430    }
431
432    #[test]
433    fn test_is_opaque_dir_nonexistent_path() {
434        let dir = tempfile::tempdir().unwrap();
435        let layer = create_mock_layer(dir.path());
436
437        // Non-existent directory should return false, not error
438        assert!(!layer.is_opaque_dir("no/such/path").unwrap());
439    }
440
441    #[test]
442    fn test_is_opaque_dir_nested() {
443        let dir = tempfile::tempdir().unwrap();
444        let layer = create_mock_layer(dir.path());
445        let diff = dir.path().join("overlay/test-layer-001/diff");
446
447        // Only the nested dir is opaque, not its parents
448        std::fs::create_dir_all(diff.join("a/b/c")).unwrap();
449        std::fs::write(diff.join("a/b/c/.wh..wh..opq"), "").unwrap();
450
451        assert!(!layer.is_opaque_dir("a").unwrap());
452        assert!(!layer.is_opaque_dir("a/b").unwrap());
453        assert!(layer.is_opaque_dir("a/b/c").unwrap());
454    }
455
456    // --- Interaction between whiteout and opaque ---
457
458    #[test]
459    fn test_whiteout_and_opaque_coexist() {
460        let dir = tempfile::tempdir().unwrap();
461        let layer = create_mock_layer(dir.path());
462        let diff = dir.path().join("overlay/test-layer-001/diff");
463
464        std::fs::create_dir_all(diff.join("mydir")).unwrap();
465        // Opaque marker in mydir
466        std::fs::write(diff.join("mydir/.wh..wh..opq"), "").unwrap();
467        // Also a file whiteout in mydir
468        std::fs::write(diff.join("mydir/.wh.oldfile"), "").unwrap();
469
470        assert!(layer.is_opaque_dir("mydir").unwrap());
471        assert!(layer.has_whiteout("mydir", "oldfile").unwrap());
472    }
473
474    #[test]
475    fn test_whiteout_of_dotdot_prefix_name() {
476        // .wh..wh. is NOT an opaque whiteout — it's a whiteout for a file
477        // literally named ".wh." (the has_whiteout logic just prepends ".wh.")
478        let dir = tempfile::tempdir().unwrap();
479        let layer = create_mock_layer(dir.path());
480        let diff = dir.path().join("overlay/test-layer-001/diff");
481
482        // Create .wh..wh. (whiteout for file named ".wh.")
483        std::fs::write(diff.join(".wh..wh."), "").unwrap();
484
485        assert!(layer.has_whiteout("", ".wh.").unwrap());
486        // This is NOT an opaque marker — the opaque marker is ".wh..wh..opq"
487        assert!(!layer.is_opaque_dir("").unwrap());
488    }
489}