use std::os::unix::fs::PermissionsExt;
use std::path::Path;
use anyhow::{Context, Result};
use crate::cpio::CpioArchive;
struct ProcStatus {
real_uid: u32,
effective_uid: u32,
real_gid: u32,
cap_eff: u64,
}
fn proc_status() -> Option<ProcStatus> {
let status = std::fs::read_to_string("/proc/self/status").ok()?;
let mut real_uid = None;
let mut effective_uid = None;
let mut real_gid = None;
let mut cap_eff = None;
for line in status.lines() {
if let Some(rest) = line.strip_prefix("Uid:") {
let mut fields = rest.split_whitespace();
real_uid = fields.next().and_then(|s| s.parse().ok());
effective_uid = fields.next().and_then(|s| s.parse().ok());
}
if let Some(rest) = line.strip_prefix("Gid:") {
real_gid = rest.split_whitespace().next().and_then(|s| s.parse().ok());
}
if let Some(rest) = line.strip_prefix("CapEff:") {
cap_eff = u64::from_str_radix(rest.trim(), 16).ok();
}
}
Some(ProcStatus {
real_uid: real_uid?,
effective_uid: effective_uid?,
real_gid: real_gid?,
cap_eff: cap_eff.unwrap_or(0),
})
}
impl ProcStatus {
fn can_chown(&self) -> bool {
self.effective_uid == 0 || (self.cap_eff & 1) != 0
}
fn can_mknod(&self) -> bool {
self.effective_uid == 0 || (self.cap_eff & (1 << 27)) != 0
}
}
fn create_special_node(
target: &Path,
mode: u32,
rdevmajor: u32,
rdevminor: u32,
) -> std::io::Result<()> {
use std::ffi::CString;
use std::os::unix::ffi::OsStrExt;
if let Some(parent) = target.parent() {
std::fs::create_dir_all(parent).ok();
}
let _ = std::fs::remove_file(target);
let c_path = CString::new(target.as_os_str().as_bytes())
.map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidInput, e))?;
let dev = libc::makedev(rdevmajor, rdevminor);
let ret = unsafe { libc::mknod(c_path.as_ptr(), mode, dev) };
if ret == 0 {
Ok(())
} else {
Err(std::io::Error::last_os_error())
}
}
fn shell_escape_args(args: &[String]) -> String {
args.iter()
.map(|a| {
if a.contains(|c: char| c.is_whitespace() || "\"'\\$`!#&|;(){}".contains(c)) {
format!("'{}'", a.replace('\'', "'\\''"))
} else {
a.clone()
}
})
.collect::<Vec<_>>()
.join(" ")
}
pub fn extract_archive(archive: &CpioArchive, dest: &Path) -> Result<()> {
std::fs::create_dir_all(dest)
.with_context(|| format!("failed to create destination: {}", dest.display()))?;
let status = proc_status();
let chown_ok = status.as_ref().is_some_and(|s| s.can_chown());
let mknod_ok = status.as_ref().is_some_and(|s| s.can_mknod());
let has_devices = archive
.entries
.iter()
.any(|e| e.is_block_device() || e.is_char_device());
let mut missing_caps: Vec<&str> = Vec::new();
if !chown_ok {
missing_caps.push("chown");
}
if !mknod_ok && has_devices {
missing_caps.push("mknod");
}
if !missing_caps.is_empty() {
let args: Vec<String> = std::env::args().collect();
let cmd = shell_escape_args(&args);
let inh_caps = missing_caps.iter().map(|c| format!("+{c}")).collect::<Vec<_>>().join(",");
let amb_caps = &inh_caps;
let (uid, gid) = status
.as_ref()
.map(|s| (s.real_uid, s.real_gid))
.unwrap_or((1000, 1000));
if !chown_ok {
eprintln!(
"warning: not running as root and missing CAP_CHOWN; file ownership will not be preserved"
);
}
if !mknod_ok && has_devices {
eprintln!(
"warning: not running as root and missing CAP_MKNOD; device nodes will be skipped"
);
}
eprintln!(
"hint: re-run with: sudo -E setpriv --reuid={uid} --regid={gid} --init-groups --inh-caps {inh_caps} --ambient-caps {amb_caps} -- env PATH=\"$PATH\" bash -c \"{cmd}\""
);
}
let mut dir_chowns: Vec<(std::path::PathBuf, u32, u32)> = Vec::new();
for entry in &archive.entries {
let target = dest.join(&entry.name);
if entry.is_dir() {
std::fs::create_dir_all(&target)
.with_context(|| format!("failed to create dir: {}", target.display()))?;
std::fs::set_permissions(&target, std::fs::Permissions::from_mode(entry.permissions()))
.ok(); if chown_ok {
dir_chowns.push((target, entry.uid, entry.gid));
}
} else if entry.is_symlink() {
let link_target =
std::str::from_utf8(&entry.data).context("symlink target is not valid UTF-8")?;
if let Some(parent) = target.parent() {
std::fs::create_dir_all(parent).ok();
}
let _ = std::fs::remove_file(&target);
std::os::unix::fs::symlink(link_target, &target)
.with_context(|| format!("failed to create symlink: {}", target.display()))?;
if chown_ok {
std::os::unix::fs::lchown(&target, Some(entry.uid), Some(entry.gid)).ok();
}
} else if entry.is_file() {
if let Some(parent) = target.parent() {
std::fs::create_dir_all(parent).ok();
}
std::fs::write(&target, &entry.data)
.with_context(|| format!("failed to write file: {}", target.display()))?;
std::fs::set_permissions(&target, std::fs::Permissions::from_mode(entry.permissions()))
.ok(); if chown_ok {
std::os::unix::fs::chown(&target, Some(entry.uid), Some(entry.gid)).ok();
}
} else if entry.is_block_device() || entry.is_char_device() {
if mknod_ok {
create_special_node(&target, entry.mode, entry.rdevmajor, entry.rdevminor)
.with_context(|| {
format!(
"failed to create device node: {} ({}:{})",
target.display(),
entry.rdevmajor,
entry.rdevminor
)
})?;
if chown_ok {
std::os::unix::fs::chown(&target, Some(entry.uid), Some(entry.gid)).ok();
}
} else {
eprintln!(
"skipping device node '{}': {} ({}:{})",
entry.file_type_char(),
entry.name,
entry.rdevmajor,
entry.rdevminor
);
}
} else if entry.is_fifo() || entry.is_socket() {
create_special_node(&target, entry.mode, 0, 0)
.with_context(|| {
format!(
"failed to create {}: {}",
if entry.is_fifo() { "FIFO" } else { "socket" },
target.display()
)
})?;
std::fs::set_permissions(&target, std::fs::Permissions::from_mode(entry.permissions()))
.ok(); if chown_ok {
std::os::unix::fs::chown(&target, Some(entry.uid), Some(entry.gid)).ok();
}
} else {
eprintln!(
"skipping unsupported entry type '{}': {}",
entry.file_type_char(),
entry.name
);
}
}
dir_chowns.sort_by(|a, b| b.0.as_os_str().len().cmp(&a.0.as_os_str().len()));
for (path, uid, gid) in &dir_chowns {
std::os::unix::fs::chown(path, Some(*uid), Some(*gid)).ok();
}
Ok(())
}