use crate::error::{Result, StorageError};
use crate::storage::Storage;
use cap_std::fs::Dir;
#[derive(Debug)]
pub struct Layer {
id: String,
layer_dir: Dir,
diff_dir: Dir,
link_id: String,
parent_links: Vec<String>,
}
impl Layer {
pub fn open(storage: &Storage, id: &str) -> Result<Self> {
let overlay_dir = storage.root_dir().open_dir("overlay")?;
let layer_dir = overlay_dir
.open_dir(id)
.map_err(|_| StorageError::LayerNotFound(id.to_string()))?;
let diff_dir = layer_dir.open_dir("diff")?;
let link_id = Self::read_link(&layer_dir)?;
let parent_links = Self::read_lower(&layer_dir)?;
Ok(Self {
id: id.to_string(),
layer_dir,
diff_dir,
link_id,
parent_links,
})
}
pub fn id(&self) -> &str {
&self.id
}
fn read_link(layer_dir: &Dir) -> Result<String> {
let content = layer_dir.read_to_string("link")?;
Ok(content.trim().to_string())
}
fn read_lower(layer_dir: &Dir) -> Result<Vec<String>> {
match layer_dir.read_to_string("lower") {
Ok(content) => {
let links: Vec<String> = content
.trim()
.split(':')
.filter_map(|s| s.strip_prefix("l/"))
.map(|s| s.to_string())
.collect();
Ok(links)
}
Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(Vec::new()), Err(e) => Err(StorageError::Io(e)),
}
}
pub fn link_id(&self) -> &str {
&self.link_id
}
pub fn parent_links(&self) -> &[String] {
&self.parent_links
}
pub fn parents(&self, storage: &Storage) -> Result<Vec<String>> {
self.parent_links
.iter()
.map(|link_id| storage.resolve_link(link_id))
.collect()
}
pub fn layer_dir(&self) -> &Dir {
&self.layer_dir
}
pub fn diff_dir(&self) -> &Dir {
&self.diff_dir
}
pub fn layer_chain(self, storage: &Storage) -> Result<Vec<Layer>> {
let mut chain = vec![self];
let mut current_idx = 0;
const MAX_DEPTH: usize = 500;
while current_idx < chain.len() && chain.len() < MAX_DEPTH {
let parent_ids = chain[current_idx].parents(storage)?;
for parent_id in parent_ids {
chain.push(Layer::open(storage, &parent_id)?);
}
current_idx += 1;
}
if chain.len() >= MAX_DEPTH {
return Err(StorageError::InvalidStorage(
"Layer chain exceeds maximum depth of 500".to_string(),
));
}
Ok(chain)
}
pub fn open_file(&self, path: impl AsRef<std::path::Path>) -> Result<cap_std::fs::File> {
self.diff_dir.open(path).map_err(StorageError::Io)
}
pub fn open_file_std(&self, path: impl AsRef<std::path::Path>) -> Result<std::fs::File> {
let file = self.diff_dir.open(path).map_err(StorageError::Io)?;
Ok(file.into_std())
}
pub fn metadata(&self, path: impl AsRef<std::path::Path>) -> Result<cap_std::fs::Metadata> {
self.diff_dir.metadata(path).map_err(StorageError::Io)
}
pub fn read_dir(&self, path: impl AsRef<std::path::Path>) -> Result<cap_std::fs::ReadDir> {
self.diff_dir.read_dir(path).map_err(StorageError::Io)
}
pub fn has_whiteout(&self, parent_path: &str, filename: &str) -> Result<bool> {
let whiteout_name = format!(".wh.{}", filename);
if parent_path.is_empty() || parent_path == "." {
Ok(self.diff_dir.try_exists(&whiteout_name)?)
} else {
match self.diff_dir.open_dir(parent_path) {
Ok(parent_dir) => Ok(parent_dir.try_exists(&whiteout_name)?),
Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(false),
Err(e) => Err(StorageError::Io(e)),
}
}
}
pub fn is_opaque_dir(&self, path: &str) -> Result<bool> {
const OPAQUE_MARKER: &str = ".wh..wh..opq";
if path.is_empty() || path == "." {
Ok(self.diff_dir.try_exists(OPAQUE_MARKER)?)
} else {
match self.diff_dir.open_dir(path) {
Ok(dir) => Ok(dir.try_exists(OPAQUE_MARKER)?),
Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(false),
Err(e) => Err(StorageError::Io(e)),
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::path::Path;
#[test]
fn test_parse_lower_format() {
let content = "l/ABCDEFGHIJKLMNOPQRSTUVWXY:l/BCDEFGHIJKLMNOPQRSTUVWXYZ";
let links: Vec<String> = content
.trim()
.split(':')
.filter_map(|s| s.strip_prefix("l/"))
.map(|s| s.to_string())
.collect();
assert_eq!(links.len(), 2);
assert_eq!(links[0], "ABCDEFGHIJKLMNOPQRSTUVWXY");
assert_eq!(links[1], "BCDEFGHIJKLMNOPQRSTUVWXYZ");
}
fn create_mock_layer(root: &Path) -> Layer {
for d in ["overlay", "overlay-layers", "overlay-images"] {
std::fs::create_dir_all(root.join(d)).unwrap();
}
let layer_id = "test-layer-001";
let layer_dir = root.join("overlay").join(layer_id);
std::fs::create_dir_all(layer_dir.join("diff")).unwrap();
std::fs::write(layer_dir.join("link"), "ABCDEFGHIJKLMNOPQRSTUVWXYZ").unwrap();
let storage = Storage::open(root).unwrap();
Layer::open(&storage, layer_id).unwrap()
}
#[test]
fn test_has_whiteout_in_root() {
let dir = tempfile::tempdir().unwrap();
let layer = create_mock_layer(dir.path());
assert!(!layer.has_whiteout("", "somefile").unwrap());
assert!(!layer.has_whiteout(".", "somefile").unwrap());
std::fs::write(
dir.path().join("overlay/test-layer-001/diff/.wh.somefile"),
"",
)
.unwrap();
assert!(layer.has_whiteout("", "somefile").unwrap());
assert!(layer.has_whiteout(".", "somefile").unwrap());
assert!(!layer.has_whiteout("", "otherfile").unwrap());
}
#[test]
fn test_has_whiteout_in_subdirectory() {
let dir = tempfile::tempdir().unwrap();
let layer = create_mock_layer(dir.path());
let diff = dir.path().join("overlay/test-layer-001/diff");
std::fs::create_dir_all(diff.join("etc")).unwrap();
std::fs::write(diff.join("etc/.wh.hosts"), "").unwrap();
assert!(layer.has_whiteout("etc", "hosts").unwrap());
assert!(!layer.has_whiteout("", "hosts").unwrap());
}
#[test]
fn test_has_whiteout_in_nested_subdirectory() {
let dir = tempfile::tempdir().unwrap();
let layer = create_mock_layer(dir.path());
let diff = dir.path().join("overlay/test-layer-001/diff");
std::fs::create_dir_all(diff.join("usr/local/bin")).unwrap();
std::fs::write(diff.join("usr/local/bin/.wh.myapp"), "").unwrap();
assert!(layer.has_whiteout("usr/local/bin", "myapp").unwrap());
assert!(!layer.has_whiteout("usr/local", "myapp").unwrap());
assert!(!layer.has_whiteout("usr", "myapp").unwrap());
}
#[test]
fn test_has_whiteout_nonexistent_parent() {
let dir = tempfile::tempdir().unwrap();
let layer = create_mock_layer(dir.path());
assert!(!layer.has_whiteout("no/such/dir", "file").unwrap());
}
#[test]
fn test_has_whiteout_multiple() {
let dir = tempfile::tempdir().unwrap();
let layer = create_mock_layer(dir.path());
let diff = dir.path().join("overlay/test-layer-001/diff");
std::fs::write(diff.join(".wh.file_a"), "").unwrap();
std::fs::write(diff.join(".wh.file_b"), "").unwrap();
assert!(layer.has_whiteout("", "file_a").unwrap());
assert!(layer.has_whiteout("", "file_b").unwrap());
assert!(!layer.has_whiteout("", "file_c").unwrap());
}
#[test]
fn test_is_opaque_dir_in_root() {
let dir = tempfile::tempdir().unwrap();
let layer = create_mock_layer(dir.path());
let diff = dir.path().join("overlay/test-layer-001/diff");
assert!(!layer.is_opaque_dir("").unwrap());
assert!(!layer.is_opaque_dir(".").unwrap());
std::fs::write(diff.join(".wh..wh..opq"), "").unwrap();
assert!(layer.is_opaque_dir("").unwrap());
assert!(layer.is_opaque_dir(".").unwrap());
}
#[test]
fn test_is_opaque_dir_in_subdirectory() {
let dir = tempfile::tempdir().unwrap();
let layer = create_mock_layer(dir.path());
let diff = dir.path().join("overlay/test-layer-001/diff");
std::fs::create_dir_all(diff.join("etc")).unwrap();
std::fs::write(diff.join("etc/.wh..wh..opq"), "").unwrap();
assert!(layer.is_opaque_dir("etc").unwrap());
assert!(!layer.is_opaque_dir("").unwrap());
}
#[test]
fn test_is_opaque_dir_false_for_normal_dir() {
let dir = tempfile::tempdir().unwrap();
let layer = create_mock_layer(dir.path());
let diff = dir.path().join("overlay/test-layer-001/diff");
std::fs::create_dir_all(diff.join("var/log")).unwrap();
std::fs::write(diff.join("var/log/syslog"), "log data").unwrap();
assert!(!layer.is_opaque_dir("var").unwrap());
assert!(!layer.is_opaque_dir("var/log").unwrap());
}
#[test]
fn test_is_opaque_dir_nonexistent_path() {
let dir = tempfile::tempdir().unwrap();
let layer = create_mock_layer(dir.path());
assert!(!layer.is_opaque_dir("no/such/path").unwrap());
}
#[test]
fn test_is_opaque_dir_nested() {
let dir = tempfile::tempdir().unwrap();
let layer = create_mock_layer(dir.path());
let diff = dir.path().join("overlay/test-layer-001/diff");
std::fs::create_dir_all(diff.join("a/b/c")).unwrap();
std::fs::write(diff.join("a/b/c/.wh..wh..opq"), "").unwrap();
assert!(!layer.is_opaque_dir("a").unwrap());
assert!(!layer.is_opaque_dir("a/b").unwrap());
assert!(layer.is_opaque_dir("a/b/c").unwrap());
}
#[test]
fn test_whiteout_and_opaque_coexist() {
let dir = tempfile::tempdir().unwrap();
let layer = create_mock_layer(dir.path());
let diff = dir.path().join("overlay/test-layer-001/diff");
std::fs::create_dir_all(diff.join("mydir")).unwrap();
std::fs::write(diff.join("mydir/.wh..wh..opq"), "").unwrap();
std::fs::write(diff.join("mydir/.wh.oldfile"), "").unwrap();
assert!(layer.is_opaque_dir("mydir").unwrap());
assert!(layer.has_whiteout("mydir", "oldfile").unwrap());
}
#[test]
fn test_whiteout_of_dotdot_prefix_name() {
let dir = tempfile::tempdir().unwrap();
let layer = create_mock_layer(dir.path());
let diff = dir.path().join("overlay/test-layer-001/diff");
std::fs::write(diff.join(".wh..wh."), "").unwrap();
assert!(layer.has_whiteout("", ".wh.").unwrap());
assert!(!layer.is_opaque_dir("").unwrap());
}
}