use crate::format::{PackFooter, SIDECAR_EXTENSION};
use std::fs::{self, File};
use std::io::{Read, Seek, SeekFrom, Write};
use std::path::{Path, PathBuf};
#[cfg(unix)]
use std::os::unix::fs::PermissionsExt;
#[cfg(unix)]
use std::os::unix::io::AsRawFd;
#[cfg(windows)]
pub(crate) fn mark_file_sparse(file: &fs::File) -> std::io::Result<()> {
use std::os::windows::io::AsRawHandle;
use windows_sys::Win32::System::IO::DeviceIoControl;
const FSCTL_SET_SPARSE: u32 = 0x000900C4;
let mut returned: u32 = 0;
let ok = unsafe {
DeviceIoControl(
file.as_raw_handle(),
FSCTL_SET_SPARSE,
std::ptr::null(),
0,
std::ptr::null_mut(),
0,
&mut returned,
std::ptr::null_mut(),
)
};
if ok == 0 {
Err(std::io::Error::last_os_error())
} else {
Ok(())
}
}
#[cfg(unix)]
fn lock_file_exclusive(lock_file: &fs::File) -> std::io::Result<()> {
let ret = unsafe { libc::flock(lock_file.as_raw_fd(), libc::LOCK_EX) };
if ret != 0 {
return Err(std::io::Error::last_os_error());
}
Ok(())
}
#[cfg(windows)]
fn lock_file_exclusive(lock_file: &fs::File) -> std::io::Result<()> {
use std::os::windows::io::AsRawHandle;
use windows_sys::Win32::Storage::FileSystem::{LockFileEx, LOCKFILE_EXCLUSIVE_LOCK};
use windows_sys::Win32::System::IO::OVERLAPPED;
let handle = lock_file.as_raw_handle() as windows_sys::Win32::Foundation::HANDLE;
let mut overlapped: OVERLAPPED = unsafe { std::mem::zeroed() };
let ret = unsafe {
LockFileEx(
handle,
LOCKFILE_EXCLUSIVE_LOCK,
0,
u32::MAX,
u32::MAX,
&mut overlapped,
)
};
if ret == 0 {
return Err(std::io::Error::last_os_error());
}
Ok(())
}
#[inline]
fn set_mode(path: &Path, mode: u32) {
#[cfg(unix)]
{
let _ = std::fs::set_permissions(path, std::fs::Permissions::from_mode(mode));
}
#[cfg(not(unix))]
{
let _ = (path, mode);
}
}
const SPARSE_WRITE_THRESHOLD: u64 = 256 * 1024 * 1024;
fn unpack_sparse<R: Read>(
entry: &mut tar::Entry<R>,
path: &Path,
entry_size: u64,
mode: u32,
real_dest: &Path,
) -> std::io::Result<()> {
verify_parent_within_dest(path, real_dest)?;
if let Some(parent) = path.parent() {
fs::create_dir_all(parent)?;
}
match path.symlink_metadata() {
Ok(meta) if meta.file_type().is_symlink() => {
return Err(std::io::Error::new(
std::io::ErrorKind::InvalidData,
format!("unpack_sparse: symlink at destination: {}", path.display()),
));
}
Ok(meta) if meta.file_type().is_dir() => {
return Err(std::io::Error::new(
std::io::ErrorKind::InvalidData,
format!(
"unpack_sparse: directory at destination: {}",
path.display()
),
));
}
Ok(_) => {
fs::remove_file(path)?;
}
Err(e) if e.kind() == std::io::ErrorKind::NotFound => {}
Err(e) => return Err(e),
}
#[cfg(unix)]
let mut file = {
use std::os::unix::fs::OpenOptionsExt;
fs::OpenOptions::new()
.write(true)
.create_new(true)
.custom_flags(libc::O_NOFOLLOW)
.open(path)?
};
#[cfg(not(unix))]
let mut file = fs::OpenOptions::new()
.write(true)
.create_new(true)
.open(path)?;
#[cfg(windows)]
mark_file_sparse(&file)?;
file.set_len(entry_size)?;
let mut offset: u64 = 0;
let mut buf = vec![0u8; 64 * 1024];
loop {
let n = entry.read(&mut buf)?;
if n == 0 {
break;
}
let chunk = &buf[..n];
if chunk.iter().any(|&b| b != 0) {
file.seek(SeekFrom::Start(offset))?;
file.write_all(chunk)?;
}
offset += n as u64;
}
#[cfg(unix)]
fs::set_permissions(path, fs::Permissions::from_mode(mode & 0o777))?;
#[cfg(not(unix))]
let _ = mode;
Ok(())
}
fn verify_parent_within_dest(path: &Path, real_dest: &Path) -> std::io::Result<()> {
let Some(parent) = path.parent() else {
return Ok(());
};
for ancestor in parent.ancestors() {
match ancestor.canonicalize() {
Ok(real) => {
if real.starts_with(real_dest) {
return Ok(());
}
return Err(std::io::Error::new(
std::io::ErrorKind::InvalidData,
format!(
"resolved parent '{}' of '{}' escapes destination '{}'",
real.display(),
path.display(),
real_dest.display()
),
));
}
Err(e) if e.kind() == std::io::ErrorKind::NotFound => continue,
Err(e) => return Err(e),
}
}
Ok(())
}
fn safe_unpack<R: Read>(archive: &mut tar::Archive<R>, dest: &Path) -> std::io::Result<()> {
safe_unpack_with_limits(archive, dest, &SafeUnpackLimits::from_env())
}
#[derive(Clone, Copy)]
struct SafeUnpackLimits {
max_entries: u64,
max_total_bytes: u64,
sparse_threshold: u64,
}
impl SafeUnpackLimits {
fn from_env() -> Self {
Self {
max_entries: max_extract_entries(),
max_total_bytes: max_extract_total_bytes(),
sparse_threshold: SPARSE_WRITE_THRESHOLD,
}
}
}
fn safe_unpack_with_limits<R: Read>(
archive: &mut tar::Archive<R>,
dest: &Path,
limits: &SafeUnpackLimits,
) -> std::io::Result<()> {
let canonical_dest = normalize_path(dest);
let real_dest = dest
.canonicalize()
.unwrap_or_else(|_| canonical_dest.clone());
let max_entries = limits.max_entries;
let max_total_bytes = limits.max_total_bytes;
let mut entry_count: u64 = 0;
let mut total_bytes: u64 = 0;
let mut deferred_dir_modes: Vec<(PathBuf, u32)> = Vec::new();
for entry_result in archive.entries()? {
let mut entry = entry_result?;
let entry_type = entry.header().entry_type();
let entry_path = entry.path()?.to_path_buf();
entry_count += 1;
if entry_count > max_entries {
return Err(std::io::Error::new(
std::io::ErrorKind::InvalidData,
format!("tar archive exceeds max entry count ({max_entries})"),
));
}
total_bytes = total_bytes.saturating_add(entry.header().size().unwrap_or(0));
if total_bytes > max_total_bytes {
return Err(std::io::Error::new(
std::io::ErrorKind::InvalidData,
format!("tar archive exceeds max total size ({max_total_bytes} bytes)"),
));
}
match entry_type {
tar::EntryType::Regular
| tar::EntryType::GNUSparse
| tar::EntryType::Directory
| tar::EntryType::Continuous => {}
tar::EntryType::GNULongName
| tar::EntryType::GNULongLink
| tar::EntryType::XGlobalHeader
| tar::EntryType::XHeader => {
continue;
}
tar::EntryType::Symlink => {
if let Some(link_target) = entry.link_name()? {
let link_target = link_target.to_path_buf();
let target_str = link_target.to_string_lossy();
let resolved = if target_str.starts_with('/') {
dest.join(target_str.trim_start_matches('/'))
} else {
let parent = entry_path.parent().unwrap_or(Path::new(""));
dest.join(parent).join(&link_target)
};
let normalized = normalize_path(&resolved);
if !normalized.starts_with(&canonical_dest) {
return Err(std::io::Error::new(
std::io::ErrorKind::InvalidData,
format!(
"tar symlink '{}' -> '{}' escapes destination directory",
entry_path.display(),
link_target.display()
),
));
}
if target_str.starts_with('/') && normalized == canonical_dest {
return Err(std::io::Error::new(
std::io::ErrorKind::InvalidData,
format!(
"tar symlink '{}' -> '{}' aliases the destination root",
entry_path.display(),
link_target.display()
),
));
}
}
}
tar::EntryType::Link => {
if let Some(link_target) = entry.link_name()? {
let target_str = link_target.to_string_lossy();
let full_target = if target_str.starts_with('/') {
dest.join(target_str.trim_start_matches('/'))
} else {
dest.join(link_target.as_ref())
};
let normalized = normalize_path(&full_target);
if !normalized.starts_with(&canonical_dest) {
return Err(std::io::Error::new(
std::io::ErrorKind::InvalidData,
format!(
"tar hardlink '{}' escapes destination directory",
entry_path.display()
),
));
}
if !normalized.exists() {
continue;
}
}
}
tar::EntryType::Char | tar::EntryType::Block | tar::EntryType::Fifo => {
continue;
}
_other => {
continue;
}
}
let full_path = dest.join(&entry_path);
let normalized = normalize_path(&full_path);
if !normalized.starts_with(&canonical_dest) {
return Err(std::io::Error::new(
std::io::ErrorKind::InvalidData,
format!(
"tar entry '{}' escapes destination directory",
entry_path.display()
),
));
}
if let Some(parent) = full_path.parent() {
if parent.is_dir() {
set_mode(parent, 0o755);
}
}
if entry_type == tar::EntryType::Directory {
let mode = entry.header().mode().unwrap_or(0o755);
if mode & 0o200 == 0 {
deferred_dir_modes.push((full_path.clone(), mode));
}
}
let is_regular =
entry_type == tar::EntryType::Regular || entry_type == tar::EntryType::GNUSparse;
if is_regular && entry.header().size().unwrap_or(0) >= limits.sparse_threshold {
let entry_size = entry.header().size()?;
let mode = entry.header().mode().unwrap_or(0o644);
if let Err(e) = unpack_sparse(&mut entry, &full_path, entry_size, mode, &real_dest) {
return Err(std::io::Error::new(
e.kind(),
format!("failed to unpack '{}': {}", entry_path.display(), e),
));
}
} else {
if let Err(e) = entry.unpack_in(dest) {
if !is_regular {
continue;
}
return Err(std::io::Error::new(
e.kind(),
format!("failed to unpack '{}': {}", entry_path.display(), e),
));
}
if entry_type == tar::EntryType::Directory && full_path.is_dir() {
set_mode(&full_path, 0o755);
}
}
}
for (path, mode) in deferred_dir_modes {
if path.is_dir() {
set_mode(&path, mode);
}
}
Ok(())
}
fn normalize_path(path: &Path) -> PathBuf {
let mut components = Vec::new();
for component in path.components() {
match component {
std::path::Component::ParentDir => {
components.pop();
}
std::path::Component::CurDir => {}
c => components.push(c),
}
}
components.iter().collect()
}
pub fn resolve_cache_asset_path(
cache_dir: &Path,
asset_rel_path: &str,
context: &str,
) -> std::io::Result<PathBuf> {
if asset_rel_path.is_empty() {
return Err(std::io::Error::new(
std::io::ErrorKind::InvalidInput,
format!("{} path is empty", context),
));
}
let rel = Path::new(asset_rel_path);
if rel.is_absolute() {
return Err(std::io::Error::new(
std::io::ErrorKind::InvalidInput,
format!("{} path must be relative", context),
));
}
for component in rel.components() {
match component {
std::path::Component::Normal(_) => {}
std::path::Component::ParentDir
| std::path::Component::CurDir
| std::path::Component::RootDir
| std::path::Component::Prefix(_) => {
return Err(std::io::Error::new(
std::io::ErrorKind::InvalidInput,
format!("{} path contains disallowed components", context),
));
}
}
}
let cache_root = cache_dir
.canonicalize()
.unwrap_or_else(|_| normalize_path(cache_dir));
let candidate = cache_dir.join(rel);
let resolved = if candidate.exists() {
candidate.canonicalize()?
} else {
let parent = candidate.parent().unwrap_or(&candidate);
let canonical_parent = parent
.canonicalize()
.unwrap_or_else(|_| normalize_path(parent));
canonical_parent.join(candidate.file_name().unwrap_or_default())
};
if !resolved.starts_with(&cache_root) {
return Err(std::io::Error::new(
std::io::ErrorKind::InvalidInput,
format!("{} path escapes cache directory", context),
));
}
Ok(resolved)
}
const EXTRACTION_MARKER: &str = ".smolvm-extracted";
pub fn get_cache_dir(checksum: u32) -> std::io::Result<PathBuf> {
let base = dirs::cache_dir()
.ok_or_else(|| std::io::Error::new(std::io::ErrorKind::NotFound, "no cache directory"))?;
Ok(base.join("smolvm-pack").join(format!("{:08x}", checksum)))
}
pub fn is_extracted(cache_dir: &Path) -> bool {
cache_dir.join(EXTRACTION_MARKER).exists()
}
pub fn pack_cache_max_bytes() -> u64 {
const DEFAULT: u64 = 5 * 1024 * 1024 * 1024;
std::env::var("SMOLVM_PACK_CACHE_MAX_BYTES")
.ok()
.and_then(|s| s.trim().parse::<u64>().ok())
.filter(|&n| n > 0)
.unwrap_or(DEFAULT)
}
fn max_extract_entries() -> u64 {
const DEFAULT: u64 = 2_000_000;
std::env::var("SMOLVM_PACK_MAX_ENTRIES")
.ok()
.and_then(|s| s.trim().parse::<u64>().ok())
.filter(|&n| n > 0)
.unwrap_or(DEFAULT)
}
fn max_extract_total_bytes() -> u64 {
const DEFAULT: u64 = 128 * 1024 * 1024 * 1024;
std::env::var("SMOLVM_PACK_MAX_EXTRACT_BYTES")
.ok()
.and_then(|s| s.trim().parse::<u64>().ok())
.filter(|&n| n > 0)
.unwrap_or(DEFAULT)
}
#[cfg(unix)]
fn file_disk_usage(meta: &fs::Metadata) -> u64 {
use std::os::unix::fs::MetadataExt;
meta.blocks().saturating_mul(512)
}
#[cfg(not(unix))]
fn file_disk_usage(meta: &fs::Metadata) -> u64 {
meta.len()
}
fn dir_disk_usage(path: &Path) -> u64 {
let mut total = 0u64;
let entries = match fs::read_dir(path) {
Ok(e) => e,
Err(_) => return 0,
};
for entry in entries.flatten() {
let meta = match entry.metadata() {
Ok(m) => m,
Err(_) => continue,
};
if meta.is_dir() {
total = total.saturating_add(dir_disk_usage(&entry.path()));
} else if meta.is_file() {
total = total.saturating_add(file_disk_usage(&meta));
}
}
total
}
pub fn evict_cache_to_size(cache_root: &Path, max_bytes: u64) -> u64 {
evict_cache_to_size_protecting(cache_root, max_bytes, None)
}
pub fn evict_cache_to_size_protecting(
cache_root: &Path,
max_bytes: u64,
protect: Option<&Path>,
) -> u64 {
let protect_canon = protect.and_then(|p| fs::canonicalize(p).ok());
let mut entries: Vec<(PathBuf, std::time::SystemTime, u64)> = Vec::new();
let read_dir = match fs::read_dir(cache_root) {
Ok(rd) => rd,
Err(_) => return 0,
};
for entry in read_dir.flatten() {
let path = entry.path();
let meta = match fs::metadata(&path) {
Ok(m) => m,
Err(_) => continue,
};
if !meta.is_dir() {
continue; }
let modified = meta.modified().unwrap_or(std::time::SystemTime::UNIX_EPOCH);
entries.push((path, modified, dir_disk_usage(&entry.path())));
}
let total: u64 = entries.iter().map(|(_, _, s)| *s).sum();
if total <= max_bytes {
return 0;
}
entries.sort_by_key(|(_, modified, _)| *modified);
let mut over = total - max_bytes;
let mut freed = 0u64;
for (path, _, size) in entries {
if over == 0 {
break;
}
if has_active_leases(&path) {
continue; }
if protect_canon
.as_deref()
.is_some_and(|pc| fs::canonicalize(&path).ok().as_deref() == Some(pc))
{
continue; }
force_detach_layers_volume(&path);
if fs::remove_dir_all(&path).is_ok() {
let _ = fs::remove_file(path.with_extension("lock"));
freed = freed.saturating_add(size);
over = over.saturating_sub(size);
}
}
freed
}
fn is_sidecar_mode(footer: &PackFooter) -> bool {
footer.assets_offset == 0
}
pub fn sidecar_path_for(exe_path: &Path) -> PathBuf {
let filename = exe_path
.file_name()
.map(|s| s.to_string_lossy().to_string())
.unwrap_or_default();
exe_path.with_file_name(format!("{}{}", filename, SIDECAR_EXTENSION))
}
pub fn extract_sidecar(
sidecar_path: &Path,
cache_dir: &Path,
footer: &PackFooter,
force: bool,
debug: bool,
) -> std::io::Result<()> {
extract_sidecar_capped(sidecar_path, cache_dir, footer, force, debug, true)
}
fn extract_sidecar_capped(
sidecar_path: &Path,
cache_dir: &Path,
footer: &PackFooter,
force: bool,
debug: bool,
cap_cache: bool,
) -> std::io::Result<()> {
if !sidecar_path.exists() {
return Err(std::io::Error::new(
std::io::ErrorKind::NotFound,
format!("sidecar file not found: {}", sidecar_path.display()),
));
}
if let Some(parent) = cache_dir.parent() {
fs::create_dir_all(parent)?;
}
let lock_path = cache_dir.with_extension("lock");
let lock_file = fs::OpenOptions::new()
.create(true)
.write(true)
.truncate(false)
.open(&lock_path)?;
lock_file_exclusive(&lock_file)?;
if !force && is_extracted(cache_dir) {
if debug {
eprintln!("debug: assets already extracted (possibly by another process)");
}
return Ok(());
}
if force && cache_dir.exists() {
force_detach_layers_volume(cache_dir);
let _ = fs::remove_dir_all(cache_dir);
}
let result = extract_sidecar_inner(sidecar_path, cache_dir, footer, debug);
if result.is_err() && cache_dir.exists() && !is_extracted(cache_dir) {
let _ = fs::remove_dir_all(cache_dir);
}
if result.is_ok() && cap_cache {
if let Some(root) = cache_dir.parent() {
let freed =
evict_cache_to_size_protecting(root, pack_cache_max_bytes(), Some(cache_dir));
if freed > 0 && debug {
eprintln!("debug: pack cache evicted {freed} bytes to stay under cap");
}
}
}
result
}
pub fn shared_extract_enabled() -> bool {
cfg!(target_os = "linux") && std::env::var_os("SMOLVM_DISABLE_SHARED_EXTRACT").is_none()
}
pub fn shared_pack_dir(shared_root: &Path, checksum: u32) -> PathBuf {
shared_root.join(format!("{:08x}", checksum))
}
pub fn extract_sidecar_shared(
sidecar_path: &Path,
shared_root: &Path,
footer: &PackFooter,
debug: bool,
) -> std::io::Result<PathBuf> {
let shared_dir = shared_pack_dir(shared_root, footer.checksum);
extract_sidecar_capped(sidecar_path, &shared_dir, footer, false, debug, false)?;
restrict_to_owner(shared_root);
restrict_to_owner(&shared_dir);
Ok(shared_dir)
}
fn restrict_to_owner(dir: &Path) {
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
if let Ok(meta) = fs::metadata(dir) {
let mut perms = meta.permissions();
perms.set_mode(0o700);
let _ = fs::set_permissions(dir, perms);
}
}
#[cfg(not(unix))]
let _ = dir;
}
fn extract_sidecar_inner(
sidecar_path: &Path,
cache_dir: &Path,
footer: &PackFooter,
debug: bool,
) -> std::io::Result<()> {
fs::create_dir_all(cache_dir)?;
if debug {
eprintln!(
"debug: reading {} bytes of compressed assets from sidecar {}",
footer.assets_size,
sidecar_path.display()
);
}
let sidecar_file = File::open(sidecar_path)?;
let limited_reader = sidecar_file.take(footer.assets_size);
let decoder = zstd::stream::Decoder::new(limited_reader)
.map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidData, e))?;
let mut archive = tar::Archive::new(decoder);
safe_unpack(&mut archive, cache_dir)?;
if debug {
eprintln!("debug: extracted assets to {}", cache_dir.display());
}
let layer_order = crate::packer::read_manifest_from_sidecar(sidecar_path)
.ok()
.map(|m| {
m.assets
.layers
.iter()
.filter_map(|l| layer_id_from_asset_path(&l.path))
.collect::<Vec<_>>()
})
.unwrap_or_default();
post_process_extraction(cache_dir, &layer_order, debug)?;
Ok(())
}
pub fn extract_from_binary(
exe_path: &Path,
cache_dir: &Path,
footer: &PackFooter,
debug: bool,
) -> std::io::Result<()> {
fs::create_dir_all(cache_dir)?;
if is_sidecar_mode(footer) {
let sidecar = sidecar_path_for(exe_path);
extract_sidecar(&sidecar, cache_dir, footer, false, debug)
} else {
let mut exe_file = File::open(exe_path)?;
exe_file.seek(SeekFrom::Start(footer.assets_offset))?;
if debug {
eprintln!(
"debug: reading {} bytes of compressed assets from offset {}",
footer.assets_size, footer.assets_offset
);
}
let limited_reader = (&mut exe_file).take(footer.assets_size);
let decoder = zstd::stream::Decoder::new(limited_reader)
.map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidData, e))?;
let mut archive = tar::Archive::new(decoder);
safe_unpack(&mut archive, cache_dir)?;
if debug {
eprintln!("debug: extracted assets to {}", cache_dir.display());
}
post_process_extraction(cache_dir, &[], debug)?;
Ok(())
}
}
#[cfg(target_os = "macos")]
pub unsafe fn extract_from_section(
cache_dir: &Path,
assets_ptr: *const u8,
assets_size: usize,
debug: bool,
) -> std::io::Result<()> {
fs::create_dir_all(cache_dir)?;
if debug {
eprintln!(
"debug: extracting {} bytes of compressed assets from section",
assets_size
);
}
let assets_slice = unsafe { std::slice::from_raw_parts(assets_ptr, assets_size) };
let cursor = std::io::Cursor::new(assets_slice);
let decoder = zstd::stream::Decoder::new(cursor)
.map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidData, e))?;
let mut archive = tar::Archive::new(decoder);
safe_unpack(&mut archive, cache_dir)?;
if debug {
eprintln!("debug: extracted assets to {}", cache_dir.display());
}
post_process_extraction(cache_dir, &[], debug)?;
Ok(())
}
const LAYER_ORDER_FILE: &str = "layer-order";
fn layer_id_from_asset_path(path: &str) -> Option<String> {
let rel = Path::new(path);
(!rel.is_absolute())
.then_some(rel)?
.file_stem()
.and_then(|s| s.to_str())
.filter(|s| !s.is_empty())
.map(ToOwned::to_owned)
}
fn post_process_extraction(
cache_dir: &Path,
layer_order: &[String],
debug: bool,
) -> std::io::Result<()> {
let rootfs_tar = cache_dir.join("agent-rootfs.tar");
let rootfs_dir = cache_dir.join("agent-rootfs");
if rootfs_tar.exists() && !rootfs_dir.exists() {
if debug {
eprintln!("debug: extracting agent-rootfs.tar...");
}
fs::create_dir_all(&rootfs_dir)?;
let tar_file = File::open(&rootfs_tar)?;
let mut archive = tar::Archive::new(tar_file);
safe_unpack(&mut archive, &rootfs_dir)?;
}
let layers_dir = cache_dir.join("layers");
if layers_dir.exists() {
if debug {
eprintln!("debug: extracting OCI layers...");
}
let extract_dir = extraction_layers_dir(cache_dir, debug)?;
for entry in fs::read_dir(&layers_dir)? {
let entry = entry?;
let path = entry.path();
if path.extension().is_some_and(|ext| ext == "tar") {
let stem = path.file_stem().unwrap_or_default().to_string_lossy();
let layer_dir = extract_dir.join(&*stem);
if !layer_dir.exists() {
if debug {
eprintln!("debug: extracting layer {}...", stem);
}
fs::create_dir_all(&layer_dir)?;
let tar_file = File::open(&path)?;
let mut archive = tar::Archive::new(tar_file);
safe_unpack(&mut archive, &layer_dir)?;
}
}
}
if !layer_order.is_empty() {
let lines: Vec<&str> = layer_order
.iter()
.filter(|id| extract_dir.join(id).is_dir())
.map(String::as_str)
.collect();
if !lines.is_empty() {
fs::write(extract_dir.join(LAYER_ORDER_FILE), lines.join("\n"))?;
}
}
}
fs::write(cache_dir.join(EXTRACTION_MARKER), "")?;
let lib_dir = cache_dir.join("lib");
if lib_dir.exists() {
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
for entry in fs::read_dir(&lib_dir)? {
let entry = entry?;
let path = entry.path();
if path.is_file() {
let mut perms = fs::metadata(&path)?.permissions();
perms.set_mode(0o755);
fs::set_permissions(&path, perms)?;
}
}
}
}
Ok(())
}
#[cfg(target_os = "macos")]
const CS_IMAGE_NAME: &str = "layers-cs.sparseimage";
#[cfg(target_os = "macos")]
const CS_MOUNT_DIR: &str = "layers-cs";
#[cfg(target_os = "macos")]
const LEASES_DIR: &str = "leases";
#[cfg(target_os = "macos")]
const LEASES_LOCK: &str = "leases.lock";
pub struct LayersVolumeLease {
pub path: PathBuf,
#[cfg(target_os = "macos")]
cache_dir: PathBuf,
}
impl Drop for LayersVolumeLease {
fn drop(&mut self) {
#[cfg(target_os = "macos")]
{
release_lease(&self.cache_dir);
}
}
}
pub fn acquire_layers_lease(cache_dir: &Path, debug: bool) -> std::io::Result<LayersVolumeLease> {
#[cfg(target_os = "macos")]
{
let image_path = cache_dir.join(CS_IMAGE_NAME);
if image_path.exists() || has_layer_tars(cache_dir) {
let path = acquire_lease(cache_dir, debug)?;
return Ok(LayersVolumeLease {
path,
cache_dir: cache_dir.to_path_buf(),
});
}
}
let _ = debug;
Ok(LayersVolumeLease {
path: cache_dir.join("layers"),
#[cfg(target_os = "macos")]
cache_dir: cache_dir.to_path_buf(),
})
}
pub fn acquire_daemon_lease(
cache_dir: &Path,
daemon_pid: i32,
debug: bool,
) -> std::io::Result<PathBuf> {
#[cfg(target_os = "macos")]
{
let image_path = cache_dir.join(CS_IMAGE_NAME);
if image_path.exists() || has_layer_tars(cache_dir) {
let leases_dir = cache_dir.join(LEASES_DIR);
fs::create_dir_all(&leases_dir)?;
let lock = lock_leases(cache_dir)?;
gc_stale_leases(&leases_dir);
ensure_cs_volume_mounted(cache_dir, debug)?;
fs::write(leases_dir.join("daemon"), format!("{}", daemon_pid))?;
drop(lock);
return Ok(cache_dir.join(CS_MOUNT_DIR));
}
}
let _ = (daemon_pid, debug);
Ok(cache_dir.join("layers"))
}
pub fn release_daemon_lease(cache_dir: &Path) {
#[cfg(target_os = "macos")]
{
let leases_dir = cache_dir.join(LEASES_DIR);
let daemon_lease = leases_dir.join("daemon");
if !daemon_lease.exists() {
return;
}
let Ok(lock) = lock_leases(cache_dir) else {
let _ = fs::remove_file(&daemon_lease);
return;
};
let _ = fs::remove_file(&daemon_lease);
gc_stale_leases(&leases_dir);
detach_if_unused(cache_dir);
drop(lock);
}
#[cfg(not(target_os = "macos"))]
{
let _ = cache_dir;
}
}
pub fn has_active_leases(cache_dir: &Path) -> bool {
#[cfg(target_os = "macos")]
{
let leases_dir = cache_dir.join(LEASES_DIR);
if !leases_dir.exists() {
return false;
}
let Ok(lock) = lock_leases(cache_dir) else {
return false;
};
gc_stale_leases(&leases_dir);
let active = count_leases(&leases_dir);
drop(lock);
active > 0
}
#[cfg(not(target_os = "macos"))]
{
let _ = cache_dir;
false
}
}
pub fn force_detach_layers_volume(cache_dir: &Path) {
#[cfg(target_os = "macos")]
{
let mount_point = cache_dir.join(CS_MOUNT_DIR);
if mount_point.exists() && is_mount_point(&mount_point) {
let _ = std::process::Command::new("hdiutil")
.args(["detach", "-quiet", "-force"])
.arg(&mount_point)
.output();
}
let _ = fs::remove_dir_all(cache_dir.join(LEASES_DIR));
}
#[cfg(not(target_os = "macos"))]
{
let _ = cache_dir;
}
}
fn extraction_layers_dir(cache_dir: &Path, debug: bool) -> std::io::Result<PathBuf> {
#[cfg(target_os = "macos")]
{
ensure_cs_volume_mounted(cache_dir, debug)?;
Ok(cache_dir.join(CS_MOUNT_DIR))
}
#[cfg(not(target_os = "macos"))]
{
let _ = debug;
Ok(cache_dir.join("layers"))
}
}
#[cfg(target_os = "macos")]
fn has_layer_tars(cache_dir: &Path) -> bool {
let layers_dir = cache_dir.join("layers");
layers_dir.exists()
&& fs::read_dir(&layers_dir)
.ok()
.map(|rd| {
rd.filter_map(|e| e.ok())
.any(|e| e.path().extension().is_some_and(|ext| ext == "tar"))
})
.unwrap_or(false)
}
#[cfg(target_os = "macos")]
fn sum_tar_sizes(dir: &Path) -> u64 {
let Ok(entries) = fs::read_dir(dir) else {
return 0;
};
entries
.filter_map(|e| e.ok())
.filter(|e| e.path().extension().is_some_and(|ext| ext == "tar"))
.filter_map(|e| e.metadata().ok())
.map(|m| m.len())
.sum()
}
#[cfg(target_os = "macos")]
fn is_mount_point(path: &Path) -> bool {
use std::os::unix::fs::MetadataExt;
let Ok(meta) = fs::metadata(path) else {
return false;
};
let Ok(parent_meta) = fs::metadata(path.parent().unwrap_or(Path::new("/"))) else {
return false;
};
meta.dev() != parent_meta.dev()
}
#[cfg(target_os = "macos")]
fn acquire_lease(cache_dir: &Path, debug: bool) -> std::io::Result<PathBuf> {
let mount_point = cache_dir.join(CS_MOUNT_DIR);
let leases_dir = cache_dir.join(LEASES_DIR);
fs::create_dir_all(&leases_dir)?;
let lock = lock_leases(cache_dir)?;
gc_stale_leases(&leases_dir);
ensure_cs_volume_mounted(cache_dir, debug)?;
let lease_path = leases_dir.join(format!("{}", std::process::id()));
fs::write(&lease_path, "")?;
drop(lock);
Ok(mount_point)
}
#[cfg(target_os = "macos")]
fn release_lease(cache_dir: &Path) {
let leases_dir = cache_dir.join(LEASES_DIR);
let lease_path = leases_dir.join(format!("{}", std::process::id()));
let Ok(lock) = lock_leases(cache_dir) else {
let _ = fs::remove_file(&lease_path);
return;
};
let _ = fs::remove_file(&lease_path);
gc_stale_leases(&leases_dir);
detach_if_unused(cache_dir);
drop(lock);
}
#[cfg(target_os = "macos")]
fn gc_stale_leases(leases_dir: &Path) {
let Ok(entries) = fs::read_dir(leases_dir) else {
return;
};
for entry in entries.filter_map(|e| e.ok()) {
let name = entry.file_name();
let name_str = name.to_string_lossy();
if name_str == "daemon" {
if let Ok(content) = fs::read_to_string(entry.path()) {
if let Ok(pid) = content.trim().parse::<i32>() {
if unsafe { libc::kill(pid, 0) } != 0 {
let _ = fs::remove_file(entry.path());
}
}
}
} else if let Ok(pid) = name_str.parse::<i32>() {
if unsafe { libc::kill(pid, 0) } != 0 {
let _ = fs::remove_file(entry.path());
}
}
}
}
#[cfg(target_os = "macos")]
fn count_leases(leases_dir: &Path) -> usize {
fs::read_dir(leases_dir)
.ok()
.map(|rd| rd.filter_map(|e| e.ok()).count())
.unwrap_or(0)
}
#[cfg(target_os = "macos")]
fn detach_if_unused(cache_dir: &Path) {
let leases_dir = cache_dir.join(LEASES_DIR);
if count_leases(&leases_dir) == 0 {
let mount_point = cache_dir.join(CS_MOUNT_DIR);
if mount_point.exists() && is_mount_point(&mount_point) {
let _ = std::process::Command::new("hdiutil")
.args(["detach", "-quiet"])
.arg(&mount_point)
.output();
}
}
}
#[cfg(target_os = "macos")]
fn lock_leases(cache_dir: &Path) -> std::io::Result<File> {
let lock_path = cache_dir.join(LEASES_LOCK);
let lock_file = fs::OpenOptions::new()
.create(true)
.write(true)
.truncate(false)
.open(&lock_path)?;
let ret = unsafe { libc::flock(lock_file.as_raw_fd(), libc::LOCK_EX) };
if ret != 0 {
return Err(std::io::Error::last_os_error());
}
Ok(lock_file)
}
#[cfg(target_os = "macos")]
fn ensure_cs_volume_mounted(cache_dir: &Path, debug: bool) -> std::io::Result<()> {
let image_path = cache_dir.join(CS_IMAGE_NAME);
let mount_point = cache_dir.join(CS_MOUNT_DIR);
if mount_point.exists() && is_mount_point(&mount_point) {
return Ok(());
}
if !image_path.exists() {
let layers_dir = cache_dir.join("layers");
let total_tar_bytes = sum_tar_sizes(&layers_dir);
let size_bytes = std::cmp::max(
(total_tar_bytes as f64 * 2.5) as u64 + 512 * 1024 * 1024,
1024 * 1024 * 1024,
);
let size_gib = size_bytes / (1024 * 1024 * 1024) + 1;
let size_arg = format!("{}g", size_gib);
if debug {
eprintln!(
"debug: creating case-sensitive APFS sparse image ({}g from {} bytes of tars)...",
size_gib, total_tar_bytes
);
}
let output = std::process::Command::new("hdiutil")
.args([
"create",
"-size",
&size_arg,
"-fs",
"Case-sensitive APFS",
"-type",
"SPARSE",
"-volname",
"smolvm-layers",
])
.arg(&image_path)
.output()?;
if !output.status.success() {
return Err(std::io::Error::other(format!(
"hdiutil create failed: {}",
String::from_utf8_lossy(&output.stderr)
)));
}
}
fs::create_dir_all(&mount_point)?;
if debug {
eprintln!(
"debug: mounting case-sensitive volume at {}",
mount_point.display()
);
}
let output = std::process::Command::new("hdiutil")
.args(["attach", "-mountpoint"])
.arg(&mount_point)
.args(["-nobrowse", "-noautoopen"])
.arg(&image_path)
.output()?;
if !output.status.success() {
return Err(std::io::Error::other(format!(
"hdiutil attach failed: {}",
String::from_utf8_lossy(&output.stderr)
)));
}
Ok(())
}
const LIBS_EXTRACTION_MARKER: &str = ".smolvm-libs-extracted";
pub fn extract_libs_from_binary(exe_path: &Path, debug: bool) -> std::io::Result<Option<PathBuf>> {
use crate::format::{LibsFooter, LIBS_FOOTER_SIZE};
let mut file = File::open(exe_path)?;
let file_size = file.metadata()?.len();
if file_size < LIBS_FOOTER_SIZE as u64 {
return Ok(None);
}
file.seek(SeekFrom::End(-(LIBS_FOOTER_SIZE as i64)))?;
let mut footer_buf = [0u8; LIBS_FOOTER_SIZE];
file.read_exact(&mut footer_buf)?;
let footer = match LibsFooter::from_bytes(&footer_buf) {
Ok(f) => f,
Err(_) => return Ok(None), };
if debug {
eprintln!(
"debug: found SMOLLIBS footer: offset={}, size={}",
footer.libs_offset, footer.libs_size
);
}
file.seek(SeekFrom::Start(footer.libs_offset))?;
let mut hasher = crc32fast::Hasher::new();
let mut remaining = footer.libs_size;
let mut buf = [0u8; 64 * 1024];
while remaining > 0 {
let to_read = remaining.min(buf.len() as u64) as usize;
let n = file.read(&mut buf[..to_read])?;
if n == 0 {
break;
}
hasher.update(&buf[..n]);
remaining -= n as u64;
}
let libs_checksum = hasher.finalize();
let cache_base = dirs::cache_dir()
.ok_or_else(|| std::io::Error::new(std::io::ErrorKind::NotFound, "no cache directory"))?;
let libs_cache_dir = cache_base
.join("smolvm-libs")
.join(format!("{:08x}", libs_checksum));
let lib_dir = libs_cache_dir.join("lib");
if let Some(parent) = libs_cache_dir.parent() {
fs::create_dir_all(parent)?;
}
let lock_path = libs_cache_dir.with_extension("lock");
let lock_file = fs::OpenOptions::new()
.create(true)
.write(true)
.truncate(false)
.open(&lock_path)?;
lock_file_exclusive(&lock_file)?;
if libs_cache_dir.join(LIBS_EXTRACTION_MARKER).exists() {
if debug {
eprintln!("debug: libs already extracted at {}", lib_dir.display());
}
let _ = lock_file;
return Ok(Some(lib_dir));
}
fs::create_dir_all(&libs_cache_dir)?;
file.seek(SeekFrom::Start(footer.libs_offset))?;
let limited_reader = (&mut file).take(footer.libs_size);
let decoder = zstd::stream::Decoder::new(limited_reader)
.map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidData, e))?;
let mut archive = tar::Archive::new(decoder);
safe_unpack(&mut archive, &libs_cache_dir)?;
if lib_dir.exists() {
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
for entry in fs::read_dir(&lib_dir)? {
let entry = entry?;
if entry.path().is_file() {
let mut perms = fs::metadata(entry.path())?.permissions();
perms.set_mode(0o755);
fs::set_permissions(entry.path(), perms)?;
}
}
}
}
fs::write(libs_cache_dir.join(LIBS_EXTRACTION_MARKER), "")?;
let _ = lock_file;
if debug {
eprintln!("debug: extracted libs to {}", lib_dir.display());
}
Ok(Some(lib_dir))
}
pub(crate) fn sparse_copy(src: &Path, dst: &Path) -> std::io::Result<()> {
let mut src_file = File::open(src)?;
let size = src_file.metadata()?.len();
let mut dst_file = File::create(dst)?;
#[cfg(windows)]
mark_file_sparse(&dst_file)?;
dst_file.set_len(size)?;
let mut buf = vec![0u8; 512 * 1024];
let mut offset: u64 = 0;
while offset < size {
let to_read = (size - offset).min(buf.len() as u64) as usize;
let n = src_file.read(&mut buf[..to_read])?;
if n == 0 {
break;
}
let chunk = &buf[..n];
if chunk.iter().any(|&b| b != 0) {
dst_file.seek(SeekFrom::Start(offset))?;
dst_file.write_all(chunk)?;
}
offset += n as u64;
}
Ok(())
}
pub fn create_storage_disk(path: &Path, size: u64) -> std::io::Result<()> {
let file = File::create(path)?;
#[cfg(windows)]
mark_file_sparse(&file)?;
file.set_len(size)?;
Ok(())
}
pub fn copy_overlay_template(
cache_dir: &Path,
template_path: Option<&str>,
dest: &Path,
size_gb_override: Option<u64>,
overlay_logical_size: Option<u64>,
) -> std::io::Result<()> {
let template = template_path.ok_or_else(|| {
std::io::Error::new(
std::io::ErrorKind::NotFound,
"overlay template not specified in manifest",
)
})?;
let src = resolve_cache_asset_path(cache_dir, template, "overlay template")?;
if !src.exists() {
return Err(std::io::Error::new(
std::io::ErrorKind::NotFound,
format!("overlay template not found: {}", src.display()),
));
}
sparse_copy(&src, dest)?;
let copied_size = fs::metadata(dest)?.len();
let target = [
Some(copied_size),
overlay_logical_size,
size_gb_override.map(|gb| gb * 1024 * 1024 * 1024),
]
.into_iter()
.flatten()
.max()
.unwrap_or(copied_size);
if target > copied_size {
let file = fs::OpenOptions::new().write(true).open(dest)?;
#[cfg(windows)]
mark_file_sparse(&file)?;
file.set_len(target)?;
}
Ok(())
}
pub fn create_or_copy_storage_disk(
cache_dir: &Path,
template_path: Option<&str>,
storage_path: &Path,
size_gb_override: Option<u64>,
) -> std::io::Result<()> {
if let Some(template) = template_path {
let template_path = resolve_cache_asset_path(cache_dir, template, "storage template")?;
if template_path.exists() {
sparse_copy(&template_path, storage_path)?;
if let Some(gb) = size_gb_override {
let desired = gb * 1024 * 1024 * 1024;
let current = fs::metadata(storage_path)?.len();
if desired > current {
let file = fs::OpenOptions::new().write(true).open(storage_path)?;
#[cfg(windows)]
mark_file_sparse(&file)?;
file.set_len(desired)?;
}
}
return Ok(());
}
}
let size = match size_gb_override {
Some(gb) => gb * 1024 * 1024 * 1024,
None => 512 * 1024 * 1024,
};
create_storage_disk(storage_path, size)
}
#[cfg(test)]
mod tests {
use super::*;
fn make_tar(name: &str, data: &[u8]) -> Vec<u8> {
let mut builder = tar::Builder::new(Vec::new());
let mut header = tar::Header::new_gnu();
header.set_size(data.len() as u64);
header.set_mode(0o644);
header.set_cksum();
builder.append_data(&mut header, name, data).unwrap();
builder.into_inner().unwrap()
}
#[cfg(unix)]
#[test]
fn test_unpack_sparse_rejects_symlink_at_destination() {
use std::os::unix::fs::symlink;
let temp_dir = tempfile::tempdir().unwrap();
let outside = temp_dir.path().join("outside.bin");
let dest = temp_dir.path().join("overlay.raw");
fs::write(&outside, b"untouched").unwrap();
symlink(&outside, &dest).unwrap();
let data = vec![0xFFu8; 512];
let tar_bytes = make_tar("overlay.raw", &data);
let mut archive = tar::Archive::new(tar_bytes.as_slice());
let mut entry = archive.entries().unwrap().next().unwrap().unwrap();
let real_dest = temp_dir.path().canonicalize().unwrap();
let result = unpack_sparse(&mut entry, &dest, data.len() as u64, 0o644, &real_dest);
assert!(result.is_err(), "should reject symlink at destination");
assert_eq!(result.unwrap_err().kind(), std::io::ErrorKind::InvalidData);
assert_eq!(fs::read(&outside).unwrap(), b"untouched");
}
fn make_symlink_tar(name: &str, link_target: &str) -> Vec<u8> {
let mut builder = tar::Builder::new(Vec::new());
let mut header = tar::Header::new_gnu();
header.set_entry_type(tar::EntryType::Symlink);
header.set_size(0);
header.set_mode(0o777);
builder.append_link(&mut header, name, link_target).unwrap();
builder.into_inner().unwrap()
}
#[cfg(unix)]
#[test]
fn test_safe_unpack_rejects_symlink_entry_escaping_dest_relative() {
let temp_dir = tempfile::tempdir().unwrap();
let outside = temp_dir.path().join("outside.bin");
fs::write(&outside, b"untouched").unwrap();
let dest = temp_dir.path().join("dest");
fs::create_dir(&dest).unwrap();
let tar_bytes = make_symlink_tar("evil", "../../outside.bin");
let mut archive = tar::Archive::new(tar_bytes.as_slice());
let result = safe_unpack(&mut archive, &dest);
assert!(
result.is_err(),
"escaping relative symlink must be rejected"
);
assert_eq!(result.unwrap_err().kind(), std::io::ErrorKind::InvalidData);
assert!(!dest.join("evil").exists(), "symlink must not be created");
assert_eq!(fs::read(&outside).unwrap(), b"untouched");
}
#[cfg(unix)]
#[test]
fn test_safe_unpack_rejects_symlink_entry_escaping_dest_absolute() {
let temp_dir = tempfile::tempdir().unwrap();
let dest = temp_dir.path().join("dest");
fs::create_dir(&dest).unwrap();
let tar_bytes = make_symlink_tar("evil", "/../../escape");
let mut archive = tar::Archive::new(tar_bytes.as_slice());
let result = safe_unpack(&mut archive, &dest);
assert!(
result.is_err(),
"escaping absolute symlink must be rejected"
);
assert_eq!(result.unwrap_err().kind(), std::io::ErrorKind::InvalidData);
assert!(!dest.join("evil").exists(), "symlink must not be created");
}
#[cfg(unix)]
#[test]
fn test_safe_unpack_allows_in_dest_symlink() {
let temp_dir = tempfile::tempdir().unwrap();
let dest = temp_dir.path().join("dest");
fs::create_dir(&dest).unwrap();
let tar_bytes = make_symlink_tar("link", "target");
let mut archive = tar::Archive::new(tar_bytes.as_slice());
safe_unpack(&mut archive, &dest).unwrap();
let meta = fs::symlink_metadata(dest.join("link")).unwrap();
assert!(
meta.file_type().is_symlink(),
"in-dest symlink should exist"
);
}
#[cfg(unix)]
#[test]
fn test_safe_unpack_sparse_symlink_parent_escape_blocked() {
use std::os::unix::fs::symlink;
let temp_dir = tempfile::tempdir().unwrap();
let sentinel = temp_dir.path().join("ESCAPE");
fs::create_dir(&sentinel).unwrap();
let dest = temp_dir.path().join("dest");
fs::create_dir(&dest).unwrap();
symlink(&sentinel, dest.join("foo")).unwrap();
let payload = vec![0xABu8; 4096];
let tar_bytes = make_tar("foo/pwned.bin", &payload);
let mut archive = tar::Archive::new(tar_bytes.as_slice());
let limits = SafeUnpackLimits {
max_entries: 1_000,
max_total_bytes: 1 << 30,
sparse_threshold: 512, };
let result = safe_unpack_with_limits(&mut archive, &dest, &limits);
assert!(
result.is_err(),
"symlink-parent escape via the sparse path must be rejected"
);
assert_eq!(result.unwrap_err().kind(), std::io::ErrorKind::InvalidData);
assert!(
!sentinel.join("pwned.bin").exists(),
"host file was written OUTSIDE dest — escape not blocked"
);
}
#[cfg(unix)]
#[test]
fn test_safe_unpack_rejects_absolute_symlink_aliasing_root() {
let temp_dir = tempfile::tempdir().unwrap();
let dest = temp_dir.path().join("dest");
fs::create_dir(&dest).unwrap();
let tar_bytes = make_symlink_tar("foo", "/");
let mut archive = tar::Archive::new(tar_bytes.as_slice());
let result = safe_unpack(&mut archive, &dest);
assert!(
result.is_err(),
"absolute symlink aliasing dest root must be rejected"
);
assert_eq!(result.unwrap_err().kind(), std::io::ErrorKind::InvalidData);
assert!(
!dest.join("foo").exists() && fs::symlink_metadata(dest.join("foo")).is_err(),
"root-aliasing symlink must never be created on disk"
);
}
#[cfg(unix)]
#[test]
fn test_unpack_sparse_strips_setuid() {
use std::os::unix::fs::PermissionsExt;
let temp_dir = tempfile::tempdir().unwrap();
let dest = temp_dir.path().join("suid.bin");
let real_dest = temp_dir.path().canonicalize().unwrap();
let data = vec![0x11u8; 4096];
let tar_bytes = make_tar("suid.bin", &data);
let mut archive = tar::Archive::new(tar_bytes.as_slice());
let mut entry = archive.entries().unwrap().next().unwrap().unwrap();
unpack_sparse(&mut entry, &dest, data.len() as u64, 0o6755, &real_dest).unwrap();
let mode = fs::metadata(&dest).unwrap().permissions().mode() & 0o7777;
assert_eq!(
mode, 0o0755,
"setuid/setgid/sticky bits must be stripped on the sparse path"
);
}
#[test]
fn test_safe_unpack_rejects_too_many_entries() {
let temp_dir = tempfile::tempdir().unwrap();
let dest = temp_dir.path().join("dest");
fs::create_dir(&dest).unwrap();
let mut builder = tar::Builder::new(Vec::new());
for i in 0..10 {
let data = b"x";
let mut h = tar::Header::new_gnu();
h.set_size(data.len() as u64);
h.set_mode(0o644);
builder
.append_data(&mut h, format!("file{i}.txt"), &data[..])
.unwrap();
}
let tar_bytes = builder.into_inner().unwrap();
let mut archive = tar::Archive::new(tar_bytes.as_slice());
let limits = SafeUnpackLimits {
max_entries: 3,
max_total_bytes: 1 << 30,
sparse_threshold: SPARSE_WRITE_THRESHOLD,
};
let err = safe_unpack_with_limits(&mut archive, &dest, &limits).unwrap_err();
assert_eq!(err.kind(), std::io::ErrorKind::InvalidData);
assert!(err.to_string().contains("max entry count"));
}
#[test]
fn test_safe_unpack_rejects_total_bytes_over_cap() {
let temp_dir = tempfile::tempdir().unwrap();
let dest = temp_dir.path().join("dest");
fs::create_dir(&dest).unwrap();
let data = vec![0u8; 4096];
let tar_bytes = make_tar("big.bin", &data);
let mut archive = tar::Archive::new(tar_bytes.as_slice());
let limits = SafeUnpackLimits {
max_entries: 1_000,
max_total_bytes: 1024, sparse_threshold: SPARSE_WRITE_THRESHOLD,
};
let err = safe_unpack_with_limits(&mut archive, &dest, &limits).unwrap_err();
assert_eq!(err.kind(), std::io::ErrorKind::InvalidData);
assert!(err.to_string().contains("max total size"));
}
#[test]
fn test_unpack_sparse_preserves_data_integrity() {
let temp_dir = tempfile::tempdir().unwrap();
let dest = temp_dir.path().join("data.raw");
let block = 64 * 1024;
let mut data = vec![0u8; 8 * block];
for i in (0..8).step_by(2) {
data[i * block..(i + 1) * block].fill(0xFF);
}
let tar_bytes = make_tar("data.raw", &data);
let mut archive = tar::Archive::new(tar_bytes.as_slice());
let mut entry = archive.entries().unwrap().next().unwrap().unwrap();
let real_dest = temp_dir.path().canonicalize().unwrap();
unpack_sparse(&mut entry, &dest, data.len() as u64, 0o644, &real_dest).unwrap();
assert_eq!(fs::read(&dest).unwrap(), data);
}
#[test]
fn test_cache_dir_format() {
let dir = get_cache_dir(0xDEADBEEF).unwrap();
assert!(dir.to_string_lossy().contains("deadbeef"));
}
#[test]
fn test_is_extracted() {
let temp_dir = tempfile::tempdir().unwrap();
assert!(!is_extracted(temp_dir.path()));
fs::write(temp_dir.path().join(EXTRACTION_MARKER), "").unwrap();
assert!(is_extracted(temp_dir.path()));
}
#[test]
fn test_is_extracted_partial() {
let temp_dir = tempfile::tempdir().unwrap();
fs::create_dir_all(temp_dir.path().join("lib")).unwrap();
fs::write(temp_dir.path().join("lib/libkrun.dylib"), "partial").unwrap();
assert!(!is_extracted(temp_dir.path()));
}
#[test]
fn test_sidecar_path_for() {
let exe = Path::new("/path/to/my-app");
let sidecar = sidecar_path_for(exe);
assert_eq!(sidecar, PathBuf::from("/path/to/my-app.smolmachine"));
}
#[test]
fn test_sidecar_mode_detection() {
let sidecar_footer = PackFooter {
stub_size: 0,
assets_offset: 0,
assets_size: 1000,
manifest_offset: 1000,
manifest_size: 500,
checksum: 0x12345678,
};
assert!(is_sidecar_mode(&sidecar_footer));
let embedded_footer = PackFooter {
stub_size: 50000,
assets_offset: 50000,
assets_size: 1000,
manifest_offset: 51000,
manifest_size: 500,
checksum: 0x12345678,
};
assert!(!is_sidecar_mode(&embedded_footer));
}
#[test]
fn test_create_storage_disk() {
let temp_dir = tempfile::tempdir().unwrap();
let disk_path = temp_dir.path().join("test.ext4");
create_storage_disk(&disk_path, 1024 * 1024).unwrap();
assert!(disk_path.exists());
assert_eq!(fs::metadata(&disk_path).unwrap().len(), 1024 * 1024);
}
#[test]
fn test_copy_overlay_template_fails_when_none() {
let temp_dir = tempfile::tempdir().unwrap();
let dest = temp_dir.path().join("overlay.raw");
let result = copy_overlay_template(temp_dir.path(), None, &dest, None, None);
assert!(result.is_err());
assert_eq!(result.unwrap_err().kind(), std::io::ErrorKind::NotFound);
}
#[test]
fn test_copy_overlay_template_fails_when_missing() {
let temp_dir = tempfile::tempdir().unwrap();
let dest = temp_dir.path().join("overlay.raw");
let result =
copy_overlay_template(temp_dir.path(), Some("nonexistent.raw"), &dest, None, None);
assert!(result.is_err());
assert_eq!(result.unwrap_err().kind(), std::io::ErrorKind::NotFound);
}
#[test]
fn test_copy_overlay_template_copies_and_extends() {
let temp_dir = tempfile::tempdir().unwrap();
let template = temp_dir.path().join("overlay.raw");
let dest = temp_dir.path().join("output.raw");
let template_data = vec![0u8; 1024];
fs::write(&template, &template_data).unwrap();
copy_overlay_template(temp_dir.path(), Some("overlay.raw"), &dest, None, None).unwrap();
assert_eq!(fs::metadata(&dest).unwrap().len(), 1024);
let dest2 = temp_dir.path().join("output2.raw");
copy_overlay_template(
temp_dir.path(),
Some("overlay.raw"),
&dest2,
None,
Some(4096),
)
.unwrap();
assert_eq!(fs::metadata(&dest2).unwrap().len(), 4096);
}
#[test]
fn test_copy_overlay_template_size_gb_takes_max() {
let temp_dir = tempfile::tempdir().unwrap();
let template = temp_dir.path().join("overlay.raw");
fs::write(&template, vec![0u8; 1024]).unwrap();
let dest = temp_dir.path().join("out_a.raw");
copy_overlay_template(
temp_dir.path(),
Some("overlay.raw"),
&dest,
Some(1), Some(4096),
)
.unwrap();
assert_eq!(fs::metadata(&dest).unwrap().len(), 1024 * 1024 * 1024);
let dest2 = temp_dir.path().join("out_b.raw");
copy_overlay_template(
temp_dir.path(),
Some("overlay.raw"),
&dest2,
None,
Some(8192), )
.unwrap();
assert_eq!(fs::metadata(&dest2).unwrap().len(), 8192);
}
#[test]
fn test_copy_overlay_template_rejects_traversal_path() {
let temp_dir = tempfile::tempdir().unwrap();
let outside = temp_dir.path().join("outside.raw");
let dest = temp_dir.path().join("overlay.raw");
fs::write(&outside, b"x").unwrap();
let result =
copy_overlay_template(temp_dir.path(), Some("../outside.raw"), &dest, None, None);
assert!(result.is_err());
assert_eq!(result.unwrap_err().kind(), std::io::ErrorKind::InvalidInput);
}
#[cfg(unix)]
#[test]
fn test_create_or_copy_storage_disk_rejects_symlink_escape() {
use std::os::unix::fs::symlink;
let temp_dir = tempfile::tempdir().unwrap();
let outside_dir = tempfile::tempdir().unwrap();
let outside_file = outside_dir.path().join("storage-template.ext4");
fs::write(&outside_file, b"template").unwrap();
symlink(outside_dir.path(), temp_dir.path().join("symlink-out")).unwrap();
let storage_path = temp_dir.path().join("storage.ext4");
let result = create_or_copy_storage_disk(
temp_dir.path(),
Some("symlink-out/storage-template.ext4"),
&storage_path,
None,
);
assert!(result.is_err());
assert_eq!(result.unwrap_err().kind(), std::io::ErrorKind::InvalidInput);
}
#[cfg(unix)]
#[test]
fn test_create_or_copy_storage_disk_preserves_sparseness() {
use std::os::unix::fs::MetadataExt;
let cache_dir = tempfile::tempdir().unwrap();
let template = cache_dir.path().join("storage-template.ext4");
{
let mut f = File::create(&template).unwrap();
f.write_all(b"real ext4 superblock stand-in").unwrap();
f.set_len(1024 * 1024 * 1024).unwrap();
}
let dest = cache_dir.path().join("storage.ext4");
create_or_copy_storage_disk(cache_dir.path(), Some("storage-template.ext4"), &dest, None)
.unwrap();
let meta = fs::metadata(&dest).unwrap();
assert_eq!(meta.len(), 1024 * 1024 * 1024);
let allocated_bytes = meta.blocks() * 512;
assert!(
allocated_bytes < 16 * 1024 * 1024,
"storage disk was densified: {allocated_bytes} bytes allocated for a sparse template"
);
}
#[test]
fn test_extract_sidecar_skips_when_already_extracted() {
let temp_dir = tempfile::tempdir().unwrap();
let cache_dir = temp_dir.path().join("cache");
fs::create_dir_all(&cache_dir).unwrap();
fs::write(cache_dir.join(EXTRACTION_MARKER), "").unwrap();
let dummy_footer = PackFooter {
stub_size: 0,
assets_offset: 0,
assets_size: 0,
manifest_offset: 0,
manifest_size: 0,
checksum: 0,
};
let result = extract_sidecar(
Path::new("/nonexistent/sidecar.smolmachine"),
&cache_dir,
&dummy_footer,
false, false,
);
drop(result);
let dummy_sidecar = temp_dir.path().join("dummy.smolmachine");
fs::write(&dummy_sidecar, b"").unwrap();
let result = extract_sidecar(
&dummy_sidecar,
&cache_dir,
&dummy_footer,
false, false,
);
assert!(result.is_ok());
}
#[test]
fn test_extract_sidecar_force_clears_marker() {
let temp_dir = tempfile::tempdir().unwrap();
let cache_dir = temp_dir.path().join("cache-force");
fs::create_dir_all(&cache_dir).unwrap();
fs::write(cache_dir.join(EXTRACTION_MARKER), "").unwrap();
assert!(is_extracted(&cache_dir));
let dummy_sidecar = temp_dir.path().join("force.smolmachine");
fs::write(&dummy_sidecar, b"not-a-real-zstd-stream").unwrap();
let dummy_footer = PackFooter {
stub_size: 0,
assets_offset: 0,
assets_size: 22, manifest_offset: 22,
manifest_size: 0,
checksum: 0,
};
let result = extract_sidecar(
&dummy_sidecar,
&cache_dir,
&dummy_footer,
true, false,
);
assert!(
result.is_err(),
"force extraction should attempt (and fail on dummy data)"
);
}
fn build_tar(entries: &[(&str, bool, &[u8])]) -> Vec<u8> {
let mut builder = tar::Builder::new(Vec::new());
for (path, is_dir, content) in entries {
let mut header = tar::Header::new_gnu();
if *is_dir {
header.set_entry_type(tar::EntryType::Directory);
header.set_size(0);
header.set_mode(0o755);
} else {
header.set_entry_type(tar::EntryType::Regular);
header.set_size(content.len() as u64);
header.set_mode(0o644);
}
header.set_cksum();
builder
.append_data(&mut header, *path, &content[..])
.unwrap();
}
builder.into_inner().unwrap()
}
#[test]
fn test_safe_unpack_normal_tar() {
let temp_dir = tempfile::tempdir().unwrap();
let dest_raw = temp_dir.path().join("out");
fs::create_dir_all(&dest_raw).unwrap();
let dest = dest_raw.canonicalize().unwrap();
let tar_data = build_tar(&[("dir/", true, b""), ("dir/file.txt", false, b"hello")]);
let mut archive = tar::Archive::new(tar_data.as_slice());
safe_unpack(&mut archive, &dest).unwrap();
assert!(dest.join("dir").is_dir());
assert_eq!(
fs::read_to_string(dest.join("dir/file.txt")).unwrap(),
"hello"
);
}
#[test]
#[cfg(target_os = "macos")]
fn test_safe_unpack_case_collision_fails_on_case_insensitive_fs() {
let temp_dir = tempfile::tempdir().unwrap();
let dest_raw = temp_dir.path().join("out");
fs::create_dir_all(&dest_raw).unwrap();
let dest = dest_raw.canonicalize().unwrap();
let tar_data = build_tar(&[
("share/", true, b""),
("share/pkg/", true, b""),
("share/pkg/lower", false, b"script content"),
("share/pkg/Lower/", true, b""),
("share/pkg/Lower/__init__.py", false, b"python code"),
]);
let mut archive = tar::Archive::new(tar_data.as_slice());
let result = safe_unpack(&mut archive, &dest);
assert!(
result.is_err(),
"case collision should fail on case-insensitive FS"
);
}
#[test]
#[cfg(target_os = "macos")]
fn test_layers_lease_creates_and_cleans_volume() {
let temp_dir = tempfile::tempdir().unwrap();
let cache_dir = temp_dir.path().join("cache");
fs::create_dir_all(cache_dir.join("layers")).unwrap();
fs::write(cache_dir.join("layers/dummy.tar"), b"").unwrap();
let lease = match acquire_layers_lease(&cache_dir, false) {
Ok(l) => l,
Err(e) => {
eprintln!("SKIP: hdiutil unavailable: {}", e);
return;
}
};
assert!(lease.path.exists());
assert!(is_mount_point(&lease.path));
fs::write(lease.path.join("lower"), "file").unwrap();
fs::create_dir_all(lease.path.join("Lower")).unwrap();
assert!(lease.path.join("lower").exists());
assert!(lease.path.join("Lower").is_dir());
let lease_file = cache_dir
.join(LEASES_DIR)
.join(format!("{}", std::process::id()));
assert!(lease_file.exists());
let mount_point = lease.path.clone();
drop(lease);
assert!(
!is_mount_point(&mount_point),
"volume should be detached after last lease drop"
);
}
#[test]
fn test_safe_unpack_skips_char_and_block_devices() {
let temp_dir = tempfile::tempdir().unwrap();
let dest_raw = temp_dir.path().join("out");
fs::create_dir_all(&dest_raw).unwrap();
let dest = dest_raw.canonicalize().unwrap();
let mut builder = tar::Builder::new(Vec::new());
let mut header = tar::Header::new_gnu();
header.set_entry_type(tar::EntryType::Regular);
header.set_size(5);
header.set_mode(0o644);
header.set_path("before.txt").unwrap();
header.set_cksum();
builder
.append_data(&mut header, "before.txt", &b"hello"[..])
.unwrap();
let mut header = tar::Header::new_gnu();
header.set_entry_type(tar::EntryType::Char);
header.set_size(0);
header.set_mode(0o644);
header.set_path("etc/alternatives/pager.1.gz").unwrap();
header.set_cksum();
builder
.append_data(&mut header, "etc/alternatives/pager.1.gz", &b""[..])
.unwrap();
let mut header = tar::Header::new_gnu();
header.set_entry_type(tar::EntryType::Block);
header.set_size(0);
header.set_mode(0o644);
header.set_path("dev/sda").unwrap();
header.set_cksum();
builder
.append_data(&mut header, "dev/sda", &b""[..])
.unwrap();
let mut header = tar::Header::new_gnu();
header.set_entry_type(tar::EntryType::Regular);
header.set_size(5);
header.set_mode(0o644);
header.set_path("after.txt").unwrap();
header.set_cksum();
builder
.append_data(&mut header, "after.txt", &b"world"[..])
.unwrap();
let tar_data = builder.into_inner().unwrap();
let mut archive = tar::Archive::new(tar_data.as_slice());
let result = safe_unpack(&mut archive, &dest);
assert!(
result.is_ok(),
"Char/Block entries should be skipped: {:?}",
result.err()
);
assert_eq!(
fs::read_to_string(dest.join("before.txt")).unwrap(),
"hello"
);
assert_eq!(fs::read_to_string(dest.join("after.txt")).unwrap(), "world");
assert!(!dest.join("etc/alternatives/pager.1.gz").exists());
assert!(!dest.join("dev/sda").exists());
}
#[test]
fn test_safe_unpack_skips_hardlink_to_whiteout() {
let temp_dir = tempfile::tempdir().unwrap();
let dest_raw = temp_dir.path().join("out");
fs::create_dir_all(&dest_raw).unwrap();
let dest = dest_raw.canonicalize().unwrap();
let mut builder = tar::Builder::new(Vec::new());
let mut header = tar::Header::new_gnu();
header.set_entry_type(tar::EntryType::Char);
header.set_size(0);
header.set_mode(0o000);
header.set_path("usr/lib/.build-id/84/target").unwrap();
header.set_cksum();
builder
.append_data(&mut header, "usr/lib/.build-id/84/target", &b""[..])
.unwrap();
let mut header = tar::Header::new_gnu();
header.set_entry_type(tar::EntryType::Link);
header.set_size(0);
header.set_mode(0o000);
header.set_path("usr/lib/.build-id/d9/link").unwrap();
header.set_link_name("usr/lib/.build-id/84/target").unwrap();
header.set_cksum();
builder
.append_data(&mut header, "usr/lib/.build-id/d9/link", &b""[..])
.unwrap();
let mut header = tar::Header::new_gnu();
header.set_entry_type(tar::EntryType::Regular);
header.set_size(2);
header.set_mode(0o644);
header.set_path("ok.txt").unwrap();
header.set_cksum();
builder
.append_data(&mut header, "ok.txt", &b"ok"[..])
.unwrap();
let tar_data = builder.into_inner().unwrap();
let mut archive = tar::Archive::new(tar_data.as_slice());
let result = safe_unpack(&mut archive, &dest);
assert!(
result.is_ok(),
"hardlink to skipped whiteout should be skipped: {:?}",
result.err()
);
assert!(!dest.join("usr/lib/.build-id/84/target").exists());
assert!(!dest.join("usr/lib/.build-id/d9/link").exists());
assert_eq!(fs::read_to_string(dest.join("ok.txt")).unwrap(), "ok");
}
#[test]
fn test_safe_unpack_readonly_parent_dir_does_not_block_children() {
let temp_dir = tempfile::tempdir().unwrap();
let dest_raw = temp_dir.path().join("out");
fs::create_dir_all(&dest_raw).unwrap();
let dest = dest_raw.canonicalize().unwrap();
let mut builder = tar::Builder::new(Vec::new());
let mut header = tar::Header::new_gnu();
header.set_entry_type(tar::EntryType::Directory);
header.set_size(0);
header.set_mode(0o555); header.set_path("usr/lib64/pm-utils/").unwrap();
header.set_cksum();
builder
.append_data(&mut header, "usr/lib64/pm-utils/", &b""[..])
.unwrap();
let mut header = tar::Header::new_gnu();
header.set_entry_type(tar::EntryType::Directory);
header.set_size(0);
header.set_mode(0o555);
header.set_path("usr/lib64/pm-utils/module.d/").unwrap();
header.set_cksum();
builder
.append_data(&mut header, "usr/lib64/pm-utils/module.d/", &b""[..])
.unwrap();
let mut header = tar::Header::new_gnu();
header.set_entry_type(tar::EntryType::Regular);
header.set_size(4);
header.set_mode(0o644);
header
.set_path("usr/lib64/pm-utils/module.d/test.conf")
.unwrap();
header.set_cksum();
builder
.append_data(
&mut header,
"usr/lib64/pm-utils/module.d/test.conf",
&b"data"[..],
)
.unwrap();
let tar_data = builder.into_inner().unwrap();
let mut archive = tar::Archive::new(tar_data.as_slice());
let result = safe_unpack(&mut archive, &dest);
assert!(
result.is_ok(),
"read-only parent should not block children: {:?}",
result.err()
);
assert!(dest.join("usr/lib64/pm-utils/module.d").is_dir());
assert_eq!(
fs::read_to_string(dest.join("usr/lib64/pm-utils/module.d/test.conf")).unwrap(),
"data"
);
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
let mode = fs::metadata(dest.join("usr/lib64/pm-utils"))
.unwrap()
.permissions()
.mode()
& 0o777;
assert_eq!(mode, 0o555, "deferred directory mode should be 555");
}
}
#[test]
fn test_safe_unpack_mixed_fedora_overlay_layer() {
let temp_dir = tempfile::tempdir().unwrap();
let dest_raw = temp_dir.path().join("out");
fs::create_dir_all(&dest_raw).unwrap();
let dest = dest_raw.canonicalize().unwrap();
let mut builder = tar::Builder::new(Vec::new());
let mut header = tar::Header::new_gnu();
header.set_entry_type(tar::EntryType::Directory);
header.set_size(0);
header.set_mode(0o755);
header.set_path("usr/").unwrap();
header.set_cksum();
builder.append_data(&mut header, "usr/", &b""[..]).unwrap();
let mut header = tar::Header::new_gnu();
header.set_entry_type(tar::EntryType::Regular);
header.set_size(11);
header.set_mode(0o644);
header.set_path("usr/good1.txt").unwrap();
header.set_cksum();
builder
.append_data(&mut header, "usr/good1.txt", &b"good file 1"[..])
.unwrap();
let mut header = tar::Header::new_gnu();
header.set_entry_type(tar::EntryType::Char);
header.set_size(0);
header.set_mode(0o000);
header.set_device_major(0).unwrap();
header.set_device_minor(0).unwrap();
header.set_path("usr/.wh.removed-pkg").unwrap();
header.set_cksum();
builder
.append_data(&mut header, "usr/.wh.removed-pkg", &b""[..])
.unwrap();
let mut header = tar::Header::new_gnu();
header.set_entry_type(tar::EntryType::Regular);
header.set_size(11);
header.set_mode(0o644);
header.set_path("usr/good2.txt").unwrap();
header.set_cksum();
builder
.append_data(&mut header, "usr/good2.txt", &b"good file 2"[..])
.unwrap();
let mut header = tar::Header::new_gnu();
header.set_entry_type(tar::EntryType::Link);
header.set_size(0);
header.set_mode(0o000);
header.set_path("usr/link-to-removed").unwrap();
header.set_link_name("usr/.wh.removed-pkg").unwrap();
header.set_cksum();
builder
.append_data(&mut header, "usr/link-to-removed", &b""[..])
.unwrap();
let mut header = tar::Header::new_gnu();
header.set_entry_type(tar::EntryType::Regular);
header.set_size(19); header.set_mode(0o755);
header.set_path("usr/good3.sh").unwrap();
header.set_cksum();
builder
.append_data(&mut header, "usr/good3.sh", &b"#!/usr/bin/env bash"[..])
.unwrap();
let mut header = tar::Header::new_gnu();
header.set_entry_type(tar::EntryType::Char);
header.set_size(0);
header.set_mode(0o000);
header.set_device_major(0).unwrap();
header.set_device_minor(0).unwrap();
header.set_path("usr/.wh.another-removed").unwrap();
header.set_cksum();
builder
.append_data(&mut header, "usr/.wh.another-removed", &b""[..])
.unwrap();
let mut header = tar::Header::new_gnu();
header.set_entry_type(tar::EntryType::Regular);
header.set_size(5);
header.set_mode(0o644);
header.set_path("usr/good4.dat").unwrap();
header.set_cksum();
builder
.append_data(&mut header, "usr/good4.dat", &b"final"[..])
.unwrap();
let tar_data = builder.into_inner().unwrap();
let mut archive = tar::Archive::new(tar_data.as_slice());
let result = safe_unpack(&mut archive, &dest);
assert!(
result.is_ok(),
"mixed Fedora overlay should extract cleanly: {:?}",
result.err()
);
assert_eq!(
fs::read_to_string(dest.join("usr/good1.txt")).unwrap(),
"good file 1"
);
assert_eq!(
fs::read_to_string(dest.join("usr/good2.txt")).unwrap(),
"good file 2"
);
assert_eq!(
fs::read_to_string(dest.join("usr/good3.sh")).unwrap(),
"#!/usr/bin/env bash"
);
assert_eq!(
fs::read_to_string(dest.join("usr/good4.dat")).unwrap(),
"final"
);
assert!(!dest.join("usr/.wh.removed-pkg").exists());
assert!(!dest.join("usr/link-to-removed").exists());
assert!(!dest.join("usr/.wh.another-removed").exists());
}
#[test]
fn test_safe_unpack_unknown_tar_type_byte() {
let temp_dir = tempfile::tempdir().unwrap();
let dest_raw = temp_dir.path().join("out");
fs::create_dir_all(&dest_raw).unwrap();
let dest = dest_raw.canonicalize().unwrap();
let mut builder = tar::Builder::new(Vec::new());
let mut header = tar::Header::new_gnu();
header.set_entry_type(tar::EntryType::Regular);
header.set_size(6);
header.set_mode(0o644);
header.set_path("before.txt").unwrap();
header.set_cksum();
builder
.append_data(&mut header, "before.txt", &b"before"[..])
.unwrap();
let mut header = tar::Header::new_gnu();
header.set_entry_type(tar::EntryType::new(b'A'));
header.set_size(0);
header.set_mode(0o644);
header.set_path("unknown-type-entry").unwrap();
header.set_cksum();
builder
.append_data(&mut header, "unknown-type-entry", &b""[..])
.unwrap();
let mut header = tar::Header::new_gnu();
header.set_entry_type(tar::EntryType::Regular);
header.set_size(5);
header.set_mode(0o644);
header.set_path("after.txt").unwrap();
header.set_cksum();
builder
.append_data(&mut header, "after.txt", &b"after"[..])
.unwrap();
let tar_data = builder.into_inner().unwrap();
let mut archive = tar::Archive::new(tar_data.as_slice());
let result = safe_unpack(&mut archive, &dest);
assert!(
result.is_ok(),
"unknown tar type should be skipped: {:?}",
result.err()
);
assert_eq!(
fs::read_to_string(dest.join("before.txt")).unwrap(),
"before"
);
assert_eq!(fs::read_to_string(dest.join("after.txt")).unwrap(), "after");
assert!(!dest.join("unknown-type-entry").exists());
}
#[test]
fn test_evict_cache_to_size_lru() {
use std::ffi::CString;
let tmp = tempfile::tempdir().unwrap();
let root = tmp.path();
let mk = |name: &str, mtime_secs: i64| {
let d = root.join(name);
fs::create_dir_all(&d).unwrap();
fs::write(d.join("data"), vec![0u8; 1024 * 1024]).unwrap(); let c = CString::new(d.to_string_lossy().as_bytes()).unwrap();
let tv = libc::timeval {
tv_sec: mtime_secs,
tv_usec: 0,
};
let times = [tv, tv];
unsafe {
libc::utimes(c.as_ptr(), times.as_ptr());
}
d
};
let old = mk("aaaa", 1_000_000);
let mid = mk("bbbb", 2_000_000);
let new = mk("cccc", 3_000_000);
assert_eq!(evict_cache_to_size(root, 100 * 1024 * 1024), 0);
assert!(old.exists() && mid.exists() && new.exists());
let freed = evict_cache_to_size(root, 5 * 1024 * 1024 / 2);
assert!(freed > 0, "expected some bytes freed");
assert!(!old.exists(), "oldest extraction should be evicted");
assert!(mid.exists() && new.exists(), "newer extractions kept");
}
#[test]
fn test_evict_cache_protects_current_extraction() {
use std::ffi::CString;
let tmp = tempfile::tempdir().unwrap();
let root = tmp.path();
let mk = |name: &str, mtime_secs: i64| {
let d = root.join(name);
fs::create_dir_all(&d).unwrap();
fs::write(d.join("data"), vec![0u8; 4 * 1024 * 1024]).unwrap(); let c = CString::new(d.to_string_lossy().as_bytes()).unwrap();
let tv = libc::timeval {
tv_sec: mtime_secs,
tv_usec: 0,
};
let times = [tv, tv];
unsafe {
libc::utimes(c.as_ptr(), times.as_ptr());
}
d
};
let old = mk("aaaa", 1_000_000);
let current = mk("cccc", 3_000_000);
let freed = evict_cache_to_size_protecting(root, 1024 * 1024, Some(¤t));
assert!(freed > 0, "the old entry should still be evicted");
assert!(!old.exists(), "oldest unprotected extraction evicted");
assert!(
current.exists() && current.join("data").exists(),
"the protected (just-extracted) dir must survive even over cap"
);
}
#[cfg(target_os = "macos")]
#[test]
fn test_evict_cache_skips_active_lease() {
let tmp = tempfile::tempdir().unwrap();
let root = tmp.path();
let d = root.join("aaaa");
fs::create_dir_all(&d).unwrap();
fs::write(d.join("data"), vec![0u8; 2 * 1024 * 1024]).unwrap();
let leases = d.join(LEASES_DIR);
fs::create_dir_all(&leases).unwrap();
fs::write(leases.join("daemon"), format!("{}", std::process::id())).unwrap();
assert!(has_active_leases(&d), "live lease should be detected");
let freed = evict_cache_to_size(root, 0);
assert_eq!(freed, 0, "leased extraction must not be evicted");
assert!(d.exists());
}
}