⚠️ VeridianOS Kernel Documentation - This is low-level kernel code. All functions are unsafe unless explicitly marked otherwise. no_std

veridian_kernel/virt/containers/
overlay.rs

1//! Overlay Filesystem - lower/upper layers, copy-up, whiteout, directory merge.
2
3#[cfg(feature = "alloc")]
4use alloc::{collections::BTreeMap, format, string::String, vec::Vec};
5
6use crate::error::KernelError;
7
8/// Whiteout marker prefix per the OCI/overlay specification.
9const WHITEOUT_PREFIX: &str = ".wh.";
10
11/// Opaque directory marker.
12const OPAQUE_WHITEOUT: &str = ".wh..wh..opq";
13
14/// Entry type in the overlay filesystem.
15#[derive(Debug, Clone, Copy, PartialEq, Eq)]
16pub enum OverlayEntryKind {
17    File,
18    Directory,
19    Symlink,
20    Whiteout,
21    OpaqueDir,
22}
23
24/// A single entry in an overlay layer.
25#[cfg(feature = "alloc")]
26#[derive(Debug, Clone, PartialEq, Eq)]
27pub struct OverlayEntry {
28    /// Full path relative to the layer root.
29    pub path: String,
30    /// Entry kind.
31    pub kind: OverlayEntryKind,
32    /// File content (empty for directories/whiteouts).
33    pub content: Vec<u8>,
34    /// File permissions (Unix mode).
35    pub mode: u32,
36}
37
38/// A single layer in the overlay filesystem.
39#[cfg(feature = "alloc")]
40#[derive(Debug, Clone)]
41pub struct OverlayLayer {
42    /// Layer entries keyed by path.
43    pub(crate) entries: BTreeMap<String, OverlayEntry>,
44    /// Whether this layer is read-only (lower layer).
45    pub readonly: bool,
46}
47
48#[cfg(feature = "alloc")]
49impl OverlayLayer {
50    /// Create a new layer.
51    pub fn new(readonly: bool) -> Self {
52        Self {
53            entries: BTreeMap::new(),
54            readonly,
55        }
56    }
57
58    /// Add an entry to the layer.
59    pub fn add_entry(&mut self, entry: OverlayEntry) -> Result<(), KernelError> {
60        if self.readonly {
61            return Err(KernelError::PermissionDenied {
62                operation: "write to readonly layer",
63            });
64        }
65        self.entries.insert(entry.path.clone(), entry);
66        Ok(())
67    }
68
69    /// Look up an entry by path.
70    pub fn get_entry(&self, path: &str) -> Option<&OverlayEntry> {
71        self.entries.get(path)
72    }
73
74    /// Check if a path has been whited out.
75    pub fn is_whiteout(&self, path: &str) -> bool {
76        // Check for explicit whiteout entry
77        if let Some(entry) = self.entries.get(path) {
78            return entry.kind == OverlayEntryKind::Whiteout;
79        }
80        // Check for whiteout file (.wh.<name>)
81        if let Some((_dir, name)) = path.rsplit_once('/') {
82            let wh_path = format!(
83                "{}/{}{}",
84                path.rsplit_once('/').map(|(d, _)| d).unwrap_or(""),
85                WHITEOUT_PREFIX,
86                name
87            );
88            self.entries.contains_key(&wh_path)
89        } else {
90            let wh_path = format!("{}{}", WHITEOUT_PREFIX, path);
91            self.entries.contains_key(&wh_path)
92        }
93    }
94
95    /// Check if a directory is opaque (blocks looking into lower layers).
96    pub fn is_opaque_dir(&self, dir_path: &str) -> bool {
97        let opq_path = if dir_path.ends_with('/') {
98            format!("{}{}", dir_path, OPAQUE_WHITEOUT)
99        } else {
100            format!("{}/{}", dir_path, OPAQUE_WHITEOUT)
101        };
102        self.entries.contains_key(&opq_path)
103    }
104
105    /// List entries in a directory (non-recursive).
106    pub fn list_dir(&self, dir_path: &str) -> Vec<&OverlayEntry> {
107        let prefix = if dir_path.ends_with('/') || dir_path.is_empty() {
108            String::from(dir_path)
109        } else {
110            format!("{}/", dir_path)
111        };
112        self.entries
113            .values()
114            .filter(|e| {
115                if e.path.starts_with(prefix.as_str()) {
116                    let rest = &e.path[prefix.len()..];
117                    !rest.is_empty() && !rest.contains('/')
118                } else {
119                    false
120                }
121            })
122            .collect()
123    }
124
125    pub fn entry_count(&self) -> usize {
126        self.entries.len()
127    }
128}
129
130/// Overlay filesystem combining multiple layers.
131#[cfg(feature = "alloc")]
132#[derive(Debug)]
133pub struct OverlayFs {
134    /// Lower (read-only) layers, ordered bottom to top.
135    lower_layers: Vec<OverlayLayer>,
136    /// Upper (writable) layer.
137    upper_layer: OverlayLayer,
138    /// Work directory path (used for atomic operations).
139    work_dir: String,
140}
141
142#[cfg(feature = "alloc")]
143impl OverlayFs {
144    /// Create a new overlay filesystem.
145    pub fn new(work_dir: &str) -> Self {
146        Self {
147            lower_layers: Vec::new(),
148            upper_layer: OverlayLayer::new(false),
149            work_dir: String::from(work_dir),
150        }
151    }
152
153    /// Add a read-only lower layer (bottom-most first).
154    pub fn add_lower_layer(&mut self, layer: OverlayLayer) {
155        self.lower_layers.push(layer);
156    }
157
158    /// Look up a file: check upper layer first, then lower layers top to
159    /// bottom.
160    pub fn lookup(&self, path: &str) -> Option<&OverlayEntry> {
161        // Check upper layer first
162        if self.upper_layer.is_whiteout(path) {
163            return None; // deleted in upper
164        }
165        if let Some(entry) = self.upper_layer.get_entry(path) {
166            return Some(entry);
167        }
168
169        // Check lower layers from top to bottom
170        for layer in self.lower_layers.iter().rev() {
171            if layer.is_whiteout(path) {
172                return None;
173            }
174            // If the parent dir is opaque in this layer, skip lower layers
175            if let Some((parent, _)) = path.rsplit_once('/') {
176                if layer.is_opaque_dir(parent) {
177                    return layer.get_entry(path);
178                }
179            }
180            if let Some(entry) = layer.get_entry(path) {
181                return Some(entry);
182            }
183        }
184
185        None
186    }
187
188    /// Write a file to the upper layer. If the file exists in a lower layer,
189    /// performs copy-up first.
190    pub fn write_file(
191        &mut self,
192        path: &str,
193        content: Vec<u8>,
194        mode: u32,
195    ) -> Result<(), KernelError> {
196        let entry = OverlayEntry {
197            path: String::from(path),
198            kind: OverlayEntryKind::File,
199            content,
200            mode,
201        };
202        self.upper_layer.entries.insert(String::from(path), entry);
203        Ok(())
204    }
205
206    /// Delete a file by creating a whiteout in the upper layer.
207    pub fn delete_file(&mut self, path: &str) -> Result<(), KernelError> {
208        // Remove from upper if present
209        self.upper_layer.entries.remove(path);
210
211        // Check if it exists in any lower layer
212        let exists_in_lower = self
213            .lower_layers
214            .iter()
215            .any(|l| l.get_entry(path).is_some());
216
217        if exists_in_lower {
218            // Create whiteout
219            if let Some((dir, name)) = path.rsplit_once('/') {
220                let wh_path = format!("{}/{}{}", dir, WHITEOUT_PREFIX, name);
221                self.upper_layer.entries.insert(
222                    wh_path.clone(),
223                    OverlayEntry {
224                        path: wh_path,
225                        kind: OverlayEntryKind::Whiteout,
226                        content: Vec::new(),
227                        mode: 0,
228                    },
229                );
230            } else {
231                let wh_path = format!("{}{}", WHITEOUT_PREFIX, path);
232                self.upper_layer.entries.insert(
233                    wh_path.clone(),
234                    OverlayEntry {
235                        path: wh_path,
236                        kind: OverlayEntryKind::Whiteout,
237                        content: Vec::new(),
238                        mode: 0,
239                    },
240                );
241            }
242        }
243
244        Ok(())
245    }
246
247    /// Make a directory opaque (hides all entries from lower layers).
248    pub fn make_opaque_dir(&mut self, dir_path: &str) -> Result<(), KernelError> {
249        let opq_path = format!("{}/{}", dir_path, OPAQUE_WHITEOUT);
250        self.upper_layer.entries.insert(
251            opq_path.clone(),
252            OverlayEntry {
253                path: opq_path,
254                kind: OverlayEntryKind::OpaqueDir,
255                content: Vec::new(),
256                mode: 0,
257            },
258        );
259        Ok(())
260    }
261
262    /// List directory contents merging all layers. Upper entries take
263    /// precedence. Whited-out entries are excluded.
264    pub fn list_dir(&self, dir_path: &str) -> Vec<&OverlayEntry> {
265        let mut seen: BTreeMap<String, &OverlayEntry> = BTreeMap::new();
266        let mut whited_out: Vec<String> = Vec::new();
267
268        // Upper layer first
269        for entry in self.upper_layer.list_dir(dir_path) {
270            if entry.kind == OverlayEntryKind::Whiteout {
271                // Extract the original filename from the whiteout name
272                if let Some(name) = entry
273                    .path
274                    .rsplit('/')
275                    .next()
276                    .and_then(|n| n.strip_prefix(WHITEOUT_PREFIX))
277                {
278                    let orig = if dir_path.is_empty() {
279                        String::from(name)
280                    } else {
281                        format!("{}/{}", dir_path, name)
282                    };
283                    whited_out.push(orig);
284                }
285            } else if entry.kind != OverlayEntryKind::OpaqueDir {
286                seen.insert(entry.path.clone(), entry);
287            }
288        }
289
290        // Check if upper declares this directory opaque
291        let is_opaque = self.upper_layer.is_opaque_dir(dir_path);
292
293        if !is_opaque {
294            // Lower layers from top to bottom
295            for layer in self.lower_layers.iter().rev() {
296                if layer.is_opaque_dir(dir_path) {
297                    // This layer is opaque, add its entries but stop going lower
298                    for entry in layer.list_dir(dir_path) {
299                        if !seen.contains_key(&entry.path) && !whited_out.contains(&entry.path) {
300                            seen.insert(entry.path.clone(), entry);
301                        }
302                    }
303                    break;
304                }
305                for entry in layer.list_dir(dir_path) {
306                    if !seen.contains_key(&entry.path) && !whited_out.contains(&entry.path) {
307                        seen.insert(entry.path.clone(), entry);
308                    }
309                }
310            }
311        }
312
313        seen.into_values().collect()
314    }
315
316    pub fn work_dir(&self) -> &str {
317        &self.work_dir
318    }
319
320    pub fn lower_layer_count(&self) -> usize {
321        self.lower_layers.len()
322    }
323}