use std::path::{Path, PathBuf};
use tokio::fs;
use super::types::{Patch, RootfsSource};
use crate::MicrosandboxResult;
pub(crate) async fn apply_patches(
image: &RootfsSource,
patches: &[Patch],
sandbox_dir: &Path,
resolved_layers: &[PathBuf],
) -> MicrosandboxResult<()> {
if patches.is_empty() {
return Ok(());
}
let (target_dir, lower_layers) = match image {
RootfsSource::Oci(_) => {
let rw_dir = sandbox_dir.join("rw");
(rw_dir, resolved_layers)
}
RootfsSource::Bind(host_dir) => (host_dir.clone(), [].as_slice()),
RootfsSource::DiskImage { .. } => {
return Err(crate::MicrosandboxError::InvalidConfig(
"patches are not compatible with disk image rootfs".into(),
));
}
};
for patch in patches {
apply_one(&target_dir, lower_layers, patch).await?;
}
Ok(())
}
async fn apply_one(
target_dir: &Path,
lower_layers: &[PathBuf],
patch: &Patch,
) -> MicrosandboxResult<()> {
match patch {
Patch::Text {
path,
content,
mode,
replace,
} => {
let dest = resolve_guest_path(target_dir, path)?;
check_replace(&dest, lower_layers, path, *replace)?;
ensure_parent(&dest).await?;
fs::write(&dest, content.as_bytes()).await?;
if let Some(mode) = mode {
set_permissions(&dest, *mode).await?;
}
}
Patch::File {
path,
content,
mode,
replace,
} => {
let dest = resolve_guest_path(target_dir, path)?;
check_replace(&dest, lower_layers, path, *replace)?;
ensure_parent(&dest).await?;
fs::write(&dest, content).await?;
if let Some(mode) = mode {
set_permissions(&dest, *mode).await?;
}
}
Patch::CopyFile {
src,
dst,
mode,
replace,
} => {
let dest = resolve_guest_path(target_dir, dst)?;
check_replace(&dest, lower_layers, dst, *replace)?;
ensure_parent(&dest).await?;
fs::copy(src, &dest).await?;
if let Some(mode) = mode {
set_permissions(&dest, *mode).await?;
}
}
Patch::CopyDir { src, dst, replace } => {
let dest = resolve_guest_path(target_dir, dst)?;
check_replace(&dest, lower_layers, dst, *replace)?;
copy_dir_recursive(src, &dest).await?;
}
Patch::Symlink {
target,
link,
replace,
} => {
let link_path = resolve_guest_path(target_dir, link)?;
check_replace(&link_path, lower_layers, link, *replace)?;
ensure_parent(&link_path).await?;
if link_path.exists() {
fs::remove_file(&link_path).await.ok();
}
#[cfg(unix)]
tokio::fs::symlink(target, &link_path).await?;
}
Patch::Mkdir { path, mode } => {
let dest = resolve_guest_path(target_dir, path)?;
fs::create_dir_all(&dest).await?;
if let Some(mode) = mode {
set_permissions(&dest, *mode).await?;
}
}
Patch::Remove { path } => {
let dest = resolve_guest_path(target_dir, path)?;
if dest.is_dir() {
fs::remove_dir_all(&dest).await.ok();
} else {
fs::remove_file(&dest).await.ok();
}
}
Patch::Append { path, content } => {
let dest = resolve_guest_path(target_dir, path)?;
if !dest.exists()
&& let Some(source) = find_in_layers(lower_layers, path)
{
ensure_parent(&dest).await?;
fs::copy(&source, &dest).await?;
}
if dest.exists() {
use tokio::io::AsyncWriteExt;
let mut file = fs::OpenOptions::new().append(true).open(&dest).await?;
file.write_all(content.as_bytes()).await?;
} else {
return Err(crate::MicrosandboxError::PatchFailed(format!(
"cannot append to '{path}': file not found in rootfs"
)));
}
}
}
Ok(())
}
fn resolve_guest_path(target_dir: &Path, guest_path: &str) -> MicrosandboxResult<PathBuf> {
if !guest_path.starts_with('/') {
return Err(crate::MicrosandboxError::PatchFailed(format!(
"patch path must be absolute: '{guest_path}'"
)));
}
let relative = guest_path.strip_prefix('/').unwrap_or(guest_path);
let resolved = target_dir.join(relative);
if !resolved.starts_with(target_dir) {
return Err(crate::MicrosandboxError::PatchFailed(format!(
"patch path escapes rootfs: '{guest_path}'"
)));
}
Ok(resolved)
}
fn check_replace(
dest: &Path,
lower_layers: &[PathBuf],
guest_path: &str,
replace: bool,
) -> MicrosandboxResult<()> {
if replace {
return Ok(());
}
if dest.exists() {
return Err(crate::MicrosandboxError::PatchFailed(format!(
"path already exists in rootfs: '{guest_path}' (set replace to allow)"
)));
}
if find_in_layers(lower_layers, guest_path).is_some() {
return Err(crate::MicrosandboxError::PatchFailed(format!(
"path exists in image layer: '{guest_path}' (set replace to allow)"
)));
}
Ok(())
}
fn find_in_layers(layers: &[PathBuf], guest_path: &str) -> Option<PathBuf> {
let relative = guest_path.strip_prefix('/').unwrap_or(guest_path);
for layer in layers.iter().rev() {
let candidate = layer.join(relative);
if candidate.exists() {
return Some(candidate);
}
}
None
}
async fn ensure_parent(path: &Path) -> MicrosandboxResult<()> {
if let Some(parent) = path.parent() {
fs::create_dir_all(parent).await?;
}
Ok(())
}
#[cfg(unix)]
async fn set_permissions(path: &Path, mode: u32) -> MicrosandboxResult<()> {
use std::os::unix::fs::PermissionsExt;
let perms = std::fs::Permissions::from_mode(mode);
fs::set_permissions(path, perms).await?;
Ok(())
}
async fn copy_dir_recursive(src: &Path, dst: &Path) -> MicrosandboxResult<()> {
fs::create_dir_all(dst).await?;
let mut entries = fs::read_dir(src).await?;
while let Some(entry) = entries.next_entry().await? {
let src_path = entry.path();
let dst_path = dst.join(entry.file_name());
if entry.file_type().await?.is_dir() {
Box::pin(copy_dir_recursive(&src_path, &dst_path)).await?;
} else {
fs::copy(&src_path, &dst_path).await?;
}
}
Ok(())
}