mod common;
use std::io::Cursor;
use macfs::{Fork, ImageFormat, MfsVolume};
const GOLDEN: [&str; 6] = [
"Sample.img",
"Finder 1.0.image",
"1.1 System Disk.image",
"2.0 System Disk.image",
"gryphel-mfs400k.image",
"gryphel-mfs800k.image",
];
fn listing(vol: &MfsVolume) -> Vec<(String, u32, u32)> {
let mut files: Vec<_> = vol
.files()
.map(|f| (f.name.clone(), f.data_len, f.rsrc_len))
.collect();
files.sort_by(|a, b| a.0.cmp(&b.0));
files
}
fn save_bytes(vol: &mut MfsVolume) -> Vec<u8> {
let mut out = Cursor::new(Vec::new());
vol.save_to(&mut out).expect("save");
out.into_inner()
}
#[test]
fn sample_img_contents() {
let Some(p) = common::image_path("Sample.img") else { return };
let vol = MfsVolume::open_path(&p).expect("open Sample.img");
let info = vol.info();
assert_eq!(info.format, ImageFormat::DiskCopy42);
assert_eq!(info.name, "Sample");
assert_eq!(info.file_count, 10);
assert_eq!(info.alloc_block_size, 1024);
assert_eq!(info.total_blocks, 391);
assert_eq!(info.free_blocks, 207);
assert!(vol.check().is_empty(), "{:?}", vol.check());
assert_eq!(
listing(&vol),
vec![
("CSillyBalls".to_string(), 0, 2417),
("CSillyBalls.make".to_string(), 1720, 382),
("DeskTop".to_string(), 0, 2540),
("PSillyBalls".to_string(), 0, 2479),
("PSillyBalls.make".to_string(), 1369, 382),
("SCN.003.SillyBalls".to_string(), 1294, 382),
("SillyBalls.c".to_string(), 8947, 382),
("SillyBalls.p".to_string(), 8573, 382),
("TN.002.Compatibility".to_string(), 13130, 833),
("TN.002.Compatibility.pdf".to_string(), 133827, 0),
]
);
let pdf = vol
.read_fork("TN.002.Compatibility.pdf", Fork::Data)
.expect("read the PDF");
assert_eq!(pdf.len(), 133_827);
assert_eq!(common::crc32(&pdf), 0xFB5B_B4D7);
assert_eq!(&pdf[..5], b"%PDF-", "the extracted bytes really are a PDF");
let app = vol.read_fork("CSillyBalls", Fork::Resource).unwrap();
assert_eq!(app.len(), 2417);
assert_eq!(common::crc32(&app), 0x0862_1B3D);
assert!(vol.read_fork("CSillyBalls", Fork::Data).unwrap().is_empty());
assert_eq!(&vol.file("csillyballs").unwrap().type_code, b"APPL");
assert_eq!(&vol.file("SillyBalls.c").unwrap().creator, b"MPS ");
}
#[test]
fn system_disks_bootable_and_readable() {
let disks: [(&str, &str, u16, u16); 3] = [
("1.1 System Disk.image", "System Disk", 10, 105),
("2.0 System Disk.image", "System Disk", 10, 68),
("Finder 1.0.image", "Write/Paint", 11, 48),
];
for (name, vol_name, file_count, free) in disks {
let Some(p) = common::image_path(name) else { continue };
let vol = MfsVolume::open_path(&p).unwrap_or_else(|e| panic!("{name}: {e}"));
let info = vol.info();
assert_eq!(info.format, ImageFormat::DiskCopy42, "{name}");
assert_eq!(info.name, vol_name, "{name}");
assert_eq!(info.file_count, file_count, "{name}");
assert_eq!(info.alloc_block_size, 1024, "{name}");
assert_eq!(info.total_blocks, 391, "{name}");
assert_eq!(info.free_blocks, free, "{name}");
assert_eq!(&vol.boot_blocks()[..2], b"LK", "{name} should be bootable");
assert_eq!(vol.boot_blocks().len(), 1024, "{name}");
for wanted in ["System", "Finder"] {
let f = vol
.file(&wanted.to_lowercase())
.unwrap_or_else(|e| panic!("{name}: {wanted}: {e}"));
assert_eq!(f.name, wanted, "{name}");
assert_eq!(&f.type_code, if wanted == "System" { b"ZSYS" } else { b"FNDR" });
assert!(f.rsrc_len > 40_000, "{name}: {wanted} resource fork is {}", f.rsrc_len);
}
for f in vol.files() {
for (fork, want) in [(Fork::Data, f.data_len), (Fork::Resource, f.rsrc_len)] {
let got = vol
.read_fork(&f.name, fork)
.unwrap_or_else(|e| panic!("{name}: {} {fork:?} fork: {e}", f.name));
assert_eq!(got.len() as u32, want, "{name}: {} {fork:?} fork", f.name);
}
}
assert!(vol.check().is_empty(), "{name}: {:?}", vol.check());
}
}
#[test]
fn blanks_match_mkfs() {
let blanks: [(&str, u32, &str, u32, u32, u16, u16); 2] = [
("gryphel-mfs400k.image", MfsVolume::FLOPPY_400K, "Desktop", 461, 1024, 391, 390),
("gryphel-mfs800k.image", MfsVolume::FLOPPY_800K, "DeskTop", 512, 2048, 392, 391),
];
for (name, size, desktop, desktop_rsrc, blk_siz, total, free) in blanks {
let Some(p) = common::image_path(name) else { continue };
let vol = MfsVolume::open_path(&p).unwrap_or_else(|e| panic!("{name}: {e}"));
let info = vol.info();
assert_eq!(info.name, "Untitled", "{name}");
assert_eq!(info.file_count, 1, "{name}");
assert_eq!(
listing(&vol),
vec![(desktop.to_string(), 0, desktop_rsrc)],
"{name}"
);
assert_eq!(&vol.file(desktop).unwrap().type_code, b"FNDR", "{name}");
assert!(vol.boot_blocks().iter().all(|&b| b == 0), "{name}");
assert!(vol.check().is_empty(), "{name}: {:?}", vol.check());
let ours = MfsVolume::format(size, "Untitled", ImageFormat::Raw).unwrap().info();
assert_eq!(ours.alloc_block_size, info.alloc_block_size, "{name}: drAlBlkSiz");
assert_eq!(ours.total_blocks, info.total_blocks, "{name}: drNmAlBlks");
assert_eq!(ours.name, info.name, "{name}: drVN");
assert_eq!(ours.alloc_block_size, blk_siz, "{name}");
assert_eq!(ours.total_blocks, total, "{name}");
assert_eq!(info.free_blocks, free, "{name}");
assert_eq!(ours.free_blocks, total, "{name}");
}
}
#[test]
fn byte_identical_all() {
let mut checked = 0;
for name in GOLDEN {
let Some(p) = common::image_path(name) else { continue };
let original = std::fs::read(&p).unwrap();
let mut vol = MfsVolume::open(Cursor::new(original.clone()))
.unwrap_or_else(|e| panic!("{name}: {e}"));
let saved = save_bytes(&mut vol);
assert_eq!(saved.len(), original.len(), "{name}: image length changed");
if saved != original {
let at = saved.iter().zip(&original).position(|(a, b)| a != b).unwrap();
panic!(
"{name}: first difference at byte {at}: saved {:#04x}, original {:#04x}",
saved[at], original[at]
);
}
assert_eq!(save_bytes(&mut vol), original, "{name}: second save differs");
checked += 1;
}
eprintln!("byte-identical open/save over {checked} golden image(s)");
}
#[test]
fn dc42_tag_preservation() {
let Some(p) = common::image_path("Sample.img") else { return };
let original = std::fs::read(&p).unwrap();
assert_eq!(original.len(), 419_284);
assert_eq!(original.len(), 84 + 409_600 + 800 * 12);
let mut vol = MfsVolume::open(Cursor::new(original.clone())).unwrap();
let saved = save_bytes(&mut vol);
assert_eq!(saved.len(), 419_284, "the tag region was dropped or resized");
assert_eq!(saved[84 + 409_600..], original[84 + 409_600..], "tags changed");
assert_eq!(saved, original);
}
#[test]
fn write_safety_on_real_image() {
let Some(p) = common::image_path("2.0 System Disk.image") else { return };
let original = std::fs::read(&p).unwrap();
let mut vol = MfsVolume::open(Cursor::new(original.clone())).unwrap();
let before: Vec<(String, u32, u32, u32)> = vol
.files()
.map(|f| {
let d = common::crc32(&vol.read_fork(&f.name, Fork::Data).unwrap());
let r = common::crc32(&vol.read_fork(&f.name, Fork::Resource).unwrap());
(f.name.clone(), f.file_num, d, r)
})
.collect();
let boot = vol.boot_blocks().to_vec();
let free_before = vol.info().free_blocks;
let count_before = vol.info().file_count;
vol.rename_file("Note Pad File", "Note Pad File (renamed by macfs)").unwrap();
let noise = common::random_bytes(0x5A5A_1984, 10 * 1024);
vol.create_file("Scratch", *b"TEXT", *b"macf").unwrap();
vol.write_fork("Scratch", Fork::Data, &noise).unwrap();
assert_eq!(vol.info().free_blocks, free_before - 10);
assert_eq!(vol.read_fork("scratch", Fork::Data).unwrap(), noise);
vol.delete_file("Scratch").unwrap();
assert_eq!(vol.info().free_blocks, free_before);
let back = MfsVolume::open(Cursor::new(save_bytes(&mut vol))).unwrap();
assert!(back.check().is_empty(), "{:?}", back.check());
assert_eq!(back.info().file_count, count_before);
assert_eq!(back.info().free_blocks, free_before);
assert_eq!(back.info().name, "System Disk");
assert_eq!(back.boot_blocks(), &boot[..]);
assert_eq!(&back.boot_blocks()[..2], b"LK");
for (name, fnum, data_crc, rsrc_crc) in &before {
let now_name = if name == "Note Pad File" {
"Note Pad File (renamed by macfs)"
} else {
name
};
let e = back
.file(now_name)
.unwrap_or_else(|e| panic!("{now_name} vanished: {e}"));
assert_eq!(e.file_num, *fnum, "{now_name}: file number changed");
assert_eq!(
common::crc32(&back.read_fork(now_name, Fork::Data).unwrap()),
*data_crc,
"{now_name}: data fork changed"
);
assert_eq!(
common::crc32(&back.read_fork(now_name, Fork::Resource).unwrap()),
*rsrc_crc,
"{now_name}: resource fork changed"
);
}
assert!(back.file("Note Pad File").is_err());
assert!(back.file("Scratch").is_err());
}