use crate::session::{CommandBuilder, Error, ImageError, Project, Session};
use std::path::{Path, PathBuf};
pub struct UnshareSession {
root: PathBuf,
_root_dir: Option<RootDir>,
cwd: PathBuf,
isolate_network: std::cell::Cell<bool>,
}
struct RootDir(PathBuf);
impl RootDir {
fn new() -> Result<Self, crate::session::Error> {
let td = tempfile::tempdir().map_err(|e| {
crate::session::Error::SetupFailure("tempdir failed".to_string(), e.to_string())
})?;
Ok(Self(td.keep()))
}
fn path(&self) -> &Path {
&self.0
}
}
impl Drop for RootDir {
fn drop(&mut self) {
if let Err(e) = remove_root(&self.0) {
log::warn!("Failed to remove session root {}: {}", self.0.display(), e);
}
}
}
fn remove_root(root: &Path) -> std::io::Result<()> {
let status = std::process::Command::new("unshare")
.arg("--map-auto")
.arg("--map-root-user")
.arg("--setuid=0")
.arg("--setgid=0")
.arg("--")
.arg("rm")
.arg("-rf")
.arg("--")
.arg(root)
.status();
if matches!(status, Ok(status) if status.success()) {
return Ok(());
}
std::fs::remove_dir_all(root)
}
fn compression_flag(path: &Path) -> Result<Option<&str>, crate::session::Error> {
match path.extension().unwrap().to_str().unwrap() {
"tar" => Ok(None),
"gz" => Ok(Some("-z")),
"bz2" => Ok(Some("-j")),
"xz" => Ok(Some("-J")),
"zst" => Ok(Some("--zstd")),
e => Err(crate::session::Error::SetupFailure(
"unknown extension".to_string(),
format!("unknown extension: {}", e),
)),
}
}
const DEV_SETUP: &str = r#"
set -e
root="$1"
wd="$2"
shift 2
for node in null zero full random urandom tty; do
[ -e "/dev/$node" ] || continue
: > "$root/dev/$node"
mount --bind "/dev/$node" "$root/dev/$node"
done
mkdir -p "$root/dev/shm"
mount --rbind /dev/shm "$root/dev/shm"
exec unshare --root="$root" --wd="$wd" -- "$@"
"#;
pub fn cached_debian_tarball_path(suite: &str) -> Result<PathBuf, crate::session::Error> {
let arch = std::env::consts::ARCH;
let arch_name = match arch {
"x86_64" => "amd64",
"aarch64" => "arm64",
other => other,
};
let base_cache_dir = dirs::cache_dir()
.ok_or_else(|| crate::session::Error::ImageError(ImageError::NoCachedImage))?;
let cache_dir = base_cache_dir.join("ognibuild").join("images");
let tarball_name = format!("debian-{}-{}.tar.gz", suite, arch_name);
Ok(cache_dir.join(&tarball_name))
}
impl UnshareSession {
pub fn set_isolate_network(&self, isolate: bool) {
self.isolate_network.set(isolate);
}
pub fn cached_debian_session(suite: &str) -> Result<Self, crate::session::Error> {
let tarball_path = cached_debian_tarball_path(suite)?;
if !tarball_path.exists() {
Err(Error::ImageError(ImageError::NoCachedImage))
} else {
log::info!(
"Using cached Debian {} image from: {}",
suite,
tarball_path.display()
);
Self::from_tarball(&tarball_path)
}
}
pub fn from_tarball(path: &Path) -> Result<Self, crate::session::Error> {
let td = RootDir::new()?;
let root = td.path();
let f = std::fs::File::open(path).map_err(|e| {
crate::session::Error::SetupFailure("open failed".to_string(), e.to_string())
})?;
for dir in &["proc", "sys", "dev"] {
std::fs::create_dir_all(root.join(dir)).map_err(|e| {
crate::session::Error::SetupFailure(
format!("Failed to create {} directory", dir),
e.to_string(),
)
})?;
}
let output = std::process::Command::new("unshare")
.arg("--map-users=auto")
.arg("--map-groups=auto")
.arg("--fork")
.arg("--pid")
.arg("--mount-proc")
.arg("--net")
.arg("--uts")
.arg("--ipc")
.arg("--wd")
.arg(root)
.arg("--")
.arg("tar")
.arg("xp")
.arg(compression_flag(path)?.unwrap_or("--"))
.stdin(std::process::Stdio::from(f))
.stderr(std::process::Stdio::piped())
.output()?;
if !output.status.success() {
let stderr = String::from_utf8(output.stderr).unwrap();
return Err(crate::session::Error::SetupFailure(
"tar failed".to_string(),
stderr,
));
}
let root = root.to_path_buf();
let s = Self {
root,
_root_dir: Some(td),
cwd: std::path::PathBuf::from("/"),
isolate_network: std::cell::Cell::new(true),
};
s.ensure_current_user()?;
Ok(s)
}
pub fn save_to_tarball(&self, path: &Path) -> Result<(), crate::session::Error> {
let mut child = self.popen(
vec![
"tar",
"c",
"--absolute-names",
"--exclude",
"/dev/*",
"--exclude",
"/proc/*",
"--exclude",
"/sys/*",
compression_flag(path)?.unwrap_or("--"),
"/",
],
Some(std::path::Path::new("/")),
Some("root"),
Some(std::process::Stdio::piped()),
None,
None,
None,
)?;
let f = std::fs::File::create(path).map_err(|e| {
crate::session::Error::SetupFailure("create failed".to_string(), e.to_string())
})?;
let mut writer = std::io::BufWriter::new(f);
std::io::copy(child.stdout.as_mut().unwrap(), &mut writer).map_err(|e| {
crate::session::Error::SetupFailure("copy failed".to_string(), e.to_string())
})?;
if child.wait()?.success() {
Ok(())
} else {
Err(crate::session::Error::SetupFailure(
"tar failed".to_string(),
"tar failed".to_string(),
))
}
}
pub fn bootstrap() -> Result<Self, crate::session::Error> {
bootstrap_debian_tarball("sid", true, &[])
}
pub fn ensure_current_user(&self) -> Result<(), crate::session::Error> {
let user = whoami::username().map_err(|e| {
crate::session::Error::SetupFailure(
"Failed to get current username".to_string(),
e.to_string(),
)
})?;
let uid = nix::unistd::getuid().to_string();
let gid = nix::unistd::getgid().to_string();
match self.check_call(
vec![
"/usr/sbin/groupadd",
"--force",
"--non-unique",
"--gid",
&gid,
user.as_str(),
],
Some(std::path::Path::new("/")),
Some("root"),
None,
) {
Ok(_) => {}
Err(e) => panic!("Error: {:?}", e),
}
let child = self.popen(
vec![
"/usr/sbin/useradd",
"--uid",
&uid,
"--gid",
&gid,
user.as_str(),
],
Some(std::path::Path::new("/")),
Some("root"),
None,
Some(std::process::Stdio::piped()),
None,
None,
)?;
match child.wait_with_output() {
Ok(output) => {
match output.status.code() {
Some(0) => Ok(()),
Some(9) => Ok(()),
Some(4) => Ok(()),
_ => panic!(
"Error: {:?}: {}",
output.status,
String::from_utf8(output.stdout).unwrap()
),
}
}
Err(e) => panic!("Error: {:?}", e),
}
}
pub fn run_argv(
&self,
argv: Vec<&str>,
cwd: Option<&std::path::Path>,
user: Option<&str>,
) -> std::vec::Vec<String> {
let mut ret: Vec<String> = [
"unshare",
"--map-users=auto",
"--map-groups=auto",
"--fork",
"--pid",
"--mount-proc",
]
.iter()
.map(|s| s.to_string())
.collect();
if self.isolate_network.get() {
ret.push("--net".to_string());
}
ret.extend(
["--uts", "--ipc", "--map-root-user"]
.iter()
.map(|s| s.to_string()),
);
ret.push("--".to_string());
ret.extend([
"/bin/sh".to_string(),
"-c".to_string(),
DEV_SETUP.to_string(),
"unshare-session".to_string(),
self.root.to_str().unwrap().to_string(),
cwd.unwrap_or(&self.cwd).to_str().unwrap().to_string(),
]);
if let Some(user) = user.filter(|u| *u != "root") {
ret.extend([
"/usr/bin/setpriv".to_string(),
format!("--reuid={}", user),
"--init-groups".to_string(),
]);
}
ret.extend(argv.into_iter().map(|s| s.to_string()));
ret
}
fn build_tempdir(&self, user: Option<&str>) -> std::path::PathBuf {
let build_dir = "/build";
self.check_call(vec!["mkdir", "-p", build_dir], None, user, None)
.unwrap();
String::from_utf8(
self.check_output(
vec!["mktemp", "-d", format!("--tmpdir={}", build_dir).as_str()],
Some(std::path::Path::new("/")),
user,
None,
)
.unwrap(),
)
.unwrap()
.trim_end_matches('\n')
.to_string()
.into()
}
}
pub fn create_debian_session_for_testing(
suite: &str,
allow_network: bool,
) -> Result<UnshareSession, crate::session::Error> {
if let Ok(tarball_path) = std::env::var("OGNIBUILD_DEBIAN_TEST_TARBALL") {
let path = Path::new(&tarball_path);
if path.exists() {
log::info!(
"Using Debian test tarball from OGNIBUILD_DEBIAN_TEST_TARBALL: {}",
tarball_path
);
return UnshareSession::from_tarball(path);
} else {
return Err(Error::SetupFailure(
"Tarball not found".to_string(),
format!(
"OGNIBUILD_DEBIAN_TEST_TARBALL points to non-existent file: {}",
tarball_path
),
));
}
}
match UnshareSession::cached_debian_session(suite) {
Ok(session) => {
return Ok(session);
}
Err(Error::ImageError(ImageError::NoCachedImage)) => {
log::debug!("No cached image available for Debian {}", suite);
}
Err(Error::ImageError(ImageError::CachedImageNotFound { path })) => {
log::debug!("Cached image not found at {}", path.display());
}
Err(e) => return Err(e), }
if !allow_network {
return Err(Error::ImageError(ImageError::NoCachedImage));
}
log::info!(
"No cached image found, bootstrapping Debian {} test session from network using mmdebstrap",
suite
);
bootstrap_debian_tarball(suite, true, &[])
}
pub fn bootstrap_debian_tarball(
suite: &str,
setup_apt_file: bool,
extra_packages: &[&str],
) -> Result<UnshareSession, crate::session::Error> {
let td = RootDir::new()?;
let root = td.path();
let mut cmd = std::process::Command::new("mmdebstrap");
cmd.current_dir(root)
.arg("--mode=unshare")
.arg("--variant=minbase");
if !extra_packages.is_empty() {
cmd.arg(format!("--include={}", extra_packages.join(",")));
}
if setup_apt_file {
log::info!(
"Setting up apt-file and the Sources index in bootstrap (this requires network access)"
);
cmd.arg("--include=apt-file")
.arg(format!(
"--customize-hook=echo 'deb-src http://deb.debian.org/debian/ {} main' >> \"$1\"/etc/apt/sources.list",
suite
))
.arg("--customize-hook=chroot \"$1\" apt-get update")
.arg("--customize-hook=chroot \"$1\" apt-file update")
.arg("--skip=cleanup/apt/lists");
}
cmd.arg("--quiet")
.arg(suite)
.arg(root)
.arg("http://deb.debian.org/debian/");
let status = cmd.status().map_err(|e| {
crate::session::Error::SetupFailure(
"mmdebstrap command not found or failed to execute".to_string(),
format!("Failed to run mmdebstrap (ensure it's installed): {}", e),
)
})?;
if !status.success() {
return Err(crate::session::Error::SetupFailure(
"mmdebstrap failed".to_string(),
format!("mmdebstrap exited with status: {}. This likely requires network access to http://deb.debian.org/debian/", status),
));
}
let root = root.to_path_buf();
let s = UnshareSession {
root,
_root_dir: Some(td),
cwd: std::path::PathBuf::from("/"),
isolate_network: std::cell::Cell::new(true),
};
s.ensure_current_user()?;
Ok(s)
}
impl Session for UnshareSession {
fn chdir(&mut self, path: &std::path::Path) -> Result<(), crate::session::Error> {
self.cwd = self.cwd.join(path);
Ok(())
}
fn pwd(&self) -> &std::path::Path {
&self.cwd
}
fn external_path(&self, path: &std::path::Path) -> std::path::PathBuf {
if let Ok(rest) = path.strip_prefix("/") {
return self.location().join(rest);
}
self.location()
.join(
self.cwd
.to_string_lossy()
.to_string()
.trim_start_matches('/'),
)
.join(path)
}
fn location(&self) -> std::path::PathBuf {
self.root.clone()
}
fn check_output(
&self,
argv: Vec<&str>,
cwd: Option<&std::path::Path>,
user: Option<&str>,
env: Option<std::collections::HashMap<String, String>>,
) -> Result<Vec<u8>, super::Error> {
let argv = self.run_argv(argv, cwd, user);
let output = std::process::Command::new(&argv[0])
.args(&argv[1..])
.stderr(std::process::Stdio::inherit())
.envs(env.unwrap_or_default())
.output();
match output {
Ok(output) => {
if output.status.success() {
Ok(output.stdout)
} else {
Err(Error::CalledProcessError(output.status))
}
}
Err(e) => Err(Error::IoError(e)),
}
}
fn create_home(&self) -> Result<(), super::Error> {
crate::session::create_home(self)
}
fn check_call(
&self,
argv: Vec<&str>,
cwd: Option<&std::path::Path>,
user: Option<&str>,
env: Option<std::collections::HashMap<String, String>>,
) -> Result<(), crate::session::Error> {
let argv = self.run_argv(argv, cwd, user);
let status = std::process::Command::new(&argv[0])
.args(&argv[1..])
.envs(env.unwrap_or_default())
.status();
match status {
Ok(status) => {
if status.success() {
Ok(())
} else {
Err(Error::CalledProcessError(status))
}
}
Err(e) => Err(Error::IoError(e)),
}
}
fn exists(&self, path: &std::path::Path) -> bool {
let args = vec!["test", "-e", path.to_str().unwrap()];
self.check_call(args, None, None, None).is_ok()
}
fn mkdir(&self, path: &std::path::Path) -> Result<(), crate::session::Error> {
let args = vec!["mkdir", path.to_str().unwrap()];
self.check_call(args, None, None, None)
}
fn rmtree(&self, path: &std::path::Path) -> Result<(), crate::session::Error> {
let args = vec!["rm", "-rf", path.to_str().unwrap()];
self.check_call(args, None, None, None)
}
fn project_from_directory(
&self,
path: &std::path::Path,
subdir: Option<&str>,
) -> Result<Project, super::Error> {
let subdir = subdir.unwrap_or("package");
let reldir = self.build_tempdir(Some("root"));
let export_directory = self.external_path(&reldir).join(subdir);
let mut options = fs_extra::dir::CopyOptions::new();
options.copy_inside = true; options.content_only = false; options.skip_exist = false; options.overwrite = true; options.buffer_size = 64000; options.depth = 0;
fs_extra::dir::copy(path, &export_directory, &options).map_err(|e| {
crate::session::Error::SetupFailure(
format!("failed to copy {} into session", path.display()),
e.to_string(),
)
})?;
Ok(Project::Temporary {
external_path: export_directory,
internal_path: reldir.join(subdir),
td: self.external_path(&reldir),
})
}
fn popen(
&self,
argv: Vec<&str>,
cwd: Option<&std::path::Path>,
user: Option<&str>,
stdout: Option<std::process::Stdio>,
stderr: Option<std::process::Stdio>,
stdin: Option<std::process::Stdio>,
env: Option<&std::collections::HashMap<String, String>>,
) -> Result<std::process::Child, Error> {
let argv = self.run_argv(argv, cwd, user);
let mut binding = std::process::Command::new(&argv[0]);
let mut cmd = binding.args(&argv[1..]);
if let Some(env) = env {
cmd = cmd.envs(env);
}
if let Some(stdin) = stdin {
cmd = cmd.stdin(stdin);
}
if let Some(stdout) = stdout {
cmd = cmd.stdout(stdout);
}
if let Some(stderr) = stderr {
cmd = cmd.stderr(stderr);
}
Ok(cmd.spawn()?)
}
fn is_temporary(&self) -> bool {
true
}
#[cfg(feature = "breezy")]
fn project_from_vcs(
&self,
tree: &dyn crate::vcs::DupableTree,
include_controldir: Option<bool>,
subdir: Option<&str>,
) -> Result<Project, Error> {
let reldir = self.build_tempdir(None);
let subdir = subdir.unwrap_or("package");
let export_directory = self.external_path(&reldir).join(subdir);
if !include_controldir.unwrap_or(false) {
tree.export_to(&export_directory, None).unwrap();
} else {
crate::vcs::dupe_vcs_tree(tree, &export_directory).unwrap();
}
Ok(Project::Temporary {
external_path: export_directory,
internal_path: reldir.join(subdir),
td: self.external_path(&reldir),
})
}
fn command<'a>(&'a self, argv: Vec<&'a str>) -> CommandBuilder<'a> {
CommandBuilder::new(self, argv)
}
fn read_dir(&self, path: &std::path::Path) -> Result<Vec<std::fs::DirEntry>, Error> {
std::fs::read_dir(self.external_path(path))
.map_err(Error::IoError)?
.collect::<Result<Vec<_>, _>>()
.map_err(Error::IoError)
}
fn set_isolate_network(&self, isolate: bool) {
self.isolate_network.set(isolate);
}
fn is_network_isolated(&self) -> bool {
self.isolate_network.get()
}
}
#[cfg(test)]
lazy_static::lazy_static! {
pub(crate) static ref TEST_ENV_LOCK: std::sync::Mutex<()> = std::sync::Mutex::new(());
static ref TEST_SESSION: std::sync::Mutex<Option<UnshareSession>> = std::sync::Mutex::new({
let _guard = TEST_ENV_LOCK.lock().unwrap_or_else(|p| p.into_inner());
create_debian_session_for_testing("sid", false).ok()
});
}
#[cfg(test)]
#[ctor::dtor]
fn drop_test_session() {
let mut guard = TEST_SESSION.lock().unwrap_or_else(|p| p.into_inner());
guard.take();
}
#[cfg(test)]
pub(crate) struct TestSession(std::sync::MutexGuard<'static, Option<UnshareSession>>);
#[cfg(test)]
impl std::ops::Deref for TestSession {
type Target = UnshareSession;
fn deref(&self) -> &Self::Target {
self.0.as_ref().expect("test session present")
}
}
#[cfg(test)]
impl std::ops::DerefMut for TestSession {
fn deref_mut(&mut self) -> &mut Self::Target {
self.0.as_mut().expect("test session present")
}
}
#[cfg(test)]
pub(crate) fn test_session() -> Option<TestSession> {
if std::env::var("GITHUB_ACTIONS").is_ok() {
return None;
}
let guard = match TEST_SESSION.lock() {
Ok(guard) => guard,
Err(poisoned) => poisoned.into_inner(),
};
if guard.is_none() {
return None;
}
Some(TestSession(guard))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_drop_removes_subuid_owned_root() {
if std::env::var("GITHUB_ACTIONS").is_ok() {
return;
}
let td = tempfile::tempdir().unwrap();
let root = td.keep();
let status = std::process::Command::new("unshare")
.arg("--map-auto")
.arg("--map-root-user")
.arg("--setuid=0")
.arg("--setgid=0")
.arg("--")
.arg("sh")
.arg("-c")
.arg(format!(
"mkdir -p {0}/usr/bin && touch {0}/usr/bin/f && chown -R 1:1 {0}/usr && chmod -R a-w {0}/usr",
root.display()
))
.status();
match status {
Ok(status) if status.success() => {}
_ => {
std::fs::remove_dir_all(&root).ok();
return;
}
}
assert!(
std::fs::remove_dir_all(&root).is_err(),
"expected the subuid-owned root to be unremovable by the calling user"
);
std::mem::drop(RootDir(root.clone()));
assert!(!root.exists(), "session root {} leaked", root.display());
}
#[test]
fn test_root_removed_when_setup_fails() {
let td = RootDir::new().unwrap();
let root = td.path().to_path_buf();
std::fs::write(root.join("half-extracted"), "x").unwrap();
std::mem::drop(td);
assert!(!root.exists(), "session root {} leaked", root.display());
}
#[test]
fn test_drop_leaves_unowned_root_alone() {
let td = tempfile::tempdir().unwrap();
let session = UnshareSession {
root: td.path().to_path_buf(),
_root_dir: None,
cwd: std::path::PathBuf::from("/"),
isolate_network: std::cell::Cell::new(true),
};
std::mem::drop(session);
assert!(td.path().exists());
}
#[test]
fn test_is_temporary() {
let session = if let Some(session) = test_session() {
session
} else {
return;
};
assert!(session.is_temporary());
}
#[test]
fn test_chdir() {
let mut session = if let Some(session) = test_session() {
session
} else {
return;
};
session.chdir(std::path::Path::new("/")).unwrap();
}
#[test]
fn test_check_output() {
let session = if let Some(session) = test_session() {
session
} else {
return;
};
let output = String::from_utf8(
session
.check_output(vec!["ls"], Some(std::path::Path::new("/")), None, None)
.unwrap(),
)
.unwrap();
let dirs = output.split_whitespace().collect::<Vec<&str>>();
assert!(dirs.contains(&"bin"));
assert!(dirs.contains(&"dev"));
assert!(dirs.contains(&"etc"));
assert!(dirs.contains(&"home"));
assert!(dirs.contains(&"lib"));
assert!(dirs.contains(&"usr"));
assert!(dirs.contains(&"proc"));
assert_eq!(
"root",
String::from_utf8(
session
.check_output(vec!["whoami"], None, Some("root"), None)
.unwrap()
)
.unwrap()
.trim_end()
);
assert_eq!(
String::from_utf8(
session
.check_output(vec!["id", "-u"], None, None, None)
.unwrap()
)
.unwrap()
.trim_end(),
String::from_utf8(
session
.check_output(vec!["id", "-u"], None, None, None)
.unwrap()
)
.unwrap()
.trim_end()
);
assert_eq!(
"nobody",
String::from_utf8(
session
.check_output(vec!["whoami"], None, Some("nobody"), None)
.unwrap()
)
.unwrap()
.trim_end()
);
}
#[test]
fn test_missing_command_is_recognisable() {
let session = if let Some(session) = test_session() {
session
} else {
return;
};
let err = crate::analyze::run_detecting_problems(
&*session,
vec!["definitely-not-a-real-command"],
None,
true,
Some(std::path::Path::new("/")),
None,
None,
None,
)
.unwrap_err();
let crate::analyze::AnalyzedError::Detailed { error, .. } = err else {
panic!("missing command not identified: {:?}", err);
};
assert_eq!(error.kind(), "command-missing");
}
#[test]
fn test_dev_nodes_are_devices() {
let session = if let Some(session) = test_session() {
session
} else {
return;
};
let out = session
.check_output(
vec!["stat", "-c", "%F", "/dev/null", "/dev/urandom"],
Some(std::path::Path::new("/")),
None,
None,
)
.unwrap();
assert_eq!(
String::from_utf8(out).unwrap(),
"character special file\ncharacter special file\n"
);
}
#[test]
fn test_dev_shm_writable() {
let session = if let Some(session) = test_session() {
session
} else {
return;
};
session
.check_call(
vec!["touch", "/dev/shm/ognibuild-test"],
Some(std::path::Path::new("/")),
None,
None,
)
.unwrap();
session
.check_call(
vec!["rm", "/dev/shm/ognibuild-test"],
Some(std::path::Path::new("/")),
None,
None,
)
.unwrap();
}
#[test]
fn test_check_call() {
let session = if let Some(session) = test_session() {
session
} else {
return;
};
session
.check_call(vec!["true"], Some(std::path::Path::new("/")), None, None)
.unwrap();
}
#[test]
fn test_create_home() {
let session = if let Some(session) = test_session() {
session
} else {
return;
};
session.create_home().unwrap();
}
fn save_and_reuse(name: &str) {
let session = if let Some(session) = test_session() {
session
} else {
return;
};
let tempdir = tempfile::tempdir().unwrap();
let path = tempdir.path().join(name);
session.save_to_tarball(&path).unwrap();
std::mem::drop(session);
let session = UnshareSession::from_tarball(&path).unwrap();
assert!(session.exists(std::path::Path::new("/bin")));
let output = String::from_utf8(
session
.check_output(vec!["ls"], Some(std::path::Path::new("/")), None, None)
.unwrap(),
)
.unwrap();
let dirs = output.split_whitespace().collect::<Vec<&str>>();
assert!(dirs.contains(&"bin"));
assert!(dirs.contains(&"dev"));
assert!(dirs.contains(&"etc"));
assert!(dirs.contains(&"home"));
assert!(dirs.contains(&"lib"));
}
#[test]
fn test_save_and_reuse() {
save_and_reuse("test.tar");
}
#[test]
fn test_save_and_reuse_gz() {
save_and_reuse("test.tar.gz");
}
#[test]
fn test_mkdir_rmdir() {
let session = if let Some(session) = test_session() {
session
} else {
return;
};
let path = std::path::Path::new("/tmp/test");
session.mkdir(path).unwrap();
assert!(session.exists(path));
session.rmtree(path).unwrap();
assert!(!session.exists(path));
}
#[test]
fn test_project_from_directory() {
let session = if let Some(session) = test_session() {
session
} else {
return;
};
let tempdir = tempfile::tempdir().unwrap();
std::fs::write(tempdir.path().join("test"), "test").unwrap();
let project = session
.project_from_directory(tempdir.path(), None)
.unwrap();
assert!(project.external_path().exists());
assert!(session.exists(project.internal_path()));
session.rmtree(project.internal_path()).unwrap();
assert!(!session.exists(project.internal_path()));
assert!(!project.external_path().exists());
}
#[test]
fn test_session_works_after_panic() {
if std::env::var("GITHUB_ACTIONS").is_ok() {
return;
}
let session1 = test_session().unwrap();
assert!(session1.exists(std::path::Path::new("/bin")));
std::mem::drop(session1);
let result = std::panic::catch_unwind(|| {
let _session = test_session().unwrap();
panic!("Intentional panic to test recovery");
});
assert!(result.is_err());
let session2 = test_session().unwrap();
assert!(session2.exists(std::path::Path::new("/bin")));
session2
.check_call(vec!["true"], Some(std::path::Path::new("/")), None, None)
.unwrap();
}
#[test]
fn test_cached_debian_session_no_download() {
let result = UnshareSession::cached_debian_session("test-suite-nonexistent");
assert!(result.is_err());
if let Err(err) = result {
assert!(
matches!(
err,
crate::session::Error::ImageError(
crate::session::ImageError::CachedImageNotFound { .. }
| crate::session::ImageError::NoCachedImage
)
),
"Expected CachedImageNotFound error, got {:?}",
err
);
}
}
#[test]
fn test_cached_debian_session_unsupported_arch() {
let arch = std::env::consts::ARCH;
if arch == "x86_64" || arch == "aarch64" {
return;
}
let result = UnshareSession::cached_debian_session("sid");
assert!(result.is_err());
if let Err(err) = result {
assert!(
matches!(
err,
crate::session::Error::ImageError(
crate::session::ImageError::UnsupportedArchitecture { .. }
)
),
"Expected UnsupportedArchitecture error, got {:?}",
err
);
}
}
#[test]
fn test_create_debian_session_with_env_var() {
let _env_guard = TEST_ENV_LOCK.lock().unwrap_or_else(|p| p.into_inner());
let temp_dir = tempfile::tempdir().unwrap();
let tarball_path = temp_dir.path().join("test.tar.xz");
std::fs::write(&tarball_path, b"test").unwrap();
std::env::set_var(
"OGNIBUILD_DEBIAN_TEST_TARBALL",
tarball_path.to_str().unwrap(),
);
let result = create_debian_session_for_testing("sid", false);
std::env::remove_var("OGNIBUILD_DEBIAN_TEST_TARBALL");
assert!(result.is_err());
if let Err(err) = result {
assert!(
matches!(err, crate::session::Error::SetupFailure(_, _)),
"Expected SetupFailure from tar extraction, got {:?}",
err
);
}
}
#[test]
fn test_create_debian_session_nonexistent_tarball() {
let _env_guard = TEST_ENV_LOCK.lock().unwrap_or_else(|p| p.into_inner());
std::env::set_var(
"OGNIBUILD_DEBIAN_TEST_TARBALL",
"/nonexistent/path/tarball.tar.xz",
);
let result = create_debian_session_for_testing("sid", false);
std::env::remove_var("OGNIBUILD_DEBIAN_TEST_TARBALL");
assert!(result.is_err());
if let Err(err) = result {
match err {
crate::session::Error::SetupFailure(_msg, detail) => {
assert!(
detail.contains("non-existent file"),
"Expected error about non-existent file, got: {}",
detail
);
}
_ => panic!("Expected SetupFailure, got {:?}", err),
}
}
}
#[cfg(not(feature = "debian"))]
#[test]
fn test_cached_debian_session_no_debian_feature() {
let result = UnshareSession::cached_debian_session("sid");
if result.is_err() {
if let Err(err) = result {
assert!(
matches!(err, crate::session::Error::ImageError(_)),
"Expected ImageError, got {:?}",
err
);
}
}
}
#[test]
fn test_popen() {
let session = if let Some(session) = test_session() {
session
} else {
return;
};
let child = session
.popen(
vec!["ls"],
Some(std::path::Path::new("/")),
None,
Some(std::process::Stdio::piped()),
Some(std::process::Stdio::piped()),
Some(std::process::Stdio::piped()),
None,
)
.unwrap();
let output = String::from_utf8(child.wait_with_output().unwrap().stdout).unwrap();
let dirs = output.split_whitespace().collect::<Vec<&str>>();
assert!(dirs.contains(&"etc"));
assert!(dirs.contains(&"home"));
assert!(dirs.contains(&"lib"));
assert!(dirs.contains(&"usr"));
assert!(dirs.contains(&"proc"));
}
#[test]
fn test_set_isolate_network() {
let session = UnshareSession {
root: std::path::PathBuf::from("/fakechroot"),
_root_dir: None,
cwd: std::path::PathBuf::from("/"),
isolate_network: std::cell::Cell::new(true),
};
let argv = session.run_argv(vec!["true"], Some(std::path::Path::new("/")), None);
assert!(argv.iter().any(|a| a == "--net"));
session.set_isolate_network(false);
let argv = session.run_argv(vec!["true"], Some(std::path::Path::new("/")), None);
assert!(!argv.iter().any(|a| a == "--net"));
session.set_isolate_network(true);
let argv = session.run_argv(vec!["true"], Some(std::path::Path::new("/")), None);
assert!(argv.iter().any(|a| a == "--net"));
}
#[test]
fn test_with_network_restores_isolation() {
let session = UnshareSession {
root: std::path::PathBuf::from("/fakechroot"),
_root_dir: None,
cwd: std::path::PathBuf::from("/"),
isolate_network: std::cell::Cell::new(true),
};
let inside = crate::session::with_network(&session, || session.isolate_network.get());
assert!(!inside);
assert!(session.isolate_network.get());
}
#[test]
fn test_external_path() {
let mut session = if let Some(session) = test_session() {
session
} else {
return;
};
let path = std::path::Path::new("/tmp/test");
assert_eq!(
session.external_path(path),
session.location().join("tmp/test")
);
session.chdir(std::path::Path::new("/tmp")).unwrap();
let path = std::path::Path::new("test");
assert_eq!(
session.external_path(path),
session.location().join("tmp/test")
);
}
}