debrepo 0.4.0

Library for manifest-driven Debian/Ubuntu bootstrap and APT archive resolution.
Documentation
use {
    crate::{
        builder::{BuildJob, Executor},
        exec::{setup_root, spawn_helper, unshare_root, unshare_user_ns, HelperStatus},
        staging::HostFileSystem,
    },
    rustix::{
        fd::{AsFd, AsRawFd, FromRawFd, OwnedFd},
        fs::{openat, Mode, OFlags, CWD},
        io::{fcntl_getfd, fcntl_setfd, FdFlags},
        mount::{move_mount, open_tree, MoveMountFlags, OpenTreeFlags},
        path::Arg,
        pipe::{self},
        process::geteuid,
    },
    serde::{Deserialize, Serialize},
    std::{
        ffi::{OsStr, OsString},
        io::{self, Read, Write},
        iter::once,
        os::unix::fs::OpenOptionsExt,
        path::{Path, PathBuf},
        process::{Command, Stdio},
    },
};

#[derive(Serialize, Deserialize)]
/// Sandbox executor that runs commands on the host.
pub struct HostSandboxExecutor {
    root: PathBuf,
    env: Vec<(OsString, OsString)>,
}

impl HostSandboxExecutor {
    pub fn new<P: AsRef<Path>>(root: P) -> io::Result<Self> {
        if !geteuid().is_root() {
            unshare_user_ns(std::iter::empty::<&str>())?;
        }
        Ok(Self {
            root: root.as_ref().to_path_buf(),
            env: Vec::new(),
        })
    }
}

impl Executor for HostSandboxExecutor {
    type Filesystem = HostFileSystem;
    async fn execute(&mut self, job: BuildJob<Self>) -> io::Result<()> {
        Sandbox::execute_with(self, job).await
    }
    fn env<K, V>(&mut self, k: K, v: V) -> io::Result<()>
    where
        K: AsRef<OsStr>,
        V: AsRef<OsStr>,
    {
        self.env.push((k.as_ref().into(), v.as_ref().into()));
        Ok(())
    }
    fn envs<I, K, V>(&mut self, iter: I) -> io::Result<()>
    where
        I: IntoIterator<Item = (K, V)>,
        K: AsRef<OsStr>,
        V: AsRef<OsStr>,
    {
        self.env.extend(
            iter.into_iter()
                .map(|(k, v)| (k.as_ref().into(), v.as_ref().into())),
        );
        Ok(())
    }
    fn write_file<P: AsRef<Path>, C: AsRef<str>>(
        &self,
        path: P,
        mode: u32,
        content: C,
    ) -> io::Result<()> {
        std::fs::OpenOptions::new()
            .create(true)
            .write(true)
            .mode(mode)
            .open(path.as_ref())
            .and_then(|mut f| f.write_all(content.as_ref().as_bytes()))
    }
    fn remove_file<P: AsRef<Path>>(&self, path: P) -> io::Result<()> {
        std::fs::remove_file(path.as_ref())
    }
    fn exec_cmd<I, A, C>(&self, cmd: C, args: I) -> io::Result<()>
    where
        I: IntoIterator<Item = A>,
        A: AsRef<OsStr>,
        C: AsRef<OsStr>,
    {
        Command::new(cmd)
            .stdout(Stdio::inherit())
            .env_clear()
            .envs(self.env.iter().map(|(k, v)| (k, v)))
            .args(args)
            .status()?
            .success()
            .then_some(())
            .ok_or_else(|| io::Error::other("command failed"))
    }
    fn exec_script<S>(&self, script: S) -> io::Result<()>
    where
        S: AsRef<str> + Send + 'static,
    {
        let mut child = Command::new("/bin/sh")
            .stdin(Stdio::piped())
            .stdout(Stdio::inherit())
            .env_clear()
            .envs(self.env.iter().map(|(k, v)| (k, v)))
            .arg("-s")
            .spawn()?;
        let mut stdin = child
            .stdin
            .take()
            .ok_or_else(|| io::Error::other("failed to open stdin"))?;
        std::thread::spawn(move || {
            let _ = stdin.write_all(script.as_ref().as_bytes());
        });
        child
            .wait()?
            .success()
            .then_some(())
            .ok_or_else(|| io::Error::other("script failed"))
    }
}

