Skip to content

Commit

Permalink
oci: improve layer whiteout handling
Browse files Browse the repository at this point in the history
Add support for opaque directories and generally improve our path
handling.  Add some very simple tests.

There's a major outstanding issue here: the OCI spec says that .wh.
files SHOULD come before the files that they affect, and that they only
affect previous layers.  If we do happen to get a whiteout after a file
that was added in the same layer, we're going to delete it, though.

I have a semi-crazy idea involving a hashset containing weak references
(or pinned pointers)...
  • Loading branch information
allisonkarlitskaya committed Oct 14, 2024
1 parent 09c5167 commit 9c1b0dc
Show file tree
Hide file tree
Showing 2 changed files with 105 additions and 23 deletions.
4 changes: 4 additions & 0 deletions src/image.rs
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,10 @@ impl Directory {
_ => { /* not an error to remove an already-missing file */ }
}
}

pub fn remove_all(&mut self) {
self.entries.clear();
}
}

pub struct FileSystem {
Expand Down
124 changes: 101 additions & 23 deletions src/oci/image.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,12 @@ use std::{
ffi::OsStr,
io::Read,
os::unix::ffi::OsStrExt,
path::Component,
process::{
Command,
Stdio
},
rc::Rc,
};

use anyhow::{
Expand All @@ -24,35 +26,41 @@ use crate::{
splitstream::SplitStreamReader,
};

fn is_whiteout(name: &OsStr) -> Option<&OsStr> {
let bytes = name.as_bytes();
if bytes.len() > 4 && &bytes[0..4] == b".wh." {
Some(OsStr::from_bytes(&bytes[4..]))
} else {
None
pub fn process_entry(filesystem: &mut FileSystem, entry: oci::tar::TarEntry) -> Result<()> {
let mut components = entry.path.components();

let Some(Component::Normal(filename)) = components.next_back() else { bail!("Empty filename") };

let mut dir = &mut filesystem.root;
for component in components {
match component {
Component::Normal(name) => { dir = dir.recurse(name)?; },
_ => { }
}
}
}

pub fn process_entry(filesystem: &mut FileSystem, entry: oci::tar::TarEntry) -> Result<()> {
if let Some(filename) = entry.path.file_name() {
if let Some(whiteout) = is_whiteout(filename) {
filesystem.remove(&entry.path.parent().unwrap().join(whiteout))?;
let bytes = filename.as_bytes();
if let Some(whiteout) = bytes.strip_prefix(b".wh.") {
if whiteout == b".wh.opq" { // complete name is '.wh..wh.opq'
dir.remove_all()
} else {
match entry.item {
oci::tar::TarItem::Directory => {
filesystem.mkdir(&entry.path, entry.stat)?;
},
oci::tar::TarItem::Leaf(content) => {
filesystem.insert(&entry.path, Leaf { stat: entry.stat, content })?;
},
oci::tar::TarItem::Hardlink(target) => {
filesystem.hardlink(&entry.path, &target)?;
},
}
dir.remove(OsStr::from_bytes(whiteout))
}
} else {
bail!("Invalid filename");
match entry.item {
oci::tar::TarItem::Directory => {
dir.mkdir(filename, entry.stat)
},
oci::tar::TarItem::Leaf(content) => {
dir.insert(filename, Rc::new(Leaf { stat: entry.stat, content }))
},
oci::tar::TarItem::Hardlink(ref target) => {
// TODO: would be nice to do this inline, but borrow checker doesn't like it
filesystem.hardlink(&entry.path, target)?;
},
}
}

Ok(())
}

Expand Down Expand Up @@ -109,3 +117,73 @@ pub fn create_image(repo: &Repository, name: &str, layers: &Vec<String>) -> Resu

repo.write_image(name, &image)
}

#[cfg(test)]
use std::{
io::BufRead,
path::PathBuf,
};
#[cfg(test)]
use crate::image::{
LeafContent,
Stat,
};

#[cfg(test)]
fn file_entry(path: &str) -> oci::tar::TarEntry {
oci::tar::TarEntry {
path: PathBuf::from(path),
stat: Stat { st_mode: 0o644, st_uid: 0, st_gid: 0, st_mtim_sec: 0, xattrs: vec![]},
item: oci::tar::TarItem::Leaf(LeafContent::InlineFile(vec![])),
}
}

#[cfg(test)]
fn dir_entry(path: &str) -> oci::tar::TarEntry {
oci::tar::TarEntry {
path: PathBuf::from(path),
stat: Stat { st_mode: 0o755, st_uid: 0, st_gid: 0, st_mtim_sec: 0, xattrs: vec![]},
item: oci::tar::TarItem::Directory,
}
}

#[cfg(test)]
fn assert_files(fs: &FileSystem, expected: &[&str]) -> Result<()> {
let mut out = vec![];
write_dumpfile(&mut out, fs)?;
let actual: Vec<String> = out
.lines()
.map(|line| line.unwrap().split_once(|c| c == ' ').unwrap().0.into() )
.collect();

assert_eq!(actual, expected);
Ok(())
}

#[test]
fn test_process_entry() -> Result<()> {
let mut fs = FileSystem::new();

// both with and without leading slash should be supported
process_entry(&mut fs, dir_entry("/a"))?;
process_entry(&mut fs, dir_entry("b"))?;
process_entry(&mut fs, dir_entry("c"))?;
assert_files(&fs, &["/", "/a", "/b", "/c"])?;

// add some files
process_entry(&mut fs, file_entry("/a/b"))?;
process_entry(&mut fs, file_entry("/a/c"))?;
process_entry(&mut fs, file_entry("/b/a"))?;
process_entry(&mut fs, file_entry("/b/c"))?;
process_entry(&mut fs, file_entry("/c/a"))?;
process_entry(&mut fs, file_entry("/c/c"))?;
assert_files(&fs, &["/", "/a", "/a/b", "/a/c", "/b", "/b/a", "/b/c", "/c", "/c/a", "/c/c"])?;

// try some whiteouts
process_entry(&mut fs, file_entry(".wh.a"))?; // entire dir
process_entry(&mut fs, file_entry("/b/.wh..wh.opq"))?; // opaque dir
process_entry(&mut fs, file_entry("/c/.wh.c"))?; // single file
assert_files(&fs, &["/", "/b", "/c", "/c/a"])?;

Ok(())
}

0 comments on commit 9c1b0dc

Please sign in to comment.