use std::fs::File;
use std::io::Read;
use std::path::Path;
use a3s_box_core::error::{BoxError, Result};
use super::ext4::{
validate_ext4_image, validate_ext4_image_for_resume, Ext4Artifact, Ext4ArtifactManifest,
Ext4ArtifactOptions, Ext4ResumeValidation, DISK_FILE_NAME, EXT4_ARTIFACT_SCHEMA,
EXT4_BUILDER_ID, LEGACY_EXT4_BUILDER_IDS, MANIFEST_FILE_NAME,
};
const MAX_ARTIFACT_MANIFEST_BYTES: u64 = 64 * 1024;
pub(super) fn open_ext4_artifact(directory: &Path) -> Result<Ext4Artifact> {
open_ext4_artifact_inner(directory, false).map(|(artifact, _)| artifact)
}
#[cfg(target_os = "macos")]
pub(super) fn open_ext4_artifact_for_resume(
directory: &Path,
) -> Result<(Ext4Artifact, Ext4ResumeValidation)> {
open_ext4_artifact_inner(directory, true)
}
fn open_ext4_artifact_inner(
directory: &Path,
allow_journal_recovery: bool,
) -> Result<(Ext4Artifact, Ext4ResumeValidation)> {
let directory_metadata = std::fs::symlink_metadata(directory).map_err(|error| {
BoxError::BuildError(format!(
"Failed to inspect ext4 artifact directory {}: {error}",
directory.display()
))
})?;
if !directory_metadata.is_dir() || directory_metadata.file_type().is_symlink() {
return Err(BoxError::BuildError(format!(
"ext4 artifact path is not a plain directory: {}",
directory.display()
)));
}
let manifest_path = directory.join(MANIFEST_FILE_NAME);
let manifest_metadata = std::fs::symlink_metadata(&manifest_path).map_err(|error| {
BoxError::BuildError(format!(
"Failed to inspect ext4 artifact manifest {}: {error}",
manifest_path.display()
))
})?;
if !manifest_metadata.is_file() || manifest_metadata.file_type().is_symlink() {
return Err(BoxError::BuildError(format!(
"ext4 artifact manifest is not a plain file: {}",
manifest_path.display()
)));
}
if manifest_metadata.len() > MAX_ARTIFACT_MANIFEST_BYTES {
return Err(BoxError::BuildError(format!(
"ext4 artifact manifest {} exceeds {} bytes",
manifest_path.display(),
MAX_ARTIFACT_MANIFEST_BYTES
)));
}
let mut manifest_bytes = Vec::with_capacity(manifest_metadata.len() as usize);
File::open(&manifest_path)
.and_then(|file| {
file.take(MAX_ARTIFACT_MANIFEST_BYTES + 1)
.read_to_end(&mut manifest_bytes)
})
.map_err(|error| {
BoxError::BuildError(format!(
"Failed to read ext4 artifact manifest {}: {error}",
manifest_path.display()
))
})?;
if manifest_bytes.len() as u64 > MAX_ARTIFACT_MANIFEST_BYTES {
return Err(BoxError::BuildError(format!(
"ext4 artifact manifest {} grew beyond {} bytes",
manifest_path.display(),
MAX_ARTIFACT_MANIFEST_BYTES
)));
}
let manifest: Ext4ArtifactManifest =
serde_json::from_slice(&manifest_bytes).map_err(|error| {
BoxError::BuildError(format!(
"Invalid ext4 artifact manifest {}: {error}",
manifest_path.display()
))
})?;
let fs_uuid = validate_artifact_manifest(&manifest, &manifest_path)?;
let disk = directory.join(DISK_FILE_NAME);
let disk_metadata = std::fs::symlink_metadata(&disk).map_err(|error| {
BoxError::BuildError(format!(
"Failed to inspect ext4 artifact disk {}: {error}",
disk.display()
))
})?;
if !disk_metadata.is_file() || disk_metadata.file_type().is_symlink() {
return Err(BoxError::BuildError(format!(
"ext4 artifact disk is not a plain file: {}",
disk.display()
)));
}
let validation = if allow_journal_recovery {
validate_ext4_image_for_resume(&disk, manifest.capacity_bytes, fs_uuid)?
} else {
validate_ext4_image(&disk, manifest.capacity_bytes)?;
Ext4ResumeValidation::Clean
};
Ok((
Ext4Artifact {
directory: directory.to_path_buf(),
disk,
manifest,
},
validation,
))
}
fn validate_artifact_manifest(manifest: &Ext4ArtifactManifest, path: &Path) -> Result<[u8; 16]> {
let supported_builder = manifest.builder == EXT4_BUILDER_ID
|| LEGACY_EXT4_BUILDER_IDS.contains(&manifest.builder.as_str());
if manifest.schema != EXT4_ARTIFACT_SCHEMA
|| !supported_builder
|| manifest.format != "raw-ext4"
{
return Err(BoxError::BuildError(format!(
"Unsupported ext4 artifact contract in {}",
path.display()
)));
}
Ext4ArtifactOptions {
capacity_bytes: manifest.capacity_bytes,
fs_uuid: [0; 16],
epoch: 0,
}
.validate()?;
let uuid = hex::decode(&manifest.fs_uuid).map_err(|error| {
BoxError::BuildError(format!(
"Invalid ext4 artifact UUID in {}: {error}",
path.display()
))
})?;
let uuid: [u8; 16] = uuid.try_into().map_err(|uuid: Vec<u8>| {
BoxError::BuildError(format!(
"Invalid ext4 artifact UUID length {} in {}",
uuid.len(),
path.display()
))
})?;
Ok(uuid)
}