use std::{collections::BTreeMap, mem, ops::Bound};
use cap_std::fs::Dir;
use super::{
MountError,
common::{as_u64, mtime_secs},
};
pub(super) const ENTRY_MEMORY_USAGE: u64 = 256;
#[derive(Debug, Default)]
pub struct OverlayState {
entries: BTreeMap<String, OverlayEntry>,
memory_usage: u64,
}
impl OverlayState {
#[must_use]
pub fn new() -> Self {
Self::default()
}
#[must_use]
pub(super) fn get(&self, relative_path: &str) -> Option<&OverlayEntry> {
self.entries.get(relative_path)
}
#[must_use]
pub(super) fn memory_usage(&self) -> u64 {
self.memory_usage
}
pub(super) fn remove(&mut self, relative_path: &str) -> Option<OverlayEntry> {
let entry = self.entries.remove(relative_path)?;
self.memory_usage = self
.memory_usage
.saturating_sub(entry_memory_usage(relative_path, &entry));
Some(entry)
}
pub(super) fn insert(&mut self, relative_path: String, entry: OverlayEntry, limit: u64) -> Result<(), MountError> {
let projected = self.projected_usage(&relative_path, &entry);
if projected > limit {
Err(MountError::MemoryUsageLimitExceeded(limit))
} else {
self.entries.insert(relative_path, entry);
self.memory_usage = projected;
Ok(())
}
}
pub(super) fn insert_unchecked(&mut self, relative_path: String, entry: OverlayEntry) {
self.memory_usage = self.projected_usage(&relative_path, &entry);
self.entries.insert(relative_path, entry);
}
fn projected_usage(&self, relative_path: &str, entry: &OverlayEntry) -> u64 {
let old_usage = self
.entries
.get(relative_path)
.map_or(0, |old| entry_memory_usage(relative_path, old));
let new_usage = entry_memory_usage(relative_path, entry);
self.memory_usage.saturating_sub(old_usage).saturating_add(new_usage)
}
pub(super) fn append_file(
&mut self,
relative_path: &str,
data: &[u8],
mtime: f64,
limit: u64,
) -> Result<bool, MountError> {
let Some(OverlayEntry::File(file)) = self.entries.get_mut(relative_path) else {
return Ok(false);
};
let projected = self.memory_usage.saturating_add(as_u64(data.len()));
if projected > limit {
Err(MountError::MemoryUsageLimitExceeded(limit))
} else {
file.content.extend_from_slice(data);
file.mtime = mtime;
self.memory_usage = projected;
Ok(true)
}
}
pub(super) fn check_file_replacement(
&self,
relative_path: &str,
content_len: usize,
limit: u64,
) -> Result<(), MountError> {
let old_usage = self
.entries
.get(relative_path)
.map_or(0, |old| entry_memory_usage(relative_path, old));
let new_usage = base_entry_memory_usage(relative_path).saturating_add(as_u64(content_len));
let projected = self.memory_usage.saturating_sub(old_usage).saturating_add(new_usage);
if projected > limit {
Err(MountError::MemoryUsageLimitExceeded(limit))
} else {
Ok(())
}
}
pub(super) fn check_replacements<'a>(
&self,
replacements: impl IntoIterator<Item = (&'a str, &'a OverlayEntry)>,
limit: u64,
) -> Result<(), MountError> {
let mut projected = self.memory_usage;
let mut replaced = BTreeMap::new();
for (path, entry) in replacements {
let old_usage = replaced
.get(path)
.copied()
.unwrap_or_else(|| self.entries.get(path).map_or(0, |old| entry_memory_usage(path, old)));
let new_usage = entry_memory_usage(path, entry);
projected = projected.saturating_sub(old_usage).saturating_add(new_usage);
replaced.insert(path, new_usage);
}
if projected > limit {
Err(MountError::MemoryUsageLimitExceeded(limit))
} else {
Ok(())
}
}
pub(super) fn prefix_iter(&self, prefix: &str) -> impl Iterator<Item = (&str, &OverlayEntry)> {
debug_assert!(prefix.is_empty() || prefix.ends_with('/'));
let upper_storage;
let bounds: (Bound<&str>, Bound<&str>) = if prefix.is_empty() {
(Bound::Unbounded, Bound::Unbounded)
} else {
upper_storage = {
let mut upper = prefix.to_owned();
upper.pop();
upper.push('0');
upper
};
(Bound::Included(prefix), Bound::Excluded(upper_storage.as_str()))
};
self.entries
.range::<str, _>(bounds)
.map(|(key, value)| (key.as_str(), value))
}
}
fn entry_memory_usage(relative_path: &str, entry: &OverlayEntry) -> u64 {
let variable = match entry {
OverlayEntry::File(file) => file.content.len(),
OverlayEntry::RealFileRef(file_ref) => file_ref.relative.len(),
OverlayEntry::Directory { .. } | OverlayEntry::Deleted => 0,
};
base_entry_memory_usage(relative_path).saturating_add(as_u64(variable))
}
fn base_entry_memory_usage(relative_path: &str) -> u64 {
ENTRY_MEMORY_USAGE
.saturating_add(as_u64(relative_path.len()))
.saturating_add(as_u64(mem::size_of::<OverlayEntry>()))
}
#[derive(Debug)]
pub(super) enum OverlayEntry {
File(OverlayFile),
RealFileRef(OverlayFileRef),
Directory {
mtime: f64,
},
Deleted,
}
#[derive(Debug)]
pub(super) struct OverlayFile {
pub content: Vec<u8>,
pub mtime: f64,
}
#[derive(Debug)]
pub(super) struct OverlayFileRef {
pub relative: String,
pub mtime: f64,
pub size: i64,
}
impl OverlayFileRef {
#[must_use]
pub fn from_relative(dir: &Dir, relative: &str) -> Option<Self> {
let metadata = dir.symlink_metadata(relative).ok()?;
metadata.is_file().then(|| Self {
relative: relative.to_owned(),
mtime: mtime_secs(&metadata),
size: i64::try_from(metadata.len()).unwrap_or(i64::MAX),
})
}
}