#[cfg(target_os = "macos")]
mod apfs;
mod baseline;
mod builder;
#[cfg(unix)]
mod ext4;
#[cfg(any(target_os = "macos", all(unix, test)))]
mod ext4_artifact;
#[cfg(any(target_os = "macos", all(unix, test)))]
mod ext4_cache;
#[cfg(target_os = "macos")]
mod guest_native_ext4;
#[cfg(target_os = "macos")]
mod guest_native_migration;
mod layout;
#[cfg(any(target_os = "macos", all(unix, test)))]
mod oci_ext4;
pub(crate) mod overlay;
mod provider;
mod staging_path;
pub use baseline::{
create_diff_baseline_if_absent, guest_diff_baseline_required, publish_guest_diff_baseline,
walk_rootfs, RootfsFileInfo, DIFF_BASELINE_FILE,
};
pub use builder::RootfsBuilder;
#[cfg(unix)]
pub use ext4::{
publish_ext4_artifact, Ext4Artifact, Ext4ArtifactManifest, Ext4ArtifactOptions,
EXT4_ARTIFACT_SCHEMA, EXT4_BUILDER_ID,
};
#[cfg(target_os = "macos")]
pub(crate) use ext4_cache::{Ext4ArtifactCache, Ext4CacheIdentity};
#[cfg(target_os = "macos")]
pub use guest_native_ext4::GuestNativeExt4Provider;
pub use layout::{GuestLayout, GUEST_WORKDIR};
pub use provider::{
default_provider, default_provider_for_box, CopyProvider, OverlayProvider, ResumedRootfs,
RootfsArtifactCacheOptions, RootfsFinalizeOptions, RootfsOciPrepareOptions, RootfsProvider,
RootfsResumeOptions,
};
pub(crate) use provider::{default_provider_for_boot, default_provider_for_box_boot};
pub(crate) use staging_path::{
ensure_directory_transport_is_lossless, host_staging_path, logical_path_for_staged_child,
staging_path_map,
};
use std::io::Read;
use std::path::{Path, PathBuf};
use a3s_box_core::error::{BoxError, Result};
use a3s_box_core::guest_exec::{
GuestTerminalStatus, GUEST_TERMINAL_STATUS_FILE_NAME, MAX_GUEST_TERMINAL_STATUS_BYTES,
};
enum TerminalStatusRead {
Absent,
PendingOrInvalid,
Complete(GuestTerminalStatus),
}
fn read_guest_terminal_status(box_dir: &Path) -> TerminalStatusRead {
let path = box_dir
.join("runtime-control")
.join(GUEST_TERMINAL_STATUS_FILE_NAME);
let metadata = match std::fs::symlink_metadata(&path) {
Ok(metadata) => metadata,
Err(error) if error.kind() == std::io::ErrorKind::NotFound => {
return TerminalStatusRead::Absent;
}
Err(_) => return TerminalStatusRead::PendingOrInvalid,
};
if !metadata.is_file()
|| metadata.file_type().is_symlink()
|| metadata.len() > MAX_GUEST_TERMINAL_STATUS_BYTES as u64
{
return TerminalStatusRead::PendingOrInvalid;
}
let mut options = std::fs::OpenOptions::new();
options.read(true);
#[cfg(unix)]
{
use std::os::unix::fs::OpenOptionsExt;
options.custom_flags(libc::O_CLOEXEC | libc::O_NOFOLLOW);
}
let Ok(file) = options.open(&path) else {
return TerminalStatusRead::PendingOrInvalid;
};
let mut bytes = Vec::with_capacity(metadata.len() as usize);
if file
.take(MAX_GUEST_TERMINAL_STATUS_BYTES as u64 + 1)
.read_to_end(&mut bytes)
.is_err()
|| bytes.is_empty()
|| bytes.len() > MAX_GUEST_TERMINAL_STATUS_BYTES
{
return TerminalStatusRead::PendingOrInvalid;
}
let Ok(status) = serde_json::from_slice::<GuestTerminalStatus>(&bytes) else {
return TerminalStatusRead::PendingOrInvalid;
};
if status.validate().is_err() {
return TerminalStatusRead::PendingOrInvalid;
}
TerminalStatusRead::Complete(status)
}
pub(crate) fn guest_rootfs_handoff_complete(box_dir: &Path) -> bool {
matches!(
read_guest_terminal_status(box_dir),
TerminalStatusRead::Complete(GuestTerminalStatus {
rootfs_quiesced: true,
..
})
)
}
pub fn read_persisted_exit_code(box_dir: &Path) -> Option<i32> {
resolve_workload_exit_code(box_dir, None)
}
pub fn resolve_workload_exit_code(box_dir: &Path, provider_exit_code: Option<i32>) -> Option<i32> {
match read_guest_terminal_status(box_dir) {
TerminalStatusRead::Complete(status) => return Some(status.exit_code),
TerminalStatusRead::PendingOrInvalid => {
return provider_exit_code.filter(|exit_code| *exit_code != 0);
}
TerminalStatusRead::Absent => {}
}
let candidates = [
box_dir
.join("upper")
.join(a3s_box_core::rootfs_metadata::EXIT_CODE_PATH.trim_start_matches('/')),
box_dir
.join("rootfs")
.join(".a3s-rootfs")
.join(a3s_box_core::rootfs_metadata::EXIT_CODE_PATH.trim_start_matches('/')),
box_dir
.join("rootfs")
.join(a3s_box_core::rootfs_metadata::EXIT_CODE_PATH.trim_start_matches('/')),
];
candidates
.into_iter()
.find_map(|path| {
std::fs::read_to_string(path)
.ok()
.and_then(|contents| contents.trim().parse::<i32>().ok())
})
.or(provider_exit_code)
}
pub struct AttachedRootfs {
path: std::path::PathBuf,
detach_on_drop: bool,
}
pub fn guest_native_ext4_generation_exists(box_dir: &Path) -> Result<bool> {
let path = box_dir.join("rootfs-ext4-v1");
match std::fs::symlink_metadata(&path) {
Ok(_) => Ok(true),
Err(error) if error.kind() == std::io::ErrorKind::NotFound => Ok(false),
Err(error) => Err(BoxError::BuildError(format!(
"Failed to inspect guest-native rootfs generation {}: {error}",
path.display()
))),
}
}
pub fn guest_native_ext4_disk_mib(box_dir: &Path) -> Result<Option<u32>> {
if !guest_native_ext4_generation_exists(box_dir)? {
return Ok(None);
}
#[cfg(target_os = "macos")]
{
let directory = GuestNativeExt4Provider::artifact_directory(box_dir);
let (artifact, _) = ext4_artifact::open_ext4_artifact_for_resume(&directory)?;
const MIB: u64 = 1024 * 1024;
if artifact.manifest.capacity_bytes % MIB != 0 {
return Err(BoxError::StateError(format!(
"Guest-native rootfs capacity is not MiB-aligned at {}",
artifact.disk.display()
)));
}
let disk_mib = u32::try_from(artifact.manifest.capacity_bytes / MIB).map_err(|_| {
BoxError::StateError(format!(
"Guest-native rootfs capacity exceeds the Box configuration range at {}",
artifact.disk.display()
))
})?;
Ok(Some(disk_mib))
}
#[cfg(not(target_os = "macos"))]
{
Err(BoxError::StateError(format!(
"Guest-native rootfs state is unsupported on this host: {}",
box_dir.join("rootfs-ext4-v1").display()
)))
}
}
#[cfg(target_os = "macos")]
pub(crate) fn open_clean_guest_native_ext4_artifact(
artifact_directory: &Path,
) -> Result<Ext4Artifact> {
let (artifact, validation) = ext4_artifact::open_ext4_artifact_for_resume(artifact_directory)?;
if validation == ext4::Ext4ResumeValidation::JournalRecoveryRequired {
return Err(BoxError::StateError(format!(
"Guest-native rootfs at {} needs ext4 journal recovery; start the box and stop it cleanly before creating or restoring a filesystem snapshot",
artifact.disk.display()
)));
}
Ok(artifact)
}
#[cfg(target_os = "macos")]
pub(crate) fn clone_clean_guest_native_ext4_artifact(
artifact_directory: &Path,
destination: &Path,
) -> Result<Ext4Artifact> {
let source = open_clean_guest_native_ext4_artifact(artifact_directory)?;
let cloned = ext4_cache::clone_artifact(&source, destination)?;
let validated = match open_clean_guest_native_ext4_artifact(destination) {
Ok(validated) => validated,
Err(error) => {
let _ = std::fs::remove_dir_all(destination);
return Err(error);
}
};
if cloned != validated || source.manifest != validated.manifest {
let _ = std::fs::remove_dir_all(destination);
return Err(BoxError::StateError(format!(
"Cloned guest-native rootfs identity changed at {}",
destination.display()
)));
}
Ok(validated)
}
#[cfg(target_os = "macos")]
pub(crate) fn guest_native_ext4_sparse_digest(artifact: &Ext4Artifact) -> Result<String> {
ext4_cache::sparse_sha256(&artifact.disk, artifact.manifest.capacity_bytes)
}
#[cfg(target_os = "macos")]
pub(crate) fn guest_native_ext4_allocated_bytes(artifact_directory: &Path) -> Result<u64> {
ext4_cache::allocated_bytes(artifact_directory)
}
#[cfg(target_os = "macos")]
pub(crate) fn guest_native_ext4_maintenance_disk(box_dir: &Path) -> Result<PathBuf> {
let directory = GuestNativeExt4Provider::artifact_directory(box_dir);
let (artifact, validation) = ext4_artifact::open_ext4_artifact_for_resume(&directory)?;
if validation == ext4::Ext4ResumeValidation::JournalRecoveryRequired {
return Err(BoxError::StateError(format!(
"Guest-native rootfs at {} needs ext4 journal recovery; start the box and stop it cleanly before offline diff, export, or commit",
artifact.disk.display()
)));
}
Ok(artifact.disk)
}
impl AttachedRootfs {
pub fn path(&self) -> &Path {
&self.path
}
}
impl Drop for AttachedRootfs {
fn drop(&mut self) {
if self.detach_on_drop {
unmount_box_rootfs(&self.path);
}
}
}
pub fn attach_persistent_rootfs(
box_dir: &Path,
) -> a3s_box_core::error::Result<Option<AttachedRootfs>> {
if guest_native_ext4_generation_exists(box_dir)? {
return Err(BoxError::StateError(
"Guest-native rootfs generations have no host directory attachment; use the trusted maintenance archive path for stopped access"
.to_string(),
));
}
#[cfg(target_os = "macos")]
{
let image = box_dir.join("rootfs-apfs-v2.sparseimage");
if !image.is_file() {
return Ok(None);
}
let rootfs = box_dir.join("rootfs");
let was_mounted = is_mountpoint(&rootfs);
let path = provider::CaseSensitiveApfsProvider.prepare_empty(box_dir)?;
Ok(Some(AttachedRootfs {
path,
detach_on_drop: !was_mounted,
}))
}
#[cfg(not(target_os = "macos"))]
{
let _ = box_dir;
Ok(None)
}
}
pub fn stage_box_terminal_rootfs_metadata(box_dir: &Path) -> a3s_box_core::error::Result<()> {
if guest_native_ext4_generation_exists(box_dir)? {
return Ok(());
}
let attached = attach_persistent_rootfs(box_dir)?;
let mut roots = Vec::<PathBuf>::new();
if let Some(rootfs) = attached.as_ref() {
roots.push(rootfs.path().to_path_buf());
}
roots.extend([
box_dir.join("rootfs"),
box_dir.join("upper"),
box_dir.join("merged"),
]);
roots.sort();
roots.dedup();
let mut existing_roots = Vec::new();
for root in roots {
match std::fs::symlink_metadata(&root) {
Ok(_) => existing_roots.push(root),
Err(error) if error.kind() == std::io::ErrorKind::NotFound => {}
Err(error) => return Err(error.into()),
}
}
stage_metadata_roots(&existing_roots)?;
Ok(())
}
fn stage_metadata_roots(roots: &[PathBuf]) -> std::io::Result<()> {
for root in roots {
a3s_box_core::rootfs_metadata::stage_terminal_rootfs_metadata_for_boot(root)?;
}
Ok(())
}
pub fn unmount_box_overlay(merged: &Path) {
for _ in 0..8 {
if !is_mountpoint(merged) {
break;
}
if overlay::overlay_unmount(merged).is_err() {
break;
}
}
}
pub(crate) fn unmount_box_overlay_for_reuse(merged: &Path) -> a3s_box_core::error::Result<()> {
for _ in 0..8 {
if !is_mountpoint(merged) {
return Ok(());
}
overlay::overlay_unmount_for_reuse(merged)?;
}
if is_mountpoint(merged) {
return Err(a3s_box_core::error::BoxError::BuildError(format!(
"Overlay at {} remained mounted after synchronous cleanup",
merged.display()
)));
}
Ok(())
}
#[cfg(unix)]
pub(crate) fn is_mountpoint(path: &Path) -> bool {
use std::os::unix::fs::MetadataExt;
match (std::fs::metadata(path), std::fs::metadata(path.join(".."))) {
(Ok(here), Ok(parent)) => here.dev() != parent.dev(),
_ => false,
}
}
#[cfg(not(unix))]
pub(crate) fn is_mountpoint(_path: &Path) -> bool {
false
}
pub fn unmount_box_rootfs(rootfs: &Path) {
#[cfg(target_os = "macos")]
{
let mountpoint = if rootfs.file_name().is_some_and(|name| name == ".a3s-rootfs") {
rootfs.parent().unwrap_or(rootfs)
} else {
rootfs
};
if !is_mountpoint(mountpoint) {
return;
}
match std::process::Command::new("hdiutil")
.arg("detach")
.arg("-quiet")
.arg(mountpoint)
.status()
{
Ok(status) if status.success() => {}
Ok(status) => tracing::warn!(
path = %mountpoint.display(),
?status,
"Failed to detach case-sensitive rootfs image"
),
Err(error) => tracing::warn!(
path = %mountpoint.display(),
%error,
"Failed to run hdiutil detach"
),
}
}
#[cfg(not(target_os = "macos"))]
let _ = rootfs;
}
#[cfg(target_os = "macos")]
pub(crate) fn unmount_box_rootfs_for_handoff(rootfs: &Path) -> a3s_box_core::error::Result<()> {
let mountpoint = if rootfs.file_name().is_some_and(|name| name == ".a3s-rootfs") {
rootfs.parent().unwrap_or(rootfs)
} else {
rootfs
};
if !is_mountpoint(mountpoint) {
return Err(a3s_box_core::error::BoxError::BuildError(format!(
"Expected a mounted rootfs staging filesystem at {}",
mountpoint.display()
)));
}
let status = std::process::Command::new("hdiutil")
.arg("detach")
.arg("-quiet")
.arg(mountpoint)
.status()
.map_err(|error| {
a3s_box_core::error::BoxError::BuildError(format!(
"Failed to run hdiutil detach for {}: {error}",
mountpoint.display()
))
})?;
if !status.success() || is_mountpoint(mountpoint) {
return Err(a3s_box_core::error::BoxError::BuildError(format!(
"Rootfs staging filesystem remained attached at {} after handoff",
mountpoint.display()
)));
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn persisted_exit_code_supports_each_rootfs_provider_layout() {
for (relative, expected) in [
("upper/.a3s_exit_code", 17),
("rootfs/.a3s_exit_code", 23),
("rootfs/.a3s-rootfs/.a3s_exit_code", 29),
] {
let temp = tempfile::tempdir().unwrap();
let path = temp.path().join(relative);
std::fs::create_dir_all(path.parent().unwrap()).unwrap();
std::fs::write(path, format!("{expected}\n")).unwrap();
assert_eq!(read_persisted_exit_code(temp.path()), Some(expected));
}
}
#[test]
fn persisted_exit_code_ignores_missing_or_invalid_files() {
let temp = tempfile::tempdir().unwrap();
assert_eq!(read_persisted_exit_code(temp.path()), None);
let path = temp.path().join("rootfs/.a3s_exit_code");
std::fs::create_dir_all(path.parent().unwrap()).unwrap();
std::fs::write(path, "not-an-exit-code").unwrap();
assert_eq!(read_persisted_exit_code(temp.path()), None);
}
#[test]
fn terminal_status_is_preferred_over_legacy_rootfs_marker() {
let temp = tempfile::tempdir().unwrap();
let terminal = temp
.path()
.join("runtime-control")
.join(GUEST_TERMINAL_STATUS_FILE_NAME);
std::fs::create_dir_all(terminal.parent().unwrap()).unwrap();
std::fs::write(
&terminal,
serde_json::to_vec(&GuestTerminalStatus::new(31)).unwrap(),
)
.unwrap();
let legacy = temp.path().join("rootfs/.a3s_exit_code");
std::fs::create_dir_all(legacy.parent().unwrap()).unwrap();
std::fs::write(legacy, "7").unwrap();
assert_eq!(read_persisted_exit_code(temp.path()), Some(31));
}
#[test]
fn rootfs_handoff_requires_an_explicit_guest_quiescence_ack() {
let temp = tempfile::tempdir().unwrap();
let terminal = temp
.path()
.join("runtime-control")
.join(GUEST_TERMINAL_STATUS_FILE_NAME);
std::fs::create_dir_all(terminal.parent().unwrap()).unwrap();
std::fs::write(
&terminal,
serde_json::to_vec(&GuestTerminalStatus::new(0)).unwrap(),
)
.unwrap();
assert!(!guest_rootfs_handoff_complete(temp.path()));
std::fs::write(
&terminal,
serde_json::to_vec(&GuestTerminalStatus::new(0).with_rootfs_quiesced()).unwrap(),
)
.unwrap();
assert!(guest_rootfs_handoff_complete(temp.path()));
}
#[test]
fn pending_terminal_status_blocks_stale_rootfs_fallback() {
let temp = tempfile::tempdir().unwrap();
let terminal = temp
.path()
.join("runtime-control")
.join(GUEST_TERMINAL_STATUS_FILE_NAME);
std::fs::create_dir_all(terminal.parent().unwrap()).unwrap();
std::fs::write(terminal, []).unwrap();
let legacy = temp.path().join("rootfs/.a3s_exit_code");
std::fs::create_dir_all(legacy.parent().unwrap()).unwrap();
std::fs::write(legacy, "0").unwrap();
assert_eq!(read_persisted_exit_code(temp.path()), None);
assert_eq!(resolve_workload_exit_code(temp.path(), Some(0)), None);
assert_eq!(resolve_workload_exit_code(temp.path(), Some(9)), Some(9));
}
#[test]
fn missing_path_is_not_mountpoint() {
let temp = tempfile::tempdir().unwrap();
let missing = temp.path().join("missing");
assert!(!is_mountpoint(&missing));
}
#[test]
fn unmount_overlay_noops_for_non_mountpoint() {
let temp = tempfile::tempdir().unwrap();
let merged = temp.path().join("merged");
std::fs::create_dir(&merged).unwrap();
unmount_box_overlay(&merged);
assert!(merged.exists());
}
#[test]
fn staging_is_idempotent_until_guest_replay_succeeds() {
let root = tempfile::tempdir().unwrap();
let terminal = root
.path()
.join(a3s_box_core::rootfs_metadata::ROOTFS_METADATA_PATH.trim_start_matches('/'));
let previous = root.path().join(
a3s_box_core::rootfs_metadata::PREVIOUS_ROOTFS_METADATA_PATH.trim_start_matches('/'),
);
std::fs::write(&terminal, b"clean generation").unwrap();
stage_metadata_roots(&[root.path().to_path_buf()]).unwrap();
stage_metadata_roots(&[root.path().to_path_buf()]).unwrap();
assert!(!terminal.exists());
assert_eq!(std::fs::read(previous).unwrap(), b"clean generation");
}
#[test]
fn staging_one_candidate_never_discards_an_alias_replay() {
let directory = tempfile::tempdir().unwrap();
let merged = directory.path().join("merged");
let upper = directory.path().join("upper");
std::fs::create_dir_all(&merged).unwrap();
std::fs::create_dir_all(&upper).unwrap();
let terminal_name =
a3s_box_core::rootfs_metadata::ROOTFS_METADATA_PATH.trim_start_matches('/');
let previous_name =
a3s_box_core::rootfs_metadata::PREVIOUS_ROOTFS_METADATA_PATH.trim_start_matches('/');
std::fs::write(merged.join(terminal_name), b"clean generation").unwrap();
std::fs::write(upper.join(previous_name), b"clean generation").unwrap();
stage_metadata_roots(&[merged.clone(), upper.clone()]).unwrap();
assert!(merged.join(previous_name).is_file());
assert!(upper.join(previous_name).is_file());
}
#[test]
fn staging_box_roots_clears_every_previous_exit_status() {
let directory = tempfile::tempdir().unwrap();
let box_dir = directory.path().join("box");
for provider_root in ["rootfs", "upper", "merged"] {
let root = box_dir.join(provider_root);
std::fs::create_dir_all(&root).unwrap();
std::fs::write(
root.join(a3s_box_core::rootfs_metadata::EXIT_CODE_PATH.trim_start_matches('/')),
b"17\n",
)
.unwrap();
}
stage_box_terminal_rootfs_metadata(&box_dir).unwrap();
assert_eq!(read_persisted_exit_code(&box_dir), None);
for provider_root in ["rootfs", "upper", "merged"] {
assert!(!box_dir
.join(provider_root)
.join(a3s_box_core::rootfs_metadata::EXIT_CODE_PATH.trim_start_matches('/'))
.exists());
}
}
#[test]
fn raw_generation_keeps_terminal_fencing_inside_guest() {
let directory = tempfile::tempdir().unwrap();
let box_dir = directory.path().join("box");
let artifact = box_dir.join("rootfs-ext4-v1");
let rootfs = box_dir.join("rootfs");
std::fs::create_dir_all(&artifact).unwrap();
std::fs::create_dir_all(&rootfs).unwrap();
let terminal = rootfs
.join(a3s_box_core::rootfs_metadata::ROOTFS_METADATA_PATH.trim_start_matches('/'));
std::fs::write(&terminal, b"guest-owned").unwrap();
assert!(guest_native_ext4_generation_exists(&box_dir).unwrap());
stage_box_terminal_rootfs_metadata(&box_dir).unwrap();
assert_eq!(std::fs::read(&terminal).unwrap(), b"guest-owned");
assert!(attach_persistent_rootfs(&box_dir).is_err());
}
}