use std::io::{Cursor, Read, Seek};
use crate::hfs::{create_blank_hfs_sized, HfsFilesystem};
use crate::hfs_clone::{clone_hfs_volume, CloneReport};
use disk_core::filesystem::{Filesystem, FilesystemError};
pub const HFV_MAX_BYTES: u64 = 2047 * 1024 * 1024;
pub const BLOCK_SIZE_CHOICES: &[u32] = &[4096, 8192, 16384, 32768, 65536];
pub fn max_volume_for_block_size(bs: u32) -> u64 {
65535u64 * bs as u64
}
pub fn suggest_block_size(target_bytes: u64) -> u32 {
for &bs in BLOCK_SIZE_CHOICES {
if max_volume_for_block_size(bs) >= target_bytes {
return bs;
}
}
*BLOCK_SIZE_CHOICES.last().unwrap()
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum HfvVolumeKind {
Hfs,
HfsPlus,
}
impl HfvVolumeKind {
pub fn from_fs_type(fs_type: &str) -> Self {
if fs_type.eq_ignore_ascii_case("HFS") {
HfvVolumeKind::Hfs
} else {
HfvVolumeKind::HfsPlus
}
}
}
pub fn validate_hfv_target(size_bytes: u64, kind: HfvVolumeKind) -> Result<(), FilesystemError> {
if kind != HfvVolumeKind::Hfs {
return Err(FilesystemError::InvalidData(
"HFV volumes must be classic HFS, not HFS+/HFSX (a flat HFS+ image \
needs Mac OS 8.1+ and is out of scope)"
.to_string(),
));
}
if size_bytes > HFV_MAX_BYTES {
return Err(FilesystemError::InvalidData(format!(
"HFV volume size {} bytes exceeds the classic-HFS limit of {} bytes (2047 MB)",
size_bytes, HFV_MAX_BYTES
)));
}
Ok(())
}
pub fn build_blank_hfv(
size_bytes: u64,
block_size: u32,
volume_name: &str,
) -> Result<Vec<u8>, FilesystemError> {
validate_hfv_target(size_bytes, HfvVolumeKind::Hfs)?;
create_blank_hfs_sized(size_bytes, block_size, volume_name, 0, 0)
}
pub fn clone_into_hfv<RS>(
source: &mut HfsFilesystem<RS>,
target_size: u64,
block_size: u32,
volume_name: &str,
) -> Result<(Vec<u8>, CloneReport), FilesystemError>
where
RS: Read + Seek + Send,
{
if source.fs_type() != "HFS" {
return Err(FilesystemError::InvalidData(format!(
"source is {}, not classic HFS; HFV output requires an HFS source",
source.fs_type()
)));
}
validate_hfv_target(target_size, HfvVolumeKind::Hfs)?;
let summary = source.volume_summary();
let (catalog_min, extents_min) = crate::hfs::clone_target_btree_sizes(
target_size,
block_size,
summary.catalog_file_size,
summary.extents_file_size,
);
let mut target_buf = create_blank_hfs_sized(
target_size,
block_size,
volume_name,
extents_min,
catalog_min,
)?;
let report = {
let target_cursor = Cursor::new(&mut target_buf);
let mut target_fs = HfsFilesystem::open(target_cursor, 0)?;
clone_hfs_volume(source, &mut target_fs)?
};
Ok((target_buf, report))
}
#[cfg(test)]
mod tests {
use super::*;
use crate::hfs::create_blank_hfs;
const MIB: u64 = 1024 * 1024;
#[test]
fn validate_rejects_hfs_plus() {
let err = validate_hfv_target(10 * MIB, HfvVolumeKind::HfsPlus).unwrap_err();
assert!(err.to_string().contains("classic HFS"));
}
#[test]
fn validate_rejects_oversize() {
assert!(validate_hfv_target(HFV_MAX_BYTES, HfvVolumeKind::Hfs).is_ok());
let err = validate_hfv_target(HFV_MAX_BYTES + 1, HfvVolumeKind::Hfs).unwrap_err();
assert!(err.to_string().contains("2047 MB"));
}
#[test]
fn suggest_block_size_covers_full_range() {
assert_eq!(suggest_block_size(10 * MIB), 4096);
let bs = suggest_block_size(HFV_MAX_BYTES);
assert!(max_volume_for_block_size(bs) >= HFV_MAX_BYTES);
assert_eq!(bs, 32768);
}
#[test]
fn build_blank_hfv_detects_and_fscks() {
let bs = suggest_block_size(16 * MIB);
let img = build_blank_hfv(16 * MIB, bs, "BlankHFV").unwrap();
let mut fs = HfsFilesystem::open(Cursor::new(img), 0).unwrap();
assert_eq!(fs.fs_type(), "HFS");
let summary = fs.volume_summary();
assert_eq!(summary.volume_name, "BlankHFV");
assert!(summary.total_blocks > 0); let result = fs.fsck().unwrap();
assert!(result.errors.is_empty());
}
#[test]
fn build_blank_hfv_refuses_hfs_plus_and_oversize() {
assert!(build_blank_hfv(HFV_MAX_BYTES + MIB, 32768, "X").is_err());
}
#[test]
fn clone_into_hfv_round_trips_a_file() {
use disk_core::filesystem::{CreateFileOptions, EditableFilesystem};
let mut src_img = create_blank_hfs(8 * MIB, 4096, "Source").unwrap();
{
let mut efs = HfsFilesystem::open(Cursor::new(&mut src_img), 0).unwrap();
let root = efs.root().unwrap();
let payload = b"clone me into an HFV";
let mut data = Cursor::new(payload.as_slice());
efs.create_file(
&root,
"doc.txt",
&mut data,
payload.len() as u64,
&CreateFileOptions::default(),
)
.unwrap();
efs.sync_metadata().unwrap();
}
let mut source_fs = HfsFilesystem::open(Cursor::new(src_img), 0).unwrap();
let (hfv, report) = clone_into_hfv(&mut source_fs, 16 * MIB, 4096, "Cloned").unwrap();
assert_eq!(report.files_copied, 1);
let mut out = HfsFilesystem::open(Cursor::new(hfv), 0).unwrap();
assert_eq!(out.volume_summary().volume_name, "Cloned");
let root = out.root().unwrap();
let kids = out.list_directory(&root).unwrap();
assert!(kids.iter().any(|e| e.name == "doc.txt"));
assert!(out.fsck().unwrap().errors.is_empty());
}
}