Skip to main content

monty_fs/
overlay_state.rs

1//! Internal storage for in-memory overlay mounts.
2//!
3//! This module keeps the overlay data structures separate from the public
4//! [`MountMode`](super::MountMode) definition so the public API stays easy to
5//! scan while the storage internals can evolve independently.
6
7use std::{collections::BTreeMap, mem, ops::Bound};
8
9use cap_std::fs::Dir;
10
11use super::{
12    MountError,
13    common::{as_u64, mtime_secs},
14};
15
16/// Conservative bookkeeping charge for each overlay map entry.
17///
18/// This covers the map node, key allocation, and entry metadata. Variable-size
19/// file contents and host paths are charged separately.
20pub(super) const ENTRY_MEMORY_USAGE: u64 = 256;
21
22/// In-memory overlay state for [`super::MountMode::OverlayMemory`].
23///
24/// A single [`BTreeMap`] stores relative mount paths and the overlay entry that
25/// currently shadows or extends the underlying real filesystem.
26#[derive(Debug, Default)]
27pub struct OverlayState {
28    /// Entries keyed by forward-slash-separated relative path (e.g.
29    /// `"subdir/file.txt"`). The mount root is represented by `""`.
30    ///
31    /// [`BTreeMap`] is used so prefix walks for directory operations can stay
32    /// `O(log n + k)` rather than scanning the entire overlay.
33    entries: BTreeMap<String, OverlayEntry>,
34    /// Estimated live bytes retained by `entries`.
35    memory_usage: u64,
36}
37
38impl OverlayState {
39    /// Creates a new empty overlay state.
40    #[must_use]
41    pub fn new() -> Self {
42        Self::default()
43    }
44
45    /// Looks up the overlay entry for `relative_path`.
46    #[must_use]
47    pub(super) fn get(&self, relative_path: &str) -> Option<&OverlayEntry> {
48        self.entries.get(relative_path)
49    }
50
51    /// Returns the estimated live memory retained by this overlay.
52    #[must_use]
53    pub(super) fn memory_usage(&self) -> u64 {
54        self.memory_usage
55    }
56
57    /// Removes and returns the entry for `relative_path`.
58    pub(super) fn remove(&mut self, relative_path: &str) -> Option<OverlayEntry> {
59        let entry = self.entries.remove(relative_path)?;
60        self.memory_usage = self
61            .memory_usage
62            .saturating_sub(entry_memory_usage(relative_path, &entry));
63        Some(entry)
64    }
65
66    /// Inserts an entry if the resulting overlay stays within `limit`.
67    pub(super) fn insert(&mut self, relative_path: String, entry: OverlayEntry, limit: u64) -> Result<(), MountError> {
68        let projected = self.projected_usage(&relative_path, &entry);
69        if projected > limit {
70            Err(MountError::MemoryUsageLimitExceeded(limit))
71        } else {
72            self.entries.insert(relative_path, entry);
73            self.memory_usage = projected;
74            Ok(())
75        }
76    }
77
78    /// Inserts an entry after the caller has preflighted a multi-entry update.
79    pub(super) fn insert_unchecked(&mut self, relative_path: String, entry: OverlayEntry) {
80        self.memory_usage = self.projected_usage(&relative_path, &entry);
81        self.entries.insert(relative_path, entry);
82    }
83
84    /// Returns total retained usage as if `entry` replaced `relative_path`.
85    fn projected_usage(&self, relative_path: &str, entry: &OverlayEntry) -> u64 {
86        let old_usage = self
87            .entries
88            .get(relative_path)
89            .map_or(0, |old| entry_memory_usage(relative_path, old));
90        let new_usage = entry_memory_usage(relative_path, entry);
91        self.memory_usage.saturating_sub(old_usage).saturating_add(new_usage)
92    }
93
94    /// Appends bytes to an overlay file while accounting for retained content.
95    pub(super) fn append_file(
96        &mut self,
97        relative_path: &str,
98        data: &[u8],
99        mtime: f64,
100        limit: u64,
101    ) -> Result<bool, MountError> {
102        let Some(OverlayEntry::File(file)) = self.entries.get_mut(relative_path) else {
103            return Ok(false);
104        };
105        let projected = self.memory_usage.saturating_add(as_u64(data.len()));
106        if projected > limit {
107            Err(MountError::MemoryUsageLimitExceeded(limit))
108        } else {
109            file.content.extend_from_slice(data);
110            file.mtime = mtime;
111            self.memory_usage = projected;
112            Ok(true)
113        }
114    }
115
116    /// Checks replacing `relative_path` with a file of `content_len` bytes.
117    pub(super) fn check_file_replacement(
118        &self,
119        relative_path: &str,
120        content_len: usize,
121        limit: u64,
122    ) -> Result<(), MountError> {
123        let old_usage = self
124            .entries
125            .get(relative_path)
126            .map_or(0, |old| entry_memory_usage(relative_path, old));
127        let new_usage = base_entry_memory_usage(relative_path).saturating_add(as_u64(content_len));
128        let projected = self.memory_usage.saturating_sub(old_usage).saturating_add(new_usage);
129        if projected > limit {
130            Err(MountError::MemoryUsageLimitExceeded(limit))
131        } else {
132            Ok(())
133        }
134    }
135
136    /// Checks a sequence of replacements as one atomic overlay update.
137    pub(super) fn check_replacements<'a>(
138        &self,
139        replacements: impl IntoIterator<Item = (&'a str, &'a OverlayEntry)>,
140        limit: u64,
141    ) -> Result<(), MountError> {
142        let mut projected = self.memory_usage;
143        let mut replaced = BTreeMap::new();
144        for (path, entry) in replacements {
145            let old_usage = replaced
146                .get(path)
147                .copied()
148                .unwrap_or_else(|| self.entries.get(path).map_or(0, |old| entry_memory_usage(path, old)));
149            let new_usage = entry_memory_usage(path, entry);
150            projected = projected.saturating_sub(old_usage).saturating_add(new_usage);
151            replaced.insert(path, new_usage);
152        }
153        if projected > limit {
154            Err(MountError::MemoryUsageLimitExceeded(limit))
155        } else {
156            Ok(())
157        }
158    }
159
160    /// Iterates over overlay entries whose keys start with `prefix`.
161    ///
162    /// `prefix` must be either `""` or end with `'/'`. The upper bound uses a
163    /// lexical successor so the range query stays tight without scanning the
164    /// whole map.
165    pub(super) fn prefix_iter(&self, prefix: &str) -> impl Iterator<Item = (&str, &OverlayEntry)> {
166        debug_assert!(prefix.is_empty() || prefix.ends_with('/'));
167
168        let upper_storage;
169        let bounds: (Bound<&str>, Bound<&str>) = if prefix.is_empty() {
170            (Bound::Unbounded, Bound::Unbounded)
171        } else {
172            upper_storage = {
173                let mut upper = prefix.to_owned();
174                upper.pop();
175                upper.push('0');
176                upper
177            };
178            (Bound::Included(prefix), Bound::Excluded(upper_storage.as_str()))
179        };
180
181        self.entries
182            .range::<str, _>(bounds)
183            .map(|(key, value)| (key.as_str(), value))
184    }
185}
186
187/// Estimates retained heap bytes for one overlay entry.
188fn entry_memory_usage(relative_path: &str, entry: &OverlayEntry) -> u64 {
189    let variable = match entry {
190        OverlayEntry::File(file) => file.content.len(),
191        OverlayEntry::RealFileRef(file_ref) => file_ref.relative.len(),
192        OverlayEntry::Directory { .. } | OverlayEntry::Deleted => 0,
193    };
194    base_entry_memory_usage(relative_path).saturating_add(as_u64(variable))
195}
196
197/// Returns the fixed and key-dependent charge for an overlay entry.
198fn base_entry_memory_usage(relative_path: &str) -> u64 {
199    ENTRY_MEMORY_USAGE
200        .saturating_add(as_u64(relative_path.len()))
201        .saturating_add(as_u64(mem::size_of::<OverlayEntry>()))
202}
203
204/// An entry stored in an overlay mount.
205#[derive(Debug)]
206pub(super) enum OverlayEntry {
207    /// A file written by sandbox code and stored directly in memory.
208    File(OverlayFile),
209
210    /// A lazily-read reference to a real host file that has been renamed into
211    /// the overlay without eagerly loading its contents.
212    RealFileRef(OverlayFileRef),
213
214    /// A directory that exists only in the overlay.
215    Directory {
216        /// Modification time recorded for synthetic stat results.
217        mtime: f64,
218    },
219
220    /// A tombstone hiding a real or previously-overlay entry.
221    Deleted,
222}
223
224/// In-memory contents of a file owned by the overlay.
225#[derive(Debug)]
226pub(super) struct OverlayFile {
227    /// Raw file contents.
228    pub content: Vec<u8>,
229    /// Modification time recorded for synthetic stat results.
230    pub mtime: f64,
231}
232
233/// A lazy reference to a real file preserved during overlay rename.
234///
235/// The path is *mount-relative*, so reads go back through the mount descriptor
236/// and cannot name anything outside the mount. Dereferences revalidate the path
237/// because a host actor can replace any component after capture.
238#[derive(Debug)]
239pub(super) struct OverlayFileRef {
240    /// Path relative to the mount root.
241    pub relative: String,
242    /// Modification time copied from the original file.
243    pub mtime: f64,
244    /// File size in bytes.
245    pub size: i64,
246}
247
248impl OverlayFileRef {
249    /// Builds a lazy reference only when the final component is a real file.
250    ///
251    /// The no-follow lookup closes the caller's final-component race without
252    /// resolving a link whose target identity the overlay cannot preserve.
253    #[must_use]
254    pub fn from_relative(dir: &Dir, relative: &str) -> Option<Self> {
255        let metadata = dir.symlink_metadata(relative).ok()?;
256        metadata.is_file().then(|| Self {
257            relative: relative.to_owned(),
258            mtime: mtime_secs(&metadata),
259            size: i64::try_from(metadata.len()).unwrap_or(i64::MAX),
260        })
261    }
262}