use crate::blockmap::{BlockMap, packed_len};
use crate::dc42::Dc42Image;
use crate::error::{MfsError, Result};
use crate::mdb::{MDB_LEN, MFS_SIGNATURE, Mdb};
use crate::timestamp::MacTimestamp;
use crate::volume::{Container, ImageFormat, MfsVolume};
const SECTOR: u32 = 512;
const MIN_SIZE: u32 = 100 * 1024;
const MAX_BLOCKS: u32 = 0xFEF - 1;
const RESERVED_TAIL_SECTORS: u32 = 2;
const TARGET_BLOCKS: u32 = 400;
const DIR_SECTORS_PER_400K: u32 = 12;
const CLUMP_BLOCKS: u32 = 8;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
struct Geometry {
dir_st: u16,
dir_len: u16,
al_bl_st: u16,
n_blocks: u16,
al_blk_siz: u32,
}
fn map_bytes(n_blocks: u32) -> u32 {
(n_blocks * 3).div_ceil(2)
}
fn geometry(size_bytes: u32) -> Result<Geometry> {
if !size_bytes.is_multiple_of(SECTOR) {
return Err(MfsError::InvalidGeometry(format!(
"{size_bytes} bytes is not a multiple of the {SECTOR}-byte sector size"
)));
}
if size_bytes < MIN_SIZE {
return Err(MfsError::InvalidGeometry(format!(
"{size_bytes} bytes is below the {MIN_SIZE}-byte minimum volume size"
)));
}
let sectors = size_bytes / SECTOR;
let usable = sectors - RESERVED_TAIL_SECTORS;
let per_block = (sectors / TARGET_BLOCKS).next_power_of_two().max(2);
let al_blk_siz = per_block * SECTOR;
let dir_len = (sectors * DIR_SECTORS_PER_400K / 800).max(1);
let mut n_blocks = usable / per_block;
let mut dir_st;
let mut al_bl_st;
loop {
let mdb_sectors = (MDB_LEN as u32 + map_bytes(n_blocks)).div_ceil(SECTOR);
dir_st = 2 + mdb_sectors;
al_bl_st = dir_st + dir_len;
if al_bl_st + per_block > usable {
return Err(MfsError::InvalidGeometry(format!(
"{size_bytes} bytes leaves no room for allocation blocks after the \
boot blocks, the {mdb_sectors}-sector MDB area and a \
{dir_len}-sector directory"
)));
}
let next = (usable - al_bl_st) / per_block;
if next >= n_blocks {
n_blocks = n_blocks.min(next);
break;
}
n_blocks = next;
}
let slack = usable - (al_bl_st + n_blocks * per_block);
let dir_len = dir_len + slack;
let al_bl_st = al_bl_st + slack;
if n_blocks == 0 || n_blocks > MAX_BLOCKS {
return Err(MfsError::InvalidGeometry(format!(
"{size_bytes} bytes needs {n_blocks} allocation blocks of {al_blk_siz} \
bytes; MFS supports 1..={MAX_BLOCKS}"
)));
}
debug_assert!(al_bl_st + n_blocks * per_block <= sectors);
Ok(Geometry {
dir_st: dir_st as u16,
dir_len: dir_len as u16,
al_bl_st: al_bl_st as u16,
n_blocks: n_blocks as u16,
al_blk_siz,
})
}
pub(crate) fn format(size_bytes: u32, name: &str, format: ImageFormat) -> Result<MfsVolume> {
let raw_name = crate::volume::encode_volume_name(name)?;
if format == ImageFormat::DiskCopy42
&& size_bytes != MfsVolume::FLOPPY_400K
&& size_bytes != MfsVolume::FLOPPY_800K
{
return Err(MfsError::InvalidGeometry(format!(
"DiskCopy 4.2 only defines the standard floppy geometries: {size_bytes} \
bytes is neither {} nor {}",
MfsVolume::FLOPPY_400K,
MfsVolume::FLOPPY_800K
)));
}
let g = geometry(size_bytes)?;
let mut name_raw = [0u8; 28];
name_raw[0] = raw_name.len() as u8;
name_raw[1..1 + raw_name.len()].copy_from_slice(&raw_name);
let now = MacTimestamp::now().0;
let mdb = Mdb {
sig_word: MFS_SIGNATURE,
cr_date: now,
ls_mod: now,
atrb: 0,
nm_fls: 0,
dir_st: g.dir_st,
dir_len: g.dir_len,
nm_al_blks: g.n_blocks,
al_blk_siz: g.al_blk_siz,
clp_siz: g.al_blk_siz * CLUMP_BLOCKS,
al_bl_st: g.al_bl_st,
nxt_fnum: 1,
free_bks: g.n_blocks,
name_raw,
};
let mut data = vec![0u8; size_bytes as usize];
mdb.write_to(&mut data[2 * SECTOR as usize..]);
let map_start = 2 * SECTOR as usize + MDB_LEN;
let map = BlockMap::new_empty(g.n_blocks);
map.pack(&mut data[map_start..map_start + packed_len(g.n_blocks)]);
match format {
ImageFormat::Raw => MfsVolume::from_parts(data, Container::Raw),
ImageFormat::DiskCopy42 => {
let img = Dc42Image::new_blank(&raw_name, data)?;
MfsVolume::from_parts(
img.data,
Container::Dc42 {
name: img.name,
tags: img.tags,
disk_format: img.disk_format,
format_byte: img.format_byte,
},
)
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn apple_400k_geometry() {
let g = geometry(MfsVolume::FLOPPY_400K).unwrap();
assert_eq!(
g,
Geometry { dir_st: 4, dir_len: 12, al_bl_st: 16, n_blocks: 391, al_blk_siz: 1024 }
);
}
#[test]
fn apple_800k_geometry() {
let g = geometry(MfsVolume::FLOPPY_800K).unwrap();
assert_eq!(
g,
Geometry { dir_st: 4, dir_len: 26, al_bl_st: 30, n_blocks: 392, al_blk_siz: 2048 }
);
}
#[test]
fn geometry_is_self_consistent_across_sizes() {
for sectors in [200u32, 201, 400, 800, 801, 1600, 2880, 4000, 20_000] {
let size = sectors * SECTOR;
if size < MIN_SIZE {
continue;
}
let g = geometry(size).unwrap();
let mdb_sectors = g.dir_st as u32 - 2;
assert!(
MDB_LEN as u32 + map_bytes(g.n_blocks as u32) <= mdb_sectors * SECTOR,
"map does not fit for {size} bytes"
);
assert_eq!(g.al_bl_st as u32, g.dir_st as u32 + g.dir_len as u32);
let end = g.al_bl_st as u32 + g.n_blocks as u32 * (g.al_blk_siz / SECTOR);
assert!(end <= sectors, "allocation area overruns the disk for {size} bytes");
assert_eq!(sectors - end, RESERVED_TAIL_SECTORS, "slack not absorbed for {size}");
assert!(g.n_blocks as u32 <= MAX_BLOCKS);
}
}
#[test]
fn rejects_bad_sizes() {
assert!(matches!(geometry(409_601), Err(MfsError::InvalidGeometry(_))));
assert!(matches!(geometry(0), Err(MfsError::InvalidGeometry(_))));
assert!(matches!(geometry(50 * 1024), Err(MfsError::InvalidGeometry(_))));
}
#[test]
fn dc42_rejects_non_floppy_sizes() {
let err = format(200 * 1024, "Odd", ImageFormat::DiskCopy42).unwrap_err();
assert!(matches!(err, MfsError::InvalidGeometry(_)), "{err}");
assert!(format(200 * 1024, "Odd", ImageFormat::Raw).is_ok());
}
#[test]
fn formatted_volume_is_empty_and_consistent() {
let vol = format(MfsVolume::FLOPPY_400K, "Untitled", ImageFormat::Raw).unwrap();
let info = vol.info();
assert_eq!(info.name, "Untitled");
assert_eq!(info.file_count, 0);
assert_eq!(info.total_blocks, 391);
assert_eq!(info.free_blocks, 391);
assert_eq!(info.alloc_block_size, 1024);
assert_eq!(vol.files().count(), 0);
assert!(vol.check().is_empty(), "{:?}", vol.check());
}
#[test]
fn rejects_bad_volume_names() {
for bad in ["", "Has:Colon", "twenty-eight characters long"] {
assert!(
matches!(
format(MfsVolume::FLOPPY_400K, bad, ImageFormat::Raw),
Err(MfsError::InvalidName(_))
),
"{bad:?} should be rejected"
);
}
assert!(format(MfsVolume::FLOPPY_400K, "twenty-seven characters ok!", ImageFormat::Raw).is_ok());
}
}