impl SandboxExecutor for HostSandboxExecutor {
    fn spawn<E>(&mut self, job: BuildJob<Self>, spawner: E) -> io::Result<OwnedFd>
    where
        E: FnOnce(&Self, BuildJob<Self>) -> io::Result<OwnedFd>,
    {
        spawner(self, job)
    }
    fn setup_rootfs(&mut self) -> io::Result<OwnedFd> {
        let dfd = openat(
            CWD,
            &self.root,
            OFlags::RDONLY | OFlags::DIRECTORY | OFlags::CLOEXEC,
            Mode::empty(),
        )?;
        let mfd = open_tree(
            &dfd,
            "",
            OpenTreeFlags::OPEN_TREE_CLONE | OpenTreeFlags::AT_EMPTY_PATH,
        )?;
        move_mount(
            &mfd,
            "",
            &dfd,
            "",
            MoveMountFlags::MOVE_MOUNT_F_EMPTY_PATH | MoveMountFlags::MOVE_MOUNT_T_EMPTY_PATH,
        )?;
        Ok(openat(
            CWD,
            &self.root,
            OFlags::RDONLY | OFlags::DIRECTORY | OFlags::CLOEXEC,
            Mode::empty(),
        )?)
    }
}

pub trait SandboxExecutor
where
    Self: Executor + Serialize + for<'de> Deserialize<'de>,
{
    fn spawn<E>(&mut self, job: BuildJob<Self>, spawner: E) -> io::Result<OwnedFd>
    where
        E: FnOnce(&Self, BuildJob<Self>) -> io::Result<OwnedFd>;
    fn setup_rootfs(&mut self) -> io::Result<OwnedFd>;
}

/// Sandbox wrapper with lifecycle and execution helpers.
pub struct Sandbox<'a, E: SandboxExecutor> {
    runner: &'a mut E,
}

impl<'a, E: SandboxExecutor> Sandbox<'a, E> {
    pub fn new(runner: &'a mut E) -> Self {
        Self { runner }
    }
    pub async fn execute_with(runner: &'a mut E, job: BuildJob<E>) -> io::Result<()> {
        Self::new(runner).execute(job.with_executor::<Self>()).await
    }
}

#[derive(Serialize)]
struct OutJob<'a, E, J>
where
    E: Serialize,
    J: Serialize,
{
    executor: &'a E,
    job: &'a J,
}
impl<E, J> OutJob<'_, E, J>
where
    E: Serialize,
    J: Serialize,
{
    fn write_to(&self, mut w: std::fs::File) -> io::Result<()> {
        let doc = toml_edit::ser::to_vec(&self).map_err(|err| {
            io::Error::other(format!(
                "failed to encode parameters to helper process: {}",
                err
            ))
        })?;
        w.write_all(&doc).map_err(|err| {
            io::Error::other(format!(
                "failed to send parameters to helper process: {}",
                err
            ))
        })?;
        Ok(())
    }
}

#[derive(Deserialize)]
struct InJob<E, J> {
    executor: E,
    job: J,
}
impl<E, J> InJob<E, J>
where
    E: for<'de> Deserialize<'de>,
    J: for<'de> Deserialize<'de>,
{
    fn read_from(mut r: std::fs::File) -> io::Result<Self> {
        let mut doc = Vec::new();
        r.read_to_end(&mut doc).map_err(|err| {
            io::Error::other(format!(
                "failed to read parameters from calling process: {}",
                err
            ))
        })?;
        toml_edit::de::from_slice(&doc).map_err(|err| {
            io::Error::other(format!(
                "failed to decode parameters from calling process: {}",
                err
            ))
        })
    }
}

impl<E: SandboxExecutor> Executor for Sandbox<'_, E> {
    type Filesystem = E::Filesystem;
    async fn prepare_tree(&mut self, _fs: &Self::Filesystem) -> io::Result<()> {
        unimplemented!()
    }
    async fn process_changes(&mut self, _fs: &Self::Filesystem) -> io::Result<()> {
        unimplemented!()
    }
    fn env<K, V>(&mut self, _k: K, _v: V) -> io::Result<()>
    where
        K: AsRef<OsStr>,
        V: AsRef<OsStr>,
    {
        unimplemented!()
    }
    fn envs<I, K, V>(&mut self, _iter: I) -> io::Result<()>
    where
        I: IntoIterator<Item = (K, V)>,
        K: AsRef<OsStr>,
        V: AsRef<OsStr>,
    {
        unimplemented!()
    }
    fn exec_cmd<I, A, C>(&self, _cmd: C, _args: I) -> io::Result<()>
    where
        I: IntoIterator<Item = A>,
        A: AsRef<OsStr>,
        C: AsRef<OsStr>,
    {
        unimplemented!()
    }
    fn exec_script<S>(&self, _script: S) -> io::Result<()>
    where
        S: AsRef<str>,
    {
        unimplemented!()
    }
    fn write_file<P: AsRef<Path>, C: AsRef<str>>(
        &self,
        _path: P,
        _mode: u32,
        _content: C,
    ) -> io::Result<()> {
        unimplemented!()
    }
    fn remove_file<P: AsRef<Path>>(&self, _path: P) -> io::Result<()> {
        unimplemented!()
    }
    async fn execute(&mut self, job: BuildJob<Self>) -> io::Result<()>
    where
        Self: Sized,
    {
        let pid = self.runner.spawn(job.with_executor::<E>(), |runner, job| {
            let (rfd, wfd) = pipe::pipe()?;
            set_cloexec(&wfd, true)?;
            let pid_fd = spawn_helper("runner", once(rfd.as_raw_fd().to_string()), true)?;
            drop(rfd);
            let wr: std::fs::File = wfd.into();
            OutJob {
                executor: runner,
                job: &job,
            }
            .write_to(wr)?;
            Ok(pid_fd)
        })?;
        HelperStatus::from(pid).status().await.and_then(|status| {
            if status.is_success() {
                Ok(())
            } else {
                Err(io::Error::other(format!(
                    "sandbox exited with code {}",
                    status.code()
                )))
            }
        })
    }
}

pub fn run_sandbox<E: SandboxExecutor>(mut args: std::env::ArgsOs) -> io::Result<()> {
    let rd: std::fs::File = args.fd_arg().map(Into::into)?;
    unshare_root()?;
    let mut job = InJob::<E, BuildJob<E>>::read_from(rd)?;
    let root_dfd = job.executor.setup_rootfs()?;
    setup_root(&root_dfd)?;
    let (job, mut executor) = (job.job, job.executor);
    job.run(&mut executor)
}

fn set_cloexec(fd: impl AsFd, cloexec: bool) -> io::Result<()> {
    let mut flags = fcntl_getfd(&fd)?;
    if cloexec {
        flags.insert(FdFlags::CLOEXEC);
    } else {
        flags.remove(FdFlags::CLOEXEC);
    }
    fcntl_setfd(&fd, flags).map_err(Into::into)
}

trait FdArg {
    fn fd_arg(&mut self) -> io::Result<OwnedFd>;
}

impl FdArg for std::env::ArgsOs {
    fn fd_arg(&mut self) -> io::Result<OwnedFd> {
        self.next()
            .ok_or_else(|| io::Error::other("missing fd argument"))
            .and_then(|s| {
                s.into_string().map_err(|err| {
                    io::Error::other(format!("invalid fd argument: {}", err.to_string_lossy()))
                })
            })
            .and_then(|s| {
                s.parse::<i32>()
                    .map_err(|err| io::Error::other(format!("failed to parse fd: {}", err)))
            })
            .map(|fd| unsafe { OwnedFd::from_raw_fd(fd) })
    }
}