Skip to main content

oci_image/
layer.rs

1//! One-pass layer unpack.
2//!
3//! The tar is read once, in order. Whiteouts (`.wh.<name>`) and opaque
4//! markers (`.wh..wh..opq`) remove what LOWER layers put there; anything this
5//! layer itself writes, before or after the marker, stays. That is the OCI
6//! rule, and it makes the result independent of where the markers sit in the
7//! tar, so no separate whiteout pass is needed.
8//!
9//! Nothing is written outside the rootfs, whatever the layer holds and
10//! whatever another process changes in the rootfs meanwhile: every entry's
11//! parent is resolved beneath the rootfs's descriptor (`openat2` with
12//! `RESOLVE_BENEATH`, or an `O_NOFOLLOW` walk), and every write is an `*at`
13//! call on that descriptor that never follows the last component. A symlink
14//! is resolved inside the rootfs; one that leads out (absolute, or `..` above
15//! the root) is an [`Error::Escape`] for anything written or removed below it.
16//!
17//! Directories, links, special files and the creation of every regular file
18//! happen in order on the calling thread. Regular files up to [`SMALL_FILE`]
19//! are read into memory and filled by a pool of worker threads (write, chown,
20//! chmod, mtime, all on the descriptor opened in place), with at most
21//! [`IN_FLIGHT`] bytes and [`OPEN_FILES`] files waiting.
22
23use std::collections::HashSet;
24use std::ffi::{CStr, OsStr};
25use std::fs::{self, File};
26use std::io::{self, Read, Write};
27use std::os::fd::{AsRawFd, OwnedFd, RawFd};
28use std::os::unix::ffi::OsStrExt;
29use std::os::unix::fs::MetadataExt;
30use std::path::{Component, Path, PathBuf};
31use std::sync::{mpsc, Arc, Condvar, Mutex};
32use std::thread;
33
34use crate::beneath::{
35    cstr, cvt, escape, fstat, fstatat, is, is_escape, list, openat, remove_at, Root,
36};
37use crate::decode::{open_layer, Decoder};
38use crate::error::Context;
39use crate::{Error, Result};
40
41/// Regular files up to this size go to the writer pool; larger ones are
42/// streamed to disk on the unpacking thread.
43pub const SMALL_FILE: u64 = 1 << 20;
44/// The most file bytes held in memory for the writer pool.
45pub const IN_FLIGHT: usize = 16 << 20;
46/// The most files open and waiting for the writer pool.
47pub const OPEN_FILES: usize = 256;
48/// Read-ahead chunk between the decoder thread and the tar reader.
49const CHUNK: usize = 1 << 20;
50
51/// How to unpack layers.
52#[derive(Debug, Clone)]
53pub struct UnpackOptions {
54    /// Threads for file writes and parallel gzip decode; 0 means all cores
55    /// this process may use. 1 unpacks on the calling thread only.
56    pub threads: usize,
57    /// Decode gzip layers with `lgz` on `threads` workers (false: one-core
58    /// zlib-rs).
59    pub parallel_gzip: bool,
60    /// Restore the uid/gid of every entry (needs root or CAP_CHOWN).
61    pub same_owner: bool,
62    /// Apply whiteouts onto `rootfs` (flatten), or keep them in overlayfs
63    /// form for a per-layer directory.
64    pub layout: Layout,
65    /// What to do with a layer's character and block devices (fifos are
66    /// always made). Default [`Devices::Skip`].
67    pub devices: Devices,
68}
69
70/// What an unpack does with a layer's device nodes.
71#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
72pub enum Devices {
73    /// Make none, and count each in [`UnpackStats::skipped_devices`]. A
74    /// layer comes from outside: a root that unpacks it must not get, say,
75    /// `/dev/sda` from it. A container's runtime makes its own `/dev`.
76    #[default]
77    Skip,
78    /// Make them where this process may (root, outside a user namespace);
79    /// where the kernel refuses, skip and count them.
80    Make,
81}
82
83/// What an unpack does with whiteouts.
84#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
85pub enum Layout {
86    /// Apply them: the layer lands on top of the layers already in `rootfs`.
87    #[default]
88    Flatten,
89    /// Keep them for overlayfs: `.wh.<name>` becomes a 0/0 character device,
90    /// `.wh..wh..opq` an `overlay.opaque` xattr on its directory. For one
91    /// layer into its own empty directory.
92    Overlay(OverlayXattr),
93}
94
95/// The xattr namespace overlayfs reads its markers from.
96#[derive(Debug, Clone, Copy, PartialEq, Eq)]
97pub enum OverlayXattr {
98    /// `trusted.overlay.*`: a mount by real root.
99    Trusted,
100    /// `user.overlay.*`, mounted with `userxattr` (in a user namespace).
101    User,
102}
103
104impl Default for UnpackOptions {
105    fn default() -> Self {
106        Self {
107            threads: 0,
108            parallel_gzip: true,
109            same_owner: fs::metadata("/proc/self").is_ok_and(|m| m.uid() == 0),
110            layout: Layout::Flatten,
111            devices: Devices::Skip,
112        }
113    }
114}
115
116impl UnpackOptions {
117    /// The thread count to use.
118    pub fn threads(&self) -> usize {
119        if self.threads > 0 {
120            self.threads
121        } else {
122            thread::available_parallelism().map_or(1, |n| n.get())
123        }
124    }
125}
126
127/// What one layer unpack did.
128#[derive(Debug, Clone, PartialEq, Eq)]
129pub struct UnpackStats {
130    pub decoder: Decoder,
131    /// Tar entries read.
132    pub entries: u64,
133    /// Regular files written.
134    pub files: u64,
135    /// Bytes of regular file content written.
136    pub bytes: u64,
137    /// Whiteout and opaque markers applied.
138    pub whiteouts: u64,
139    /// Device nodes that could not be created (in a user namespace) and were skipped.
140    pub skipped_devices: u64,
141}
142
143/// Apply the layer blob at `blob` (plain, gzip or zstd) onto `rootfs`.
144pub fn apply_layer_file(blob: &Path, rootfs: &Path, opts: &UnpackOptions) -> Result<UnpackStats> {
145    let (reader, decoder, _) = open_layer(blob, opts, false)?;
146    let mut stats = apply_layer(reader, rootfs, opts)?;
147    stats.decoder = decoder;
148    Ok(stats)
149}
150
151/// [`apply_layer_file`], also returning the sha256 of the uncompressed tar
152/// (the layer's diff_id). On the lgz path it is computed on one more thread
153/// from the decoder's in-order output, beside the tar reader: no extra pass.
154pub fn apply_layer_file_digest(
155    blob: &Path,
156    rootfs: &Path,
157    opts: &UnpackOptions,
158) -> Result<(UnpackStats, crate::Digest)> {
159    let (reader, decoder, digest) = open_layer(blob, opts, true)?;
160    let mut stats = apply_layer(reader, rootfs, opts)?;
161    stats.decoder = decoder;
162    let digest = digest
163        .finish()
164        .ok_or_else(|| Error::Invalid(format!("hashing layer {} failed", blob.display())))?;
165    Ok((stats, digest))
166}
167
168/// Apply one uncompressed layer tar onto `rootfs`.
169///
170/// A refused layer leaves what it wrote so far (inside the rootfs): unpack
171/// into a fresh directory and remove it on error, as [`crate::LayerStore`]
172/// does.
173pub fn apply_layer<R: Read + Send>(
174    tar: R,
175    rootfs: &Path,
176    opts: &UnpackOptions,
177) -> Result<UnpackStats> {
178    let threads = opts.threads();
179    let root = Root::open(rootfs).ctx(|| format!("rootfs {}", rootfs.display()))?;
180    let mut w = Writer {
181        root: &root,
182        same_owner: opts.same_owner,
183        layout: opts.layout,
184        devices: opts.devices,
185        ours: HashSet::new(),
186        dirs: Vec::new(),
187        stats: UnpackStats {
188            decoder: Decoder::Plain,
189            entries: 0,
190            files: 0,
191            bytes: 0,
192            whiteouts: 0,
193            skipped_devices: 0,
194        },
195    };
196
197    thread::scope(|s| -> Result<()> {
198        let reader: Box<dyn Read> = if threads > 1 {
199            Box::new(read_ahead(s, tar))
200        } else {
201            Box::new(tar)
202        };
203        let mut pool = Pool::new(s, threads.saturating_sub(1), opts.same_owner);
204        let mut archive = tar::Archive::new(reader);
205        let r = (|| -> Result<()> {
206            for entry in archive.entries().ctx(|| "reading layer".into())? {
207                let mut entry = entry.ctx(|| "reading layer".into())?;
208                w.entry(&mut entry, &mut pool)?;
209            }
210            Ok(())
211        })();
212        // The pool is joined either way; the first error wins.
213        let done = pool.finish();
214        r?;
215        done?;
216        // Read to the end so the gzip/zstd trailer is checked.
217        let mut rest = archive.into_inner();
218        io::copy(&mut rest, &mut io::sink()).ctx(|| "reading the layer to its end".into())?;
219        Ok(())
220    })?;
221
222    // Directory mtimes last, deepest first: writing into a directory moves it.
223    for (rel, mtime) in w.dirs.iter().rev() {
224        if let Ok(Some(d)) = w.open_dir(rel, false) {
225            let t = times(*mtime);
226            unsafe { libc::futimens(d.as_raw_fd(), t.as_ptr()) };
227        }
228    }
229    Ok(w.stats)
230}
231
232/// Owner, mode and mtime of an entry.
233struct FileMeta {
234    mode: u32,
235    uid: u64,
236    gid: u64,
237    mtime: u64,
238}
239
240fn times(mtime: u64) -> [libc::timespec; 2] {
241    let t = libc::timespec {
242        tv_sec: mtime as libc::time_t,
243        tv_nsec: 0,
244    };
245    [t, t]
246}
247
248/// Owner, mode and mtime of a file just filled, on its descriptor.
249fn finish_file(f: &File, meta: &FileMeta, same_owner: bool) -> io::Result<()> {
250    let fd = f.as_raw_fd();
251    if same_owner {
252        cvt(unsafe { libc::fchown(fd, meta.uid as u32, meta.gid as u32) })?;
253    }
254    cvt(unsafe { libc::fchmod(fd, meta.mode) })?;
255    let t = times(meta.mtime);
256    cvt(unsafe { libc::futimens(fd, t.as_ptr()) })?;
257    Ok(())
258}
259
260/// `EXDEV` (the path leaves the rootfs) is an escape; anything else an io
261/// error about `rel`.
262fn err(e: io::Error, rel: &Path) -> Error {
263    if is_escape(&e) {
264        Error::Escape(rel.to_path_buf())
265    } else {
266        Error::Io {
267            context: format!("unpacking {}", rel.display()),
268            source: e,
269        }
270    }
271}
272
273fn split(rel: &Path) -> (&Path, &OsStr) {
274    (
275        rel.parent().unwrap_or(Path::new("")),
276        rel.file_name().unwrap_or_default(),
277    )
278}
279
280struct Writer<'r> {
281    root: &'r Root,
282    same_owner: bool,
283    layout: Layout,
284    devices: Devices,
285    /// Paths this layer has put down (and their parents): whiteouts only
286    /// hide lower layers.
287    ours: HashSet<PathBuf>,
288    dirs: Vec<(PathBuf, u64)>,
289    stats: UnpackStats,
290}
291
292impl Writer<'_> {
293    /// The parent directory of `rel`, resolved beneath the root. Without
294    /// `create`, `None` if it is not there.
295    fn parent(&self, rel: &Path, create: bool) -> Result<Option<OwnedFd>> {
296        match self.root.dir(split(rel).0, create) {
297            Ok(fd) => Ok(Some(fd)),
298            Err(e) if !create && matches!(e.raw_os_error(), Some(libc::ENOENT | libc::ENOTDIR)) => {
299                Ok(None)
300            }
301            Err(e) => Err(err(e, rel)),
302        }
303    }
304
305    /// The directory `rel` itself, opened readable (for listing, xattrs and
306    /// times).
307    fn open_dir(&self, rel: &Path, create: bool) -> Result<Option<OwnedFd>> {
308        let p = match self.root.dir(rel, create) {
309            Ok(p) => p,
310            Err(e) if !create && matches!(e.raw_os_error(), Some(libc::ENOENT | libc::ENOTDIR)) => {
311                return Ok(None)
312            }
313            Err(e) => return Err(err(e, rel)),
314        };
315        openat(p.as_raw_fd(), c".", libc::O_RDONLY | libc::O_DIRECTORY, 0)
316            .map(Some)
317            .map_err(|e| err(e, rel))
318    }
319
320    /// Hide `rel` from lower layers: remove it, unless this layer wrote it;
321    /// in a directory this layer wrote into, hide what is inside instead.
322    fn hide(&self, rel: &Path) -> Result<()> {
323        let Some(pfd) = self.parent(rel, false)? else {
324            return Ok(());
325        };
326        let c = cstr(split(rel).1).map_err(|e| err(e, rel))?;
327        if !self.ours.contains(rel) {
328            return remove_at(pfd.as_raw_fd(), &c).map_err(|e| err(e, rel));
329        }
330        let st = fstatat(pfd.as_raw_fd(), &c).map_err(|e| err(e, rel))?;
331        if st.is_some_and(|st| is(&st, libc::S_IFDIR)) {
332            let d = openat(
333                pfd.as_raw_fd(),
334                &c,
335                libc::O_RDONLY | libc::O_DIRECTORY | libc::O_NOFOLLOW,
336                0,
337            )
338            .map_err(|e| err(e, rel))?;
339            for child in list(d.as_raw_fd()).map_err(|e| err(e, rel))? {
340                self.hide(&rel.join(OsStr::from_bytes(child.to_bytes())))?;
341            }
342        }
343        Ok(())
344    }
345
346    fn whiteout(&mut self, parent: &Path, hidden: Option<&str>) -> Result<()> {
347        self.stats.whiteouts += 1;
348        match (self.layout, hidden) {
349            (Layout::Flatten, None) => {
350                if let Some(d) = self.open_dir(parent, false)? {
351                    for child in list(d.as_raw_fd()).map_err(|e| err(e, parent))? {
352                        self.hide(&parent.join(OsStr::from_bytes(child.to_bytes())))?;
353                    }
354                }
355            }
356            (Layout::Flatten, Some(h)) => self.hide(&parent.join(h))?,
357            (Layout::Overlay(xattr), None) => {
358                let d = self.open_dir(parent, true)?.expect("created");
359                let name: &CStr = match xattr {
360                    OverlayXattr::Trusted => c"trusted.overlay.opaque",
361                    OverlayXattr::User => c"user.overlay.opaque",
362                };
363                cvt(unsafe {
364                    libc::fsetxattr(d.as_raw_fd(), name.as_ptr(), b"y".as_ptr().cast(), 1, 0)
365                })
366                .map_err(|e| err(e, parent))?;
367                add_with_parents(&mut self.ours, parent);
368            }
369            (Layout::Overlay(_), Some(h)) => {
370                let rel = parent.join(h);
371                let pfd = self.parent(&rel, true)?.expect("created");
372                let c = cstr(OsStr::new(h)).map_err(|e| err(e, &rel))?;
373                remove_at(pfd.as_raw_fd(), &c).map_err(|e| err(e, &rel))?;
374                // An overlayfs whiteout: a character device 0/0.
375                cvt(unsafe { libc::mknodat(pfd.as_raw_fd(), c.as_ptr(), libc::S_IFCHR, 0) })
376                    .map_err(|e| err(e, &rel))?;
377                add_with_parents(&mut self.ours, &rel);
378            }
379        }
380        Ok(())
381    }
382
383    /// Owner, mode and times of a fifo or device just made, through an
384    /// `O_PATH` descriptor of it (`/proc/self/fd`), so a swap cannot redirect
385    /// them.
386    fn special_meta(&self, pfd: RawFd, name: &CStr, m: &FileMeta) -> io::Result<()> {
387        let fd = openat(pfd, name, libc::O_PATH | libc::O_NOFOLLOW, 0)?;
388        if is(&fstat(fd.as_raw_fd())?, libc::S_IFLNK) {
389            return Err(escape());
390        }
391        if self.same_owner {
392            cvt(unsafe {
393                libc::fchownat(
394                    fd.as_raw_fd(),
395                    c"".as_ptr(),
396                    m.uid as u32,
397                    m.gid as u32,
398                    libc::AT_EMPTY_PATH,
399                )
400            })?;
401        }
402        let proc = cstr(OsStr::new(&format!("/proc/self/fd/{}", fd.as_raw_fd())))?;
403        cvt(unsafe { libc::chmod(proc.as_ptr(), m.mode) })?;
404        let t = times(m.mtime);
405        cvt(unsafe { libc::utimensat(libc::AT_FDCWD, proc.as_ptr(), t.as_ptr(), 0) })?;
406        Ok(())
407    }
408
409    fn entry<R: Read>(&mut self, entry: &mut tar::Entry<'_, R>, pool: &mut Pool) -> Result<()> {
410        self.stats.entries += 1;
411        let raw = entry.path().ctx(|| "entry path".into())?.into_owned();
412        let Some(rel) = entry_path(&raw) else {
413            // The root itself is skipped; a `..` is a hostile layer.
414            if raw
415                .components()
416                .any(|c| matches!(c, Component::ParentDir | Component::Prefix(_)))
417            {
418                return Err(Error::Escape(raw));
419            }
420            return Ok(());
421        };
422        let (parent, name) = split(&rel);
423        let name_s = name.to_str().unwrap_or("");
424        if name_s == ".wh..wh..opq" {
425            return self.whiteout(parent, None);
426        }
427        if let Some(hidden) = name_s.strip_prefix(".wh.") {
428            // `.wh..` and `.wh...` would hide the directory itself or the one
429            // above it (for a top-level entry: the rootfs, or its parent).
430            if matches!(hidden, "" | "." | "..") {
431                return Err(Error::Escape(rel));
432            }
433            return self.whiteout(parent, Some(hidden));
434        }
435
436        let h = entry.header();
437        let kind = h.entry_type();
438        let known = kind.is_dir()
439            || kind.is_file()
440            || kind == tar::EntryType::Continuous
441            || kind.is_symlink()
442            || kind.is_hard_link()
443            || kind.is_fifo()
444            || kind.is_character_special()
445            || kind.is_block_special();
446        if !known {
447            return Ok(());
448        }
449        let m = FileMeta {
450            mode: h.mode().unwrap_or(0o644) & 0o7777,
451            uid: h.uid().unwrap_or(0),
452            gid: h.gid().unwrap_or(0),
453            mtime: h.mtime().unwrap_or(0),
454        };
455        let pfd = self.parent(&rel, true)?.expect("created");
456        let p = pfd.as_raw_fd();
457        let c = cstr(name).map_err(|e| err(e, &rel))?;
458        let io = |e: io::Error| err(e, &rel);
459
460        // A file replacing a directory (or the reverse) needs the old one gone.
461        if let Some(st) = fstatat(p, &c).map_err(io)? {
462            if !(is(&st, libc::S_IFDIR) && kind.is_dir()) {
463                remove_at(p, &c).map_err(io)?;
464            }
465        }
466
467        if kind.is_dir() {
468            match cvt(unsafe { libc::mkdirat(p, c.as_ptr(), 0o700) }) {
469                Ok(_) => {}
470                Err(e) if e.raw_os_error() == Some(libc::EEXIST) => {}
471                Err(e) => return Err(io(e)),
472            }
473            let d = openat(
474                p,
475                &c,
476                libc::O_RDONLY | libc::O_DIRECTORY | libc::O_NOFOLLOW,
477                0,
478            )
479            .map_err(io)?;
480            if self.same_owner {
481                cvt(unsafe { libc::fchown(d.as_raw_fd(), m.uid as u32, m.gid as u32) })
482                    .map_err(io)?;
483            }
484            cvt(unsafe { libc::fchmod(d.as_raw_fd(), m.mode) }).map_err(io)?;
485            self.dirs.push((rel.clone(), m.mtime));
486        } else if kind.is_file() || kind == tar::EntryType::Continuous {
487            let fd = openat(
488                p,
489                &c,
490                libc::O_WRONLY | libc::O_CREAT | libc::O_EXCL | libc::O_NOFOLLOW,
491                0o600,
492            )
493            .map_err(io)?;
494            let mut f = File::from(fd);
495            let size = entry.size();
496            if pool.active() && size > 0 && size <= SMALL_FILE {
497                let mut data = Vec::with_capacity(size as usize);
498                entry
499                    .read_to_end(&mut data)
500                    .ctx(|| format!("reading {}", rel.display()))?;
501                pool.submit(Job {
502                    file: f,
503                    data,
504                    meta: m,
505                    rel: rel.clone(),
506                })?;
507            } else {
508                io::copy(entry, &mut f).ctx(|| format!("writing {}", rel.display()))?;
509                finish_file(&f, &m, self.same_owner).map_err(io)?;
510            }
511            self.stats.files += 1;
512            self.stats.bytes += size;
513        } else if kind.is_symlink() {
514            let target = entry
515                .link_name()
516                .ctx(|| format!("link of {}", rel.display()))?
517                .ok_or_else(|| {
518                    Error::Invalid(format!("symlink {} has no target", rel.display()))
519                })?;
520            let t = cstr(target.as_os_str()).map_err(io)?;
521            cvt(unsafe { libc::symlinkat(t.as_ptr(), p, c.as_ptr()) }).map_err(io)?;
522            if self.same_owner {
523                cvt(unsafe {
524                    libc::fchownat(
525                        p,
526                        c.as_ptr(),
527                        m.uid as u32,
528                        m.gid as u32,
529                        libc::AT_SYMLINK_NOFOLLOW,
530                    )
531                })
532                .map_err(io)?;
533            }
534            let ts = times(m.mtime);
535            cvt(unsafe { libc::utimensat(p, c.as_ptr(), ts.as_ptr(), libc::AT_SYMLINK_NOFOLLOW) })
536                .map_err(io)?;
537        } else if kind.is_hard_link() {
538            let target = entry
539                .link_name()
540                .ctx(|| format!("link of {}", rel.display()))?
541                .ok_or_else(|| {
542                    Error::Invalid(format!("hard link {} has no target", rel.display()))
543                })?;
544            let src = entry_path(&target).ok_or_else(|| Error::Escape(target.to_path_buf()))?;
545            let spfd = self
546                .root
547                .dir(split(&src).0, false)
548                .map_err(|e| err(e, &src))?;
549            let sc = cstr(split(&src).1).map_err(io)?;
550            // Flags 0: a symlink source is linked itself, never followed.
551            cvt(unsafe { libc::linkat(spfd.as_raw_fd(), sc.as_ptr(), p, c.as_ptr(), 0) })
552                .map_err(io)?;
553        } else if !kind.is_fifo() && self.devices == Devices::Skip {
554            self.stats.skipped_devices += 1;
555            return Ok(());
556        } else {
557            let (t, dev) = if kind.is_fifo() {
558                (libc::S_IFIFO, 0)
559            } else {
560                let maj = h.device_major().ok().flatten().unwrap_or(0);
561                let min = h.device_minor().ok().flatten().unwrap_or(0);
562                let t = if kind.is_character_special() {
563                    libc::S_IFCHR
564                } else {
565                    libc::S_IFBLK
566                };
567                (t, libc::makedev(maj, min))
568            };
569            match cvt(unsafe { libc::mknodat(p, c.as_ptr(), t | 0o600, dev) }) {
570                Ok(_) => self.special_meta(p, &c, &m).map_err(io)?,
571                // Device nodes cannot be created in a user namespace; a
572                // container gets its own /dev anyway.
573                Err(_) if !kind.is_fifo() => {
574                    self.stats.skipped_devices += 1;
575                    return Ok(());
576                }
577                Err(e) => return Err(io(e)),
578            }
579        }
580        add_with_parents(&mut self.ours, &rel);
581        Ok(())
582    }
583}
584
585/// One step of a layer, as [`changes`] reports it.
586pub enum Change<'a, 'b, R: Read> {
587    /// `.wh..wh..opq` in this directory: lower layers' contents are hidden.
588    Opaque(PathBuf),
589    /// `.wh.<name>`: this path from lower layers is hidden.
590    Whiteout(PathBuf),
591    /// Any other entry, with its clean relative path.
592    Entry(PathBuf, &'a mut tar::Entry<'b, R>),
593}
594
595/// Walk an uncompressed layer tar in order, classifying whiteouts, for
596/// readers that keep their own view of the filesystem (scanners, catalogers).
597/// Entries whose path would escape the root, and whiteouts of `.` or `..`,
598/// are skipped.
599pub fn changes<R: Read>(
600    tar: R,
601    mut f: impl FnMut(Change<'_, '_, R>) -> io::Result<()>,
602) -> Result<()> {
603    let mut archive = tar::Archive::new(tar);
604    for entry in archive.entries().ctx(|| "reading layer".into())? {
605        let mut entry = entry.ctx(|| "reading layer".into())?;
606        let raw = entry.path().ctx(|| "entry path".into())?.into_owned();
607        let Some(rel) = entry_path(&raw) else {
608            continue;
609        };
610        let name = rel.file_name().and_then(|n| n.to_str()).unwrap_or("");
611        let parent = rel.parent().unwrap_or(Path::new("")).to_path_buf();
612        if name
613            .strip_prefix(".wh.")
614            .is_some_and(|h| matches!(h, "" | "." | ".."))
615        {
616            continue;
617        }
618        let change = if name == ".wh..wh..opq" {
619            Change::Opaque(parent)
620        } else if let Some(hidden) = name.strip_prefix(".wh.") {
621            Change::Whiteout(parent.join(hidden))
622        } else {
623            Change::Entry(rel, &mut entry)
624        };
625        f(change).ctx(|| "layer visitor".into())?;
626    }
627    Ok(())
628}
629
630/// Clean relative path of a tar entry, or `None` if it would escape (or is
631/// the root itself).
632pub fn entry_path(path: &Path) -> Option<PathBuf> {
633    let mut out = PathBuf::new();
634    for c in path.components() {
635        match c {
636            Component::Normal(p) => out.push(p),
637            Component::CurDir | Component::RootDir => {}
638            Component::ParentDir | Component::Prefix(_) => return None,
639        }
640    }
641    (!out.as_os_str().is_empty()).then_some(out)
642}
643
644fn add_with_parents(set: &mut HashSet<PathBuf>, rel: &Path) {
645    let mut p = Some(rel);
646    while let Some(q) = p.filter(|q| !q.as_os_str().is_empty()) {
647        if !set.insert(q.to_path_buf()) {
648            break;
649        }
650        p = q.parent();
651    }
652}
653
654/// A small file created in place, to be filled by the pool.
655struct Job {
656    file: File,
657    data: Vec<u8>,
658    meta: FileMeta,
659    rel: PathBuf,
660}
661
662#[derive(Default)]
663struct State {
664    pending: usize,
665    bytes: usize,
666    error: Option<Error>,
667}
668
669#[derive(Default)]
670struct Shared {
671    state: Mutex<State>,
672    cv: Condvar,
673}
674
675/// File writer threads.
676struct Pool {
677    tx: Option<mpsc::Sender<Job>>,
678    shared: Arc<Shared>,
679}
680
681impl Pool {
682    fn new<'s>(s: &'s thread::Scope<'s, '_>, workers: usize, same_owner: bool) -> Self {
683        let shared = Arc::new(Shared::default());
684        if workers == 0 {
685            return Self { tx: None, shared };
686        }
687        let (tx, rx) = mpsc::channel::<Job>();
688        let rx = Arc::new(Mutex::new(rx));
689        for _ in 0..workers {
690            let (rx, shared) = (rx.clone(), shared.clone());
691            // A32-14, stays a thread: a scoped file writer of one layer's unpack, joined when
692            // the unpack's thread::scope ends.
693            s.spawn(move || loop {
694                let job = rx.lock().unwrap().recv();
695                let Ok(mut job) = job else { return };
696                let len = job.data.len();
697                let r = job
698                    .file
699                    .write_all(&job.data)
700                    .and_then(|()| finish_file(&job.file, &job.meta, same_owner))
701                    .ctx(|| format!("writing {}", job.rel.display()));
702                drop(job);
703                let mut st = shared.state.lock().unwrap();
704                st.pending -= 1;
705                st.bytes -= len;
706                if let Err(e) = r {
707                    st.error.get_or_insert(e);
708                }
709                shared.cv.notify_all();
710            });
711        }
712        Self {
713            tx: Some(tx),
714            shared,
715        }
716    }
717
718    fn active(&self) -> bool {
719        self.tx.is_some()
720    }
721
722    fn submit(&mut self, job: Job) -> Result<()> {
723        let len = job.data.len();
724        let mut st = self.shared.state.lock().unwrap();
725        while st.pending > 0
726            && (st.bytes + len > IN_FLIGHT || st.pending >= OPEN_FILES)
727            && st.error.is_none()
728        {
729            st = self.shared.cv.wait(st).unwrap();
730        }
731        if let Some(e) = st.error.take() {
732            return Err(e);
733        }
734        st.pending += 1;
735        st.bytes += len;
736        drop(st);
737        self.tx
738            .as_ref()
739            .unwrap()
740            .send(job)
741            .expect("writer threads live");
742        Ok(())
743    }
744
745    /// Wait for every submitted write, then stop the workers.
746    fn finish(mut self) -> Result<()> {
747        let mut st = self.shared.state.lock().unwrap();
748        while st.pending > 0 {
749            st = self.shared.cv.wait(st).unwrap();
750        }
751        let r = st.error.take().map_or(Ok(()), Err);
752        drop(st);
753        self.tx = None;
754        r
755    }
756}
757
758/// Decode on its own thread, a few chunks ahead of the tar reader.
759fn read_ahead<'s, R: Read + Send + 's>(
760    s: &'s thread::Scope<'s, '_>,
761    mut inner: R,
762) -> ChannelReader {
763    let (tx, rx) = mpsc::sync_channel::<io::Result<Vec<u8>>>(4);
764    // A32-14, stays a thread: the scoped read-ahead of one layer's tar, joined when the unpack's
765    // thread::scope ends.
766    s.spawn(move || loop {
767        let mut buf = vec![0u8; CHUNK];
768        let mut n = 0;
769        while n < CHUNK {
770            match inner.read(&mut buf[n..]) {
771                Ok(0) => break,
772                Ok(k) => n += k,
773                Err(e) if e.kind() == io::ErrorKind::Interrupted => {}
774                Err(e) => {
775                    let _ = tx.send(Err(e));
776                    return;
777                }
778            }
779        }
780        buf.truncate(n);
781        let done = n < CHUNK;
782        if (n > 0 && tx.send(Ok(buf)).is_err()) || done {
783            return;
784        }
785    });
786    ChannelReader {
787        rx,
788        buf: Vec::new(),
789        pos: 0,
790    }
791}
792
793struct ChannelReader {
794    rx: mpsc::Receiver<io::Result<Vec<u8>>>,
795    buf: Vec<u8>,
796    pos: usize,
797}
798
799impl Read for ChannelReader {
800    fn read(&mut self, out: &mut [u8]) -> io::Result<usize> {
801        while self.pos == self.buf.len() {
802            match self.rx.recv() {
803                Ok(chunk) => {
804                    self.buf = chunk?;
805                    self.pos = 0;
806                }
807                Err(_) => return Ok(0),
808            }
809        }
810        let n = out.len().min(self.buf.len() - self.pos);
811        out[..n].copy_from_slice(&self.buf[self.pos..self.pos + n]);
812        self.pos += n;
813        Ok(n)
814    }
815}