use std::collections::HashSet;
use std::ffi::{CStr, OsStr};
use std::fs::{self, File};
use std::io::{self, Read, Write};
use std::os::fd::{AsRawFd, OwnedFd, RawFd};
use std::os::unix::ffi::OsStrExt;
use std::os::unix::fs::MetadataExt;
use std::path::{Component, Path, PathBuf};
use std::sync::{mpsc, Arc, Condvar, Mutex};
use std::thread;
use crate::beneath::{
cstr, cvt, escape, fstat, fstatat, is, is_escape, list, openat, remove_at, Root,
};
use crate::decode::{open_layer, Decoder};
use crate::error::Context;
use crate::{Error, Result};
pub const SMALL_FILE: u64 = 1 << 20;
pub const IN_FLIGHT: usize = 16 << 20;
pub const OPEN_FILES: usize = 256;
const CHUNK: usize = 1 << 20;
#[derive(Debug, Clone)]
pub struct UnpackOptions {
pub threads: usize,
pub parallel_gzip: bool,
pub same_owner: bool,
pub layout: Layout,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum Layout {
#[default]
Flatten,
Overlay(OverlayXattr),
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum OverlayXattr {
Trusted,
User,
}
impl Default for UnpackOptions {
fn default() -> Self {
Self {
threads: 0,
parallel_gzip: true,
same_owner: fs::metadata("/proc/self").is_ok_and(|m| m.uid() == 0),
layout: Layout::Flatten,
}
}
}
impl UnpackOptions {
pub fn threads(&self) -> usize {
if self.threads > 0 {
self.threads
} else {
thread::available_parallelism().map_or(1, |n| n.get())
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct UnpackStats {
pub decoder: Decoder,
pub entries: u64,
pub files: u64,
pub bytes: u64,
pub whiteouts: u64,
pub skipped_devices: u64,
}
pub fn apply_layer_file(blob: &Path, rootfs: &Path, opts: &UnpackOptions) -> Result<UnpackStats> {
let (reader, decoder, _) = open_layer(blob, opts, false)?;
let mut stats = apply_layer(reader, rootfs, opts)?;
stats.decoder = decoder;
Ok(stats)
}
pub fn apply_layer_file_digest(
blob: &Path,
rootfs: &Path,
opts: &UnpackOptions,
) -> Result<(UnpackStats, crate::Digest)> {
let (reader, decoder, digest) = open_layer(blob, opts, true)?;
let mut stats = apply_layer(reader, rootfs, opts)?;
stats.decoder = decoder;
let digest = digest
.finish()
.ok_or_else(|| Error::Invalid(format!("hashing layer {} failed", blob.display())))?;
Ok((stats, digest))
}
pub fn apply_layer<R: Read + Send>(
tar: R,
rootfs: &Path,
opts: &UnpackOptions,
) -> Result<UnpackStats> {
let threads = opts.threads();
let root = Root::open(rootfs).ctx(|| format!("rootfs {}", rootfs.display()))?;
let mut w = Writer {
root: &root,
same_owner: opts.same_owner,
layout: opts.layout,
ours: HashSet::new(),
dirs: Vec::new(),
stats: UnpackStats {
decoder: Decoder::Plain,
entries: 0,
files: 0,
bytes: 0,
whiteouts: 0,
skipped_devices: 0,
},
};
thread::scope(|s| -> Result<()> {
let reader: Box<dyn Read> = if threads > 1 {
Box::new(read_ahead(s, tar))
} else {
Box::new(tar)
};
let mut pool = Pool::new(s, threads.saturating_sub(1), opts.same_owner);
let mut archive = tar::Archive::new(reader);
let r = (|| -> Result<()> {
for entry in archive.entries().ctx(|| "reading layer".into())? {
let mut entry = entry.ctx(|| "reading layer".into())?;
w.entry(&mut entry, &mut pool)?;
}
Ok(())
})();
let done = pool.finish();
r?;
done?;
let mut rest = archive.into_inner();
io::copy(&mut rest, &mut io::sink()).ctx(|| "reading the layer to its end".into())?;
Ok(())
})?;
for (rel, mtime) in w.dirs.iter().rev() {
if let Ok(Some(d)) = w.open_dir(rel, false) {
let t = times(*mtime);
unsafe { libc::futimens(d.as_raw_fd(), t.as_ptr()) };
}
}
Ok(w.stats)
}
struct FileMeta {
mode: u32,
uid: u64,
gid: u64,
mtime: u64,
}
fn times(mtime: u64) -> [libc::timespec; 2] {
let t = libc::timespec {
tv_sec: mtime as libc::time_t,
tv_nsec: 0,
};
[t, t]
}
fn finish_file(f: &File, meta: &FileMeta, same_owner: bool) -> io::Result<()> {
let fd = f.as_raw_fd();
if same_owner {
cvt(unsafe { libc::fchown(fd, meta.uid as u32, meta.gid as u32) })?;
}
cvt(unsafe { libc::fchmod(fd, meta.mode) })?;
let t = times(meta.mtime);
cvt(unsafe { libc::futimens(fd, t.as_ptr()) })?;
Ok(())
}
fn err(e: io::Error, rel: &Path) -> Error {
if is_escape(&e) {
Error::Escape(rel.to_path_buf())
} else {
Error::Io {
context: format!("unpacking {}", rel.display()),
source: e,
}
}
}
fn split(rel: &Path) -> (&Path, &OsStr) {
(
rel.parent().unwrap_or(Path::new("")),
rel.file_name().unwrap_or_default(),
)
}
struct Writer<'r> {
root: &'r Root,
same_owner: bool,
layout: Layout,
ours: HashSet<PathBuf>,
dirs: Vec<(PathBuf, u64)>,
stats: UnpackStats,
}
impl Writer<'_> {
fn parent(&self, rel: &Path, create: bool) -> Result<Option<OwnedFd>> {
match self.root.dir(split(rel).0, create) {
Ok(fd) => Ok(Some(fd)),
Err(e) if !create && matches!(e.raw_os_error(), Some(libc::ENOENT | libc::ENOTDIR)) => {
Ok(None)
}
Err(e) => Err(err(e, rel)),
}
}
fn open_dir(&self, rel: &Path, create: bool) -> Result<Option<OwnedFd>> {
let p = match self.root.dir(rel, create) {
Ok(p) => p,
Err(e) if !create && matches!(e.raw_os_error(), Some(libc::ENOENT | libc::ENOTDIR)) => {
return Ok(None)
}
Err(e) => return Err(err(e, rel)),
};
openat(p.as_raw_fd(), c".", libc::O_RDONLY | libc::O_DIRECTORY, 0)
.map(Some)
.map_err(|e| err(e, rel))
}
fn hide(&self, rel: &Path) -> Result<()> {
let Some(pfd) = self.parent(rel, false)? else {
return Ok(());
};
let c = cstr(split(rel).1).map_err(|e| err(e, rel))?;
if !self.ours.contains(rel) {
return remove_at(pfd.as_raw_fd(), &c).map_err(|e| err(e, rel));
}
let st = fstatat(pfd.as_raw_fd(), &c).map_err(|e| err(e, rel))?;
if st.is_some_and(|st| is(&st, libc::S_IFDIR)) {
let d = openat(
pfd.as_raw_fd(),
&c,
libc::O_RDONLY | libc::O_DIRECTORY | libc::O_NOFOLLOW,
0,
)
.map_err(|e| err(e, rel))?;
for child in list(d.as_raw_fd()).map_err(|e| err(e, rel))? {
self.hide(&rel.join(OsStr::from_bytes(child.to_bytes())))?;
}
}
Ok(())
}
fn whiteout(&mut self, parent: &Path, hidden: Option<&str>) -> Result<()> {
self.stats.whiteouts += 1;
match (self.layout, hidden) {
(Layout::Flatten, None) => {
if let Some(d) = self.open_dir(parent, false)? {
for child in list(d.as_raw_fd()).map_err(|e| err(e, parent))? {
self.hide(&parent.join(OsStr::from_bytes(child.to_bytes())))?;
}
}
}
(Layout::Flatten, Some(h)) => self.hide(&parent.join(h))?,
(Layout::Overlay(xattr), None) => {
let d = self.open_dir(parent, true)?.expect("created");
let name: &CStr = match xattr {
OverlayXattr::Trusted => c"trusted.overlay.opaque",
OverlayXattr::User => c"user.overlay.opaque",
};
cvt(unsafe {
libc::fsetxattr(d.as_raw_fd(), name.as_ptr(), b"y".as_ptr().cast(), 1, 0)
})
.map_err(|e| err(e, parent))?;
add_with_parents(&mut self.ours, parent);
}
(Layout::Overlay(_), Some(h)) => {
let rel = parent.join(h);
let pfd = self.parent(&rel, true)?.expect("created");
let c = cstr(OsStr::new(h)).map_err(|e| err(e, &rel))?;
remove_at(pfd.as_raw_fd(), &c).map_err(|e| err(e, &rel))?;
cvt(unsafe { libc::mknodat(pfd.as_raw_fd(), c.as_ptr(), libc::S_IFCHR, 0) })
.map_err(|e| err(e, &rel))?;
add_with_parents(&mut self.ours, &rel);
}
}
Ok(())
}
fn special_meta(&self, pfd: RawFd, name: &CStr, m: &FileMeta) -> io::Result<()> {
let fd = openat(pfd, name, libc::O_PATH | libc::O_NOFOLLOW, 0)?;
if is(&fstat(fd.as_raw_fd())?, libc::S_IFLNK) {
return Err(escape());
}
if self.same_owner {
cvt(unsafe {
libc::fchownat(
fd.as_raw_fd(),
c"".as_ptr(),
m.uid as u32,
m.gid as u32,
libc::AT_EMPTY_PATH,
)
})?;
}
let proc = cstr(OsStr::new(&format!("/proc/self/fd/{}", fd.as_raw_fd())))?;
cvt(unsafe { libc::chmod(proc.as_ptr(), m.mode) })?;
let t = times(m.mtime);
cvt(unsafe { libc::utimensat(libc::AT_FDCWD, proc.as_ptr(), t.as_ptr(), 0) })?;
Ok(())
}
fn entry<R: Read>(&mut self, entry: &mut tar::Entry<'_, R>, pool: &mut Pool) -> Result<()> {
self.stats.entries += 1;
let raw = entry.path().ctx(|| "entry path".into())?.into_owned();
let Some(rel) = entry_path(&raw) else {
if raw
.components()
.any(|c| matches!(c, Component::ParentDir | Component::Prefix(_)))
{
return Err(Error::Escape(raw));
}
return Ok(());
};
let (parent, name) = split(&rel);
let name_s = name.to_str().unwrap_or("");
if name_s == ".wh..wh..opq" {
return self.whiteout(parent, None);
}
if let Some(hidden) = name_s.strip_prefix(".wh.") {
if matches!(hidden, "" | "." | "..") {
return Err(Error::Escape(rel));
}
return self.whiteout(parent, Some(hidden));
}
let h = entry.header();
let kind = h.entry_type();
let known = kind.is_dir()
|| kind.is_file()
|| kind == tar::EntryType::Continuous
|| kind.is_symlink()
|| kind.is_hard_link()
|| kind.is_fifo()
|| kind.is_character_special()
|| kind.is_block_special();
if !known {
return Ok(());
}
let m = FileMeta {
mode: h.mode().unwrap_or(0o644) & 0o7777,
uid: h.uid().unwrap_or(0),
gid: h.gid().unwrap_or(0),
mtime: h.mtime().unwrap_or(0),
};
let pfd = self.parent(&rel, true)?.expect("created");
let p = pfd.as_raw_fd();
let c = cstr(name).map_err(|e| err(e, &rel))?;
let io = |e: io::Error| err(e, &rel);
if let Some(st) = fstatat(p, &c).map_err(io)? {
if !(is(&st, libc::S_IFDIR) && kind.is_dir()) {
remove_at(p, &c).map_err(io)?;
}
}
if kind.is_dir() {
match cvt(unsafe { libc::mkdirat(p, c.as_ptr(), 0o700) }) {
Ok(_) => {}
Err(e) if e.raw_os_error() == Some(libc::EEXIST) => {}
Err(e) => return Err(io(e)),
}
let d = openat(
p,
&c,
libc::O_RDONLY | libc::O_DIRECTORY | libc::O_NOFOLLOW,
0,
)
.map_err(io)?;
if self.same_owner {
cvt(unsafe { libc::fchown(d.as_raw_fd(), m.uid as u32, m.gid as u32) })
.map_err(io)?;
}
cvt(unsafe { libc::fchmod(d.as_raw_fd(), m.mode) }).map_err(io)?;
self.dirs.push((rel.clone(), m.mtime));
} else if kind.is_file() || kind == tar::EntryType::Continuous {
let fd = openat(
p,
&c,
libc::O_WRONLY | libc::O_CREAT | libc::O_EXCL | libc::O_NOFOLLOW,
0o600,
)
.map_err(io)?;
let mut f = File::from(fd);
let size = entry.size();
if pool.active() && size > 0 && size <= SMALL_FILE {
let mut data = Vec::with_capacity(size as usize);
entry
.read_to_end(&mut data)
.ctx(|| format!("reading {}", rel.display()))?;
pool.submit(Job {
file: f,
data,
meta: m,
rel: rel.clone(),
})?;
} else {
io::copy(entry, &mut f).ctx(|| format!("writing {}", rel.display()))?;
finish_file(&f, &m, self.same_owner).map_err(io)?;
}
self.stats.files += 1;
self.stats.bytes += size;
} else if kind.is_symlink() {
let target = entry
.link_name()
.ctx(|| format!("link of {}", rel.display()))?
.ok_or_else(|| {
Error::Invalid(format!("symlink {} has no target", rel.display()))
})?;
let t = cstr(target.as_os_str()).map_err(io)?;
cvt(unsafe { libc::symlinkat(t.as_ptr(), p, c.as_ptr()) }).map_err(io)?;
if self.same_owner {
cvt(unsafe {
libc::fchownat(
p,
c.as_ptr(),
m.uid as u32,
m.gid as u32,
libc::AT_SYMLINK_NOFOLLOW,
)
})
.map_err(io)?;
}
let ts = times(m.mtime);
cvt(unsafe { libc::utimensat(p, c.as_ptr(), ts.as_ptr(), libc::AT_SYMLINK_NOFOLLOW) })
.map_err(io)?;
} else if kind.is_hard_link() {
let target = entry
.link_name()
.ctx(|| format!("link of {}", rel.display()))?
.ok_or_else(|| {
Error::Invalid(format!("hard link {} has no target", rel.display()))
})?;
let src = entry_path(&target).ok_or_else(|| Error::Escape(target.to_path_buf()))?;
let spfd = self
.root
.dir(split(&src).0, false)
.map_err(|e| err(e, &src))?;
let sc = cstr(split(&src).1).map_err(io)?;
cvt(unsafe { libc::linkat(spfd.as_raw_fd(), sc.as_ptr(), p, c.as_ptr(), 0) })
.map_err(io)?;
} else {
let (t, dev) = if kind.is_fifo() {
(libc::S_IFIFO, 0)
} else {
let maj = h.device_major().ok().flatten().unwrap_or(0);
let min = h.device_minor().ok().flatten().unwrap_or(0);
let t = if kind.is_character_special() {
libc::S_IFCHR
} else {
libc::S_IFBLK
};
(t, libc::makedev(maj, min))
};
match cvt(unsafe { libc::mknodat(p, c.as_ptr(), t | 0o600, dev) }) {
Ok(_) => self.special_meta(p, &c, &m).map_err(io)?,
Err(_) if !kind.is_fifo() => {
self.stats.skipped_devices += 1;
return Ok(());
}
Err(e) => return Err(io(e)),
}
}
add_with_parents(&mut self.ours, &rel);
Ok(())
}
}
pub enum Change<'a, 'b, R: Read> {
Opaque(PathBuf),
Whiteout(PathBuf),
Entry(PathBuf, &'a mut tar::Entry<'b, R>),
}
pub fn changes<R: Read>(
tar: R,
mut f: impl FnMut(Change<'_, '_, R>) -> io::Result<()>,
) -> Result<()> {
let mut archive = tar::Archive::new(tar);
for entry in archive.entries().ctx(|| "reading layer".into())? {
let mut entry = entry.ctx(|| "reading layer".into())?;
let raw = entry.path().ctx(|| "entry path".into())?.into_owned();
let Some(rel) = entry_path(&raw) else {
continue;
};
let name = rel.file_name().and_then(|n| n.to_str()).unwrap_or("");
let parent = rel.parent().unwrap_or(Path::new("")).to_path_buf();
if name
.strip_prefix(".wh.")
.is_some_and(|h| matches!(h, "" | "." | ".."))
{
continue;
}
let change = if name == ".wh..wh..opq" {
Change::Opaque(parent)
} else if let Some(hidden) = name.strip_prefix(".wh.") {
Change::Whiteout(parent.join(hidden))
} else {
Change::Entry(rel, &mut entry)
};
f(change).ctx(|| "layer visitor".into())?;
}
Ok(())
}
pub fn entry_path(path: &Path) -> Option<PathBuf> {
let mut out = PathBuf::new();
for c in path.components() {
match c {
Component::Normal(p) => out.push(p),
Component::CurDir | Component::RootDir => {}
Component::ParentDir | Component::Prefix(_) => return None,
}
}
(!out.as_os_str().is_empty()).then_some(out)
}
fn add_with_parents(set: &mut HashSet<PathBuf>, rel: &Path) {
let mut p = Some(rel);
while let Some(q) = p.filter(|q| !q.as_os_str().is_empty()) {
if !set.insert(q.to_path_buf()) {
break;
}
p = q.parent();
}
}
struct Job {
file: File,
data: Vec<u8>,
meta: FileMeta,
rel: PathBuf,
}
#[derive(Default)]
struct State {
pending: usize,
bytes: usize,
error: Option<Error>,
}
#[derive(Default)]
struct Shared {
state: Mutex<State>,
cv: Condvar,
}
struct Pool {
tx: Option<mpsc::Sender<Job>>,
shared: Arc<Shared>,
}
impl Pool {
fn new<'s>(s: &'s thread::Scope<'s, '_>, workers: usize, same_owner: bool) -> Self {
let shared = Arc::new(Shared::default());
if workers == 0 {
return Self { tx: None, shared };
}
let (tx, rx) = mpsc::channel::<Job>();
let rx = Arc::new(Mutex::new(rx));
for _ in 0..workers {
let (rx, shared) = (rx.clone(), shared.clone());
s.spawn(move || loop {
let job = rx.lock().unwrap().recv();
let Ok(mut job) = job else { return };
let len = job.data.len();
let r = job
.file
.write_all(&job.data)
.and_then(|()| finish_file(&job.file, &job.meta, same_owner))
.ctx(|| format!("writing {}", job.rel.display()));
drop(job);
let mut st = shared.state.lock().unwrap();
st.pending -= 1;
st.bytes -= len;
if let Err(e) = r {
st.error.get_or_insert(e);
}
shared.cv.notify_all();
});
}
Self {
tx: Some(tx),
shared,
}
}
fn active(&self) -> bool {
self.tx.is_some()
}
fn submit(&mut self, job: Job) -> Result<()> {
let len = job.data.len();
let mut st = self.shared.state.lock().unwrap();
while st.pending > 0
&& (st.bytes + len > IN_FLIGHT || st.pending >= OPEN_FILES)
&& st.error.is_none()
{
st = self.shared.cv.wait(st).unwrap();
}
if let Some(e) = st.error.take() {
return Err(e);
}
st.pending += 1;
st.bytes += len;
drop(st);
self.tx
.as_ref()
.unwrap()
.send(job)
.expect("writer threads live");
Ok(())
}
fn finish(mut self) -> Result<()> {
let mut st = self.shared.state.lock().unwrap();
while st.pending > 0 {
st = self.shared.cv.wait(st).unwrap();
}
let r = st.error.take().map_or(Ok(()), Err);
drop(st);
self.tx = None;
r
}
}
fn read_ahead<'s, R: Read + Send + 's>(
s: &'s thread::Scope<'s, '_>,
mut inner: R,
) -> ChannelReader {
let (tx, rx) = mpsc::sync_channel::<io::Result<Vec<u8>>>(4);
s.spawn(move || loop {
let mut buf = vec![0u8; CHUNK];
let mut n = 0;
while n < CHUNK {
match inner.read(&mut buf[n..]) {
Ok(0) => break,
Ok(k) => n += k,
Err(e) if e.kind() == io::ErrorKind::Interrupted => {}
Err(e) => {
let _ = tx.send(Err(e));
return;
}
}
}
buf.truncate(n);
let done = n < CHUNK;
if (n > 0 && tx.send(Ok(buf)).is_err()) || done {
return;
}
});
ChannelReader {
rx,
buf: Vec::new(),
pos: 0,
}
}
struct ChannelReader {
rx: mpsc::Receiver<io::Result<Vec<u8>>>,
buf: Vec<u8>,
pos: usize,
}
impl Read for ChannelReader {
fn read(&mut self, out: &mut [u8]) -> io::Result<usize> {
while self.pos == self.buf.len() {
match self.rx.recv() {
Ok(chunk) => {
self.buf = chunk?;
self.pos = 0;
}
Err(_) => return Ok(0),
}
}
let n = out.len().min(self.buf.len() - self.pos);
out[..n].copy_from_slice(&self.buf[self.pos..self.pos + n]);
self.pos += n;
Ok(n)
}
}