use std::collections::HashMap;
use std::path::{Component, Path};
use a3s_box_core::error::{BoxError, Result};
use serde::{Deserialize, Serialize};
pub const DIFF_BASELINE_FILE: &str = "rootfs_snapshot.json";
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct RootfsFileInfo {
pub size: u64,
pub mode: u32,
pub is_dir: bool,
}
pub fn walk_rootfs(root: &Path) -> Result<HashMap<String, RootfsFileInfo>> {
let mut entries = HashMap::new();
walk_recursive(root, root, &mut entries)?;
Ok(entries)
}
pub fn create_diff_baseline_if_absent(box_dir: &Path, rootfs: &Path) -> Result<()> {
let destination = box_dir.join(DIFF_BASELINE_FILE);
if destination.exists() {
return Ok(());
}
let entries = walk_rootfs(rootfs)?;
let encoded = serde_json::to_vec(&entries).map_err(|error| {
BoxError::BuildError(format!("failed to encode rootfs baseline: {error}"))
})?;
let temporary = box_dir.join(format!(
".{DIFF_BASELINE_FILE}.{}.tmp",
uuid::Uuid::new_v4().simple()
));
std::fs::write(&temporary, encoded).map_err(BoxError::IoError)?;
let install = std::fs::hard_link(&temporary, &destination);
let _ = std::fs::remove_file(&temporary);
match install {
Ok(()) => Ok(()),
Err(error) if error.kind() == std::io::ErrorKind::AlreadyExists => Ok(()),
Err(error) => Err(BoxError::IoError(error)),
}
}
fn walk_recursive(
root: &Path,
current: &Path,
entries: &mut HashMap<String, RootfsFileInfo>,
) -> Result<()> {
let directory = match std::fs::read_dir(current) {
Ok(directory) => directory,
Err(error) if error.kind() == std::io::ErrorKind::PermissionDenied => return Ok(()),
Err(error) => return Err(BoxError::IoError(error)),
};
for entry in directory {
let entry = entry.map_err(BoxError::IoError)?;
let path = entry.path();
let relative = match path.strip_prefix(root) {
Ok(relative) => relative,
Err(_) => continue,
};
if a3s_box_core::rootfs_metadata::is_runtime_internal_rootfs_path(relative) {
continue;
}
let metadata = match std::fs::symlink_metadata(&path) {
Ok(metadata) => metadata,
Err(error) if error.kind() == std::io::ErrorKind::NotFound => continue,
Err(error) if error.kind() == std::io::ErrorKind::PermissionDenied => continue,
Err(error) => return Err(BoxError::IoError(error)),
};
#[cfg(unix)]
let mode = {
use std::os::unix::fs::MetadataExt;
metadata.mode()
};
#[cfg(not(unix))]
let mode = 0;
entries.insert(
rootfs_path_string(relative),
RootfsFileInfo {
size: metadata.len(),
mode,
is_dir: metadata.is_dir(),
},
);
if metadata.is_dir() {
walk_recursive(root, &path, entries)?;
}
}
Ok(())
}
fn rootfs_path_string(relative: &Path) -> String {
let segments = relative
.components()
.filter_map(|component| match component {
Component::Normal(segment) => Some(segment.to_string_lossy()),
_ => None,
})
.collect::<Vec<_>>();
format!("/{}", segments.join("/"))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn baseline_is_first_writer_wins_and_excludes_runtime_control_files() {
let directory = tempfile::tempdir().unwrap();
let box_dir = directory.path().join("box");
let rootfs = box_dir.join("rootfs");
std::fs::create_dir_all(rootfs.join("root")).unwrap();
std::fs::write(rootfs.join("root/original.txt"), b"original").unwrap();
std::fs::write(rootfs.join(".a3s-box-exec.json"), b"runtime").unwrap();
create_diff_baseline_if_absent(&box_dir, &rootfs).unwrap();
std::fs::write(rootfs.join("root/later.txt"), b"later").unwrap();
create_diff_baseline_if_absent(&box_dir, &rootfs).unwrap();
let encoded = std::fs::read(box_dir.join(DIFF_BASELINE_FILE)).unwrap();
let baseline: HashMap<String, RootfsFileInfo> = serde_json::from_slice(&encoded).unwrap();
assert!(baseline.contains_key("/root/original.txt"));
assert!(!baseline.contains_key("/root/later.txt"));
assert!(!baseline.contains_key("/.a3s-box-exec.json"));
}
#[cfg(unix)]
#[test]
fn walk_does_not_follow_rootfs_symlinks() {
let directory = tempfile::tempdir().unwrap();
let outside = directory.path().join("outside");
let rootfs = directory.path().join("rootfs");
std::fs::create_dir_all(&outside).unwrap();
std::fs::create_dir_all(&rootfs).unwrap();
std::fs::write(outside.join("secret"), b"secret").unwrap();
std::os::unix::fs::symlink(&outside, rootfs.join("escape")).unwrap();
let entries = walk_rootfs(&rootfs).unwrap();
assert!(entries.contains_key("/escape"));
assert!(!entries.contains_key("/escape/secret"));
}
}