//! Resource Manager and File Manager trap handlers.
use crate::cpu::{CpuOps, Register};
use crate::managers::resource::ResourceFork;
use crate::machine_profile::ORACLE_MACHINE_PROFILE;
use crate::memory::globals::addr;
use crate::memory::{MacMemoryBus, MemoryBus};
use crate::trap::types::read_fsspec_name;
use crate::{Error, Result};
use std::sync::OnceLock;
static TRACE_MENU_PICT: OnceLock<bool> = OnceLock::new();
static TRACE_FSSPEC: OnceLock<bool> = OnceLock::new();
static TRACE_SOUND_RESOURCE: OnceLock<bool> = OnceLock::new();
static TRACE_GETRESOURCE: OnceLock<bool> = OnceLock::new();
static TRACE_LOADSEG: OnceLock<bool> = OnceLock::new();
const CURRENT_PROCESS_PSN_HIGH: u32 = 0;
const CURRENT_PROCESS_PSN_LOW: u32 = 2;
fn trace_menu_pict_enabled() -> bool {
*TRACE_MENU_PICT.get_or_init(|| std::env::var_os("SYSTEMLESS_TRACE_MENU_PICT").is_some())
}
fn trace_fsspec_enabled() -> bool {
*TRACE_FSSPEC.get_or_init(|| std::env::var_os("SYSTEMLESS_TRACE_FSSPEC").is_some())
}
fn trace_sound_resource_enabled() -> bool {
*TRACE_SOUND_RESOURCE.get_or_init(|| std::env::var_os("SYSTEMLESS_TRACE_SOUND").is_some())
}
fn trace_getresource_enabled() -> bool {
*TRACE_GETRESOURCE.get_or_init(|| std::env::var_os("SYSTEMLESS_TRACE_GETRESOURCE").is_some())
}
fn trace_loadseg_enabled() -> bool {
*TRACE_LOADSEG.get_or_init(|| std::env::var_os("SYSTEMLESS_TRACE_LOADSEG").is_some())
}
fn format_ostype(res_type: [u8; 4]) -> String {
res_type
.into_iter()
.map(|byte| {
if byte.is_ascii_graphic() || byte == b' ' {
byte as char
} else {
'.'
}
})
.collect()
}
/// Magic at the front of POD MARS Master's compressed resource bytes.
/// `'ajcp'` = `0x616A6370`. POD's CODE 1 holds an LZ77+Huffman decoder
/// at `$5BB2` that, run on a handle whose data starts with this magic,
/// replaces the bytes in place with the decompressed form. POD's
/// wrappers (`$5CD4`/`$5D1A`/`$5D58`/`$5D98`) gate trap-loaded
/// resources on this magic before invoking `$5BB2`. Application code
/// that loads via the bare trap bypasses those wrappers — for POD
/// that means scenario `'ctab'`/`'PICT'`/`'cicn'` resources stay
/// compressed in our HLE and SetEntries / DrawPicture render with
/// garbage palettes unless Systemless injects the decompressor call.
const AJCP_MAGIC: u32 = 0x616A6370;
/// Offset within POD's CODE 1 of `Decompress(handle)`.
const AJCP_DECOMPRESS_OFFSET: u32 = 0x5BB2;
/// Offset within POD's CODE 1 of `init_decompressor()`. POD's wrappers
/// call this lazily; we replicate the call with a one-shot bootstrap
/// injection on the first `'ajcp'` resource we see.
const AJCP_INIT_OFFSET: u32 = 0x3F90;
/// Sentinel value used by the `'ajcp'` decompressor trampoline. After
/// the called function `RTS`es, the trampoline runs
/// `MOVE.W #$ACAC, D0; _Pack8`, which traps into Pack8 dispatch.
pub(crate) const AJCP_TRAMPOLINE_SENTINEL: u16 = 0xACAC;
/// A5-relative offset of POD's primary Huffman tree. Probed to detect
/// whether `$3F90` has populated the trees.
const AJCP_PRIMARY_TREE_OFFSET: i32 = 0x1928;
/// `gestaltUndefSelectorErr` (-5551). Returned by `Gestalt` /
/// `ReplaceGestalt` when the selector code is not recognised.
/// Inside Macintosh: Operating System Utilities 1994, 1-32 / 1-35.
const GESTALT_UNDEF_SELECTOR_ERR: u32 = 0xFFFF_EA51;
/// `gestaltDupSelectorErr` (-5552). Returned by `NewGestalt` when the
/// caller tries to register a selector that is already known to the
/// Gestalt Manager (built-in or previously installed).
/// Inside Macintosh: Operating System Utilities 1994, 1-34.
const GESTALT_DUP_SELECTOR_ERR: u32 = 0xFFFF_EA50;
/// Closed list of `Gestalt` selectors recognised by Systemless's built-in
/// query handler. Kept in sync with the match arms in the `(false,
/// 0xAD)` dispatch — `_NewGestalt` and `_ReplaceGestalt` consult this
/// to decide between `gestaltDupSelectorErr` and noErr / between noErr
/// and `gestaltUndefSelectorErr`. See the Gestalt arm in
/// `dispatch_resource` for the response values and IM citations.
fn is_builtin_gestalt_selector(sel: &[u8; 4]) -> bool {
matches!(
sel,
b"sysv"
| b"evnt"
| b"cput"
| b"proc"
| b"mach"
| b"qd "
| b"qdrw"
| b"ram "
| b"fpu "
| b"mmu "
| b"snd "
| b"tmgr"
| b"dplv"
| b"dply"
| b"alis"
| b"fs "
| b"fold"
| b"qtim"
| b"os "
| b"powr"
| b"appr"
| b"addr"
| b"sdev"
| b"stdf"
| b"help"
| b"vm "
)
}
impl super::TrapDispatcher {
pub(crate) const RES_NOT_FOUND: i16 = -192;
pub(crate) const RES_ATTR_ERR: i16 = -198;
pub(crate) const RES_CHANGED_ATTR: u16 = 0x0002;
pub(crate) const RES_SYS_REF_ATTR: u16 = 0x0080;
pub(crate) const RES_MAP_CHANGED_ATTR: u16 = 0x0020;
/// Minimal serialized resource fork containing an empty resource map.
fn empty_resource_fork_bytes() -> Vec<u8> {
let mut bytes = vec![0u8; 46];
let mut header = [0u8; 16];
header[0..4].copy_from_slice(&16u32.to_be_bytes());
header[4..8].copy_from_slice(&16u32.to_be_bytes());
header[8..12].copy_from_slice(&0u32.to_be_bytes());
header[12..16].copy_from_slice(&30u32.to_be_bytes());
bytes[0..16].copy_from_slice(&header);
bytes[16..32].copy_from_slice(&header);
bytes[40..42].copy_from_slice(&28u16.to_be_bytes());
bytes[42..44].copy_from_slice(&30u16.to_be_bytes());
bytes[44..46].copy_from_slice(&0xFFFFu16.to_be_bytes());
bytes
}
fn patch_loadseg_entry(
bus: &mut MacMemoryBus,
seg_num: i16,
seg_addr: u32,
header_size: u32,
entry_addr: u32,
) -> u32 {
let routine_offset = if bus.read_word(entry_addr) == 0xA9F0 {
// Think C format: [A9F0, 0000, offset, seg#]
bus.read_word(entry_addr + 4) as u32
} else {
// Standard format: [offset, 3F3C, seg#, A9F0]
bus.read_word(entry_addr) as u32
};
let code_addr = seg_addr + header_size + routine_offset;
bus.write_word(entry_addr, seg_num as u16);
bus.write_word(entry_addr + 2, 0x4EF9); // JMP.L
bus.write_long(entry_addr + 4, code_addr);
code_addr
}
fn handle_data_starts_with_ajcp(&self, bus: &MacMemoryBus, handle: u32) -> bool {
if handle == 0 {
return false;
}
let master_ptr = bus.read_long(handle);
if master_ptr == 0 {
return false;
}
bus.read_long(master_ptr) == AJCP_MAGIC
}
fn ajcp_huffman_trees_populated<C: CpuOps>(&self, bus: &MacMemoryBus, cpu: &C) -> bool {
let a5 = cpu.read_reg(Register::A5);
if a5 == 0 {
return false;
}
let probe = a5.wrapping_add(AJCP_PRIMARY_TREE_OFFSET as u32);
bus.read_long(probe) != 0
}
fn ajcp_function_addrs(&self, _bus: &MacMemoryBus) -> Option<(u32, u32)> {
let seg1_addr = *self.segment_map.get(&1)?;
// The doc-quoted offsets (`$3F90`, `$5BB2`, …) are measured
// from segment-start *including* the 4-byte CODE-segment
// header. Adding `header_size` made `$3F90` jump to garbage
// and hung POD's M68k loop in an earlier experiment;
// omitting the offset puts the call into the addresses the
// reverse-engineered POD wrapper offsets refer to.
let init_addr = seg1_addr.wrapping_add(AJCP_INIT_OFFSET);
let bb2_addr = seg1_addr.wrapping_add(AJCP_DECOMPRESS_OFFSET);
Some((init_addr, bb2_addr))
}
fn allocate_ajcp_trampoline(&mut self, bus: &mut MacMemoryBus) -> u32 {
if let Some(addr) = self.ajcp_trampoline_addr {
return addr;
}
let addr = bus.alloc(8);
bus.write_word(addr, 0x303C); // MOVE.W #imm, D0
bus.write_word(addr + 2, AJCP_TRAMPOLINE_SENTINEL);
bus.write_word(addr + 4, 0xA816); // _Pack8
self.ajcp_trampoline_addr = Some(addr);
addr
}
/// On the first `'ajcp'` resource the manager sees, invoke
/// `$3F90` (no args) so POD's Huffman trees get built. The
/// triggering handle is left compressed for this trap return;
/// if POD's wrapper path also runs for it, the wrapper's own
/// `$5BB2` will handle the decompression. After init, subsequent
/// `'ajcp'` resources get a direct `$5BB2(handle)` injection.
pub(crate) fn maybe_inject_ajcp_decompress<C: CpuOps>(
&mut self,
bus: &mut MacMemoryBus,
cpu: &mut C,
handle: u32,
) -> bool {
if !self.handle_data_starts_with_ajcp(bus, handle) {
return false;
}
if self.ajcp_call_state.is_some() {
return false;
}
if self.ajcp_decompressed_handles.contains(&handle) {
return false;
}
let Some((init_addr, bb2_addr)) = self.ajcp_function_addrs(bus) else {
return false;
};
let phase = if self.ajcp_decompressor_ready || self.ajcp_huffman_trees_populated(bus, cpu) {
self.ajcp_decompressor_ready = true;
super::dispatch::AjcpCallPhase::Decompress
} else {
super::dispatch::AjcpCallPhase::Init
};
let trampoline = self.allocate_ajcp_trampoline(bus);
let return_pc = cpu.read_reg(Register::PC);
let sp_before = cpu.read_reg(Register::A7);
let (entry_pc, expected_sp_after_rts) = match phase {
super::dispatch::AjcpCallPhase::Init => {
// `$3F90()` — no args. Push only the trampoline as
// the return PC for `$3F90 RTS`.
let new_sp = sp_before.wrapping_sub(4);
bus.write_long(new_sp, trampoline);
cpu.write_reg(Register::A7, new_sp);
// After `RTS` SP is back at sp_before.
(init_addr, sp_before)
}
super::dispatch::AjcpCallPhase::Decompress => {
// `$5BB2(handle)` — Pascal procedure, one 4-byte arg.
let new_sp = sp_before.wrapping_sub(8);
bus.write_long(new_sp, trampoline);
bus.write_long(new_sp + 4, handle);
cpu.write_reg(Register::A7, new_sp);
// After `RTS` SP at sp_before - 4 (handle still on
// stack); after `RTD #4` SP at sp_before.
(bb2_addr, sp_before.wrapping_sub(4))
}
};
cpu.write_reg(Register::PC, entry_pc);
self.ajcp_call_state = Some(super::dispatch::AjcpCallState {
return_pc,
expected_sp_after_rts,
phase,
});
if matches!(phase, super::dispatch::AjcpCallPhase::Decompress) {
self.ajcp_decompressed_handles.insert(handle);
}
eprintln!(
"[AJCP] {} handle=${:08X} → JSR ${:08X} (trampoline=${:08X}, return_pc=${:08X})",
match phase {
super::dispatch::AjcpCallPhase::Init => "init",
super::dispatch::AjcpCallPhase::Decompress => "decompress",
},
handle,
entry_pc,
trampoline,
return_pc,
);
true
}
fn should_trace_extended_resource_activity(&self) -> bool {
trace_sound_resource_enabled()
&& self
.resources
.as_ref()
.is_some_and(|resources| resources.files.len() > 1)
}
fn resource_trace_chain(&self) -> String {
self.resource_search_order()
.into_iter()
.map(|refnum| {
if let Some(name) = self.resource_file_name(refnum) {
format!("{}:{}", refnum, name)
} else {
refnum.to_string()
}
})
.collect::<Vec<_>>()
.join(" -> ")
}
pub(crate) fn get_or_create_resource_handle_in_file(
&mut self,
bus: &mut MacMemoryBus,
res_type: [u8; 4],
res_id: i16,
ptr: u32,
refnum: u16,
) -> u32 {
if let Some((&handle, _)) = self.loaded_handles.iter().find(
|(handle, (existing_ptr, existing_type, existing_id))| {
*existing_ptr == ptr
&& *existing_type == res_type
&& *existing_id == res_id
&& self.resource_handle_files.get(handle).copied() == Some(refnum)
},
) {
// IM:More Macintosh Toolbox 1993, 1-79: SetResLoad(TRUE)
// restores automatic loading for resource-returning routines.
// If this resource was first seen while SetResLoad(FALSE) was
// active, the handle exists but its master pointer is NIL; a
// later lookup with loading enabled should fill it and restore
// the reverse ptr->handle ownership so RecoverHandle keeps
// working after the refill.
if self.res_load && bus.read_long(handle) == 0 {
bus.write_long(handle, ptr);
}
if ptr != 0 {
self.ptr_to_handle.insert(ptr, handle);
}
return handle;
}
let handle = bus.alloc(4);
// IM:More Macintosh Toolbox 1993, 1-79: SetResLoad(FALSE) returns
// an empty handle (master pointer NIL) for resource data that is not
// already in memory. Keep the true data pointer in loaded_handles so
// LoadResource can populate the master pointer later.
bus.write_long(handle, if self.res_load { ptr } else { 0 });
self.loaded_handles.insert(handle, (ptr, res_type, res_id));
self.resource_handle_files.insert(handle, refnum);
handle
}
pub(crate) fn get_or_create_resource_handle(
&mut self,
bus: &mut MacMemoryBus,
res_type: [u8; 4],
res_id: i16,
ptr: u32,
) -> u32 {
let refnum = self
.resource_refnum_for_ptr(res_type, res_id, ptr)
.unwrap_or_else(|| self.current_resource_refnum());
self.get_or_create_resource_handle_in_file(bus, res_type, res_id, ptr, refnum)
}
fn restore_loaded_resource_handle(&mut self, handle: u32, ptr: u32) {
self.ptr_to_handle.insert(ptr, handle);
self.detached_handles.remove(&handle);
if let Some(refnum) = self.detached_handle_files.remove(&handle) {
// A reload should repair any stale detached-file bookkeeping so
// later Resource Manager queries still see a live resource.
self.resource_handle_files.insert(handle, refnum);
}
}
pub(crate) fn live_resource_identity_for_handle(
&self,
handle: u32,
) -> Option<([u8; 4], i16, Option<u16>)> {
let (_, res_type, res_id) = self.loaded_handles.get(&handle).copied()?;
let refnum = self.resource_handle_files.get(&handle).copied();
Some((res_type, res_id, refnum))
}
fn current_system_reference_for_handle(&self, handle: u32) -> Option<(u16, i16)> {
let (ptr, res_type, _res_id) = self.loaded_handles.get(&handle).copied()?;
let home_refnum = self.resource_handle_files.get(&handle).copied()?;
if home_refnum != 0 {
return None;
}
let current_refnum = self.current_resource_refnum();
if current_refnum == 0 {
return None;
}
let resources = self.resources.as_ref()?;
let file = resources.files.get(¤t_refnum)?;
file.loaded.iter().find_map(|((loaded_type, loaded_id), loaded_ptr)| {
if *loaded_type == res_type
&& *loaded_ptr == ptr
&& file.attrs.get(&(*loaded_type, *loaded_id)).is_some_and(|attrs| {
(*attrs & Self::RES_SYS_REF_ATTR as u8) != 0
})
{
Some((current_refnum, *loaded_id))
} else {
None
}
})
}
pub(crate) fn resource_record_for_handle(&self, handle: u32) -> Option<(u16, [u8; 4], i16)> {
let (_, res_type, res_id) = self.loaded_handles.get(&handle).copied()?;
let home_refnum = self.resource_handle_files.get(&handle).copied()?;
if let Some((current_refnum, current_id)) = self.current_system_reference_for_handle(handle) {
return Some((current_refnum, res_type, current_id));
}
Some((home_refnum, res_type, res_id))
}
pub(crate) fn resource_attributes_for_handle(&self, handle: u32) -> Option<u16> {
let (refnum, res_type, res_id) = self.resource_record_for_handle(handle)?;
let attrs = self.resources.as_ref().and_then(|resources| {
resources
.files
.get(&refnum)
.and_then(|file| file.attrs.get(&(res_type, res_id)).copied())
});
Some(attrs.unwrap_or(0) as u16)
}
pub(crate) fn remove_resource_reference(
&mut self,
bus: &mut MacMemoryBus,
handle: u32,
) -> bool {
const RMV_RES_FAILED: i16 = -196;
if let Some((refnum, res_type, res_id)) = self.resource_record_for_handle(handle) {
let current_refnum = self.current_resource_refnum();
let attrs = self.resource_attributes_for_handle(handle).unwrap_or(0);
let home_refnum = self.resource_handle_files.get(&handle).copied();
let shadowed_system_ref = home_refnum == Some(0) && self.current_system_reference_for_handle(handle).is_some();
// Must be in the current file and not protected.
if (!shadowed_system_ref && refnum != current_refnum) || (attrs & 0x0008) != 0 {
bus.write_word(0x0A60, RMV_RES_FAILED as u16);
false
} else if shadowed_system_ref {
if let Some(resources) = self.resources.as_mut() {
if let Some(file) = resources.files.get_mut(&refnum) {
file.loaded.remove(&(res_type, res_id));
file.attrs.remove(&(res_type, res_id));
file.named
.retain(|(t, _), (id, _)| !(*t == res_type && *id == res_id));
}
}
bus.write_word(0x0A60, 0);
true
} else {
if let Some(resources) = self.resources.as_mut() {
if let Some(file) = resources.files.get_mut(¤t_refnum) {
file.loaded.remove(&(res_type, res_id));
file.attrs.remove(&(res_type, res_id));
file.named
.retain(|(t, _), (id, _)| !(*t == res_type && *id == res_id));
}
}
self.loaded_handles.remove(&handle);
self.resource_handle_files.remove(&handle);
bus.write_word(0x0A60, 0);
true
}
} else {
bus.write_word(0x0A60, RMV_RES_FAILED as u16);
false
}
}
pub(crate) fn add_resource_reference(
&mut self,
bus: &mut MacMemoryBus,
handle: u32,
new_id: i16,
name_ptr: u32,
) -> bool {
const ADD_REF_FAILED: i16 = -195;
let Some((res_type, _res_id, source_refnum)) = self.live_resource_identity_for_handle(handle) else {
bus.write_word(0x0A60, ADD_REF_FAILED as u16);
return false;
};
if source_refnum != Some(0) {
bus.write_word(0x0A60, ADD_REF_FAILED as u16);
return false;
}
let current_refnum = self.current_resource_refnum();
if current_refnum == 0 {
bus.write_word(0x0A60, ADD_REF_FAILED as u16);
return false;
}
let Some(ptr) = self.loaded_handles.get(&handle).map(|(ptr, _, _)| *ptr) else {
bus.write_word(0x0A60, ADD_REF_FAILED as u16);
return false;
};
let Some(source_attrs) = self.resource_attributes_for_handle(handle) else {
bus.write_word(0x0A60, ADD_REF_FAILED as u16);
return false;
};
let Some(resources) = self.resources.as_mut() else {
bus.write_word(0x0A60, ADD_REF_FAILED as u16);
return false;
};
let Some(file) = resources.files.get_mut(¤t_refnum) else {
bus.write_word(0x0A60, ADD_REF_FAILED as u16);
return false;
};
if file.loaded.contains_key(&(res_type, new_id))
|| file.loaded.values().any(|&existing_ptr| existing_ptr == ptr)
{
bus.write_word(0x0A60, ADD_REF_FAILED as u16);
return false;
}
let attrs = (source_attrs as u8)
| Self::RES_CHANGED_ATTR as u8
| Self::RES_SYS_REF_ATTR as u8;
file.loaded.insert((res_type, new_id), ptr);
file.attrs.insert((res_type, new_id), attrs);
file.map_attrs |= Self::RES_MAP_CHANGED_ATTR;
if name_ptr != 0 {
let name_bytes = bus.read_pstring(name_ptr);
if let Ok(name) = String::from_utf8(name_bytes) {
if !name.is_empty() {
file.named.insert((res_type, name), (new_id, ptr));
}
}
}
bus.write_word(0x0A60, 0);
true
}
/// Simulate writing a resource-backed handle out to disk by
/// clearing its resChanged bit in the resource map. Returns true
/// when the handle was a writable changed resource and the flag was
/// cleared, false otherwise.
pub(crate) fn write_resource_backing_if_changed(&mut self, handle: u32) -> bool {
let Some((refnum, res_type, res_id)) = self.resource_record_for_handle(handle) else {
return false;
};
let attrs = self.resource_attributes_for_handle(handle).unwrap_or(0);
if (attrs & 0x0008) != 0 || (attrs & Self::RES_CHANGED_ATTR) == 0 {
return false;
}
if let Some(resources) = self.resources.as_mut() {
if let Some(file) = resources.files.get_mut(&refnum) {
if let Some(a) = file.attrs.get_mut(&(res_type, res_id)) {
*a &= !(Self::RES_CHANGED_ATTR as u8);
}
}
}
true
}
fn resource_metadata_for_handle(&self, handle: u32) -> Option<([u8; 4], i16, Option<String>)> {
let (refnum, res_type, res_id) = self.resource_record_for_handle(handle)?;
let name = self.resources.as_ref().and_then(|resources| {
resources.files.get(&refnum).and_then(|file| {
file.named
.iter()
.find_map(|((name_type, name), (name_id, _))| {
(*name_type == res_type && *name_id == res_id).then(|| name.clone())
})
})
});
Some((res_type, res_id, name))
}
pub(crate) fn rsrc_map_entry_for_handle(&self, handle: u32) -> Option<u32> {
let (_, res_type, res_id) = self.loaded_handles.get(&handle).copied()?;
let refnum = self.resource_handle_files.get(&handle).copied()?;
if let Some(file_name) = self.resource_file_name(refnum) {
if let Some(rsrc_bytes) = self.vfs_rsrc.get(file_name) {
if let Some(fork) = ResourceFork::parse(rsrc_bytes) {
if let Some(resource) = fork.resources().get(&(res_type, res_id)) {
return Some(resource.reference_offset as u32);
}
}
}
}
let resources = self.resources.as_ref()?;
let file = resources.files.get(&refnum)?;
let mut ordered_keys: Vec<([u8; 4], i16)> = file.loaded.keys().copied().collect();
ordered_keys.sort_unstable_by(|(type_a, id_a), (type_b, id_b)| type_a.cmp(type_b).then(id_a.cmp(id_b)));
let index = ordered_keys
.iter()
.position(|key| *key == (res_type, res_id))? as u32;
Some(38 + index * 12)
}
pub(crate) fn dispatch_resource<C: CpuOps>(
&mut self,
is_tool: bool,
trap_num: u16,
cpu: &mut C,
bus: &mut MacMemoryBus,
) -> Option<Result<()>> {
Some(match (is_tool, trap_num) {
// GetResource ($A9A0)
// Returns a handle to the resource with the given type and ID.
// FUNCTION GetResource(theType: ResType; theID: INTEGER): Handle;
// Inside Macintosh Volume I, I-119
// GetResource ($A9A0): Loads from ResourceFork, allocates in guest memory
(true, 0x1A0) => {
let sp = cpu.read_reg(Register::A7);
let res_id = bus.read_word(sp) as i16;
let raw_res_type = bus.read_long(sp + 2).to_be_bytes();
let res_type = super::TrapDispatcher::normalize_ostype(raw_res_type);
let trace_sound_resource = trace_sound_resource_enabled() && res_type == *b"snd ";
let trace_extended = self.should_trace_extended_resource_activity();
if trace_extended {
eprintln!(
"[RSRC] GetResource raw='{}' norm='{}' id={} current={} chain=[{}]",
format_ostype(raw_res_type),
format_ostype(res_type),
res_id,
self.current_resource_refnum(),
self.resource_trace_chain()
);
}
// Wide-net GetResource trace (gate: SYSTEMLESS_TRACE_GETRESOURCE=1).
// Logs tick + type + id for every GetResource call.
if trace_getresource_enabled() {
eprintln!(
"[GETRESOURCE] tick={} type='{}' id={}",
self.tick_count,
format_ostype(res_type),
res_id,
);
}
if trace_menu_pict_enabled() && res_type == *b"PICT" {
eprintln!("[RSRC] GetResource('PICT', {})", res_id);
}
if trace_sound_resource {
eprintln!("[RSRC] GetResource('snd ', {})", res_id);
}
if let Some((refnum, ptr)) = self.find_resource_any(res_type, res_id) {
if trace_menu_pict_enabled() && res_type == *b"PICT" {
eprintln!(
"[RSRC] GetResource('PICT', {}) -> ${:08X} (refnum {})",
res_id, ptr, refnum
);
}
if trace_sound_resource {
eprintln!(
"[RSRC] GetResource('snd ', {}) -> ${:08X} (refnum {})",
res_id, ptr, refnum
);
}
if trace_getresource_enabled() {
let preview: Vec<String> = bus
.read_bytes(ptr, 16)
.iter()
.map(|b| format!("{:02X}", b))
.collect();
eprintln!(
"[GETRESOURCE] -> ptr=${:08X} preview={}",
ptr,
preview.join(" ")
);
}
let handle = self
.get_or_create_resource_handle_in_file(bus, res_type, res_id, ptr, refnum);
cpu.write_reg(Register::A0, handle);
cpu.write_reg(Register::D0, 0);
bus.write_word(0x0A60, 0); // ResErr = noErr
bus.write_long(sp + 6, handle);
cpu.write_reg(Register::A7, sp + 6);
self.maybe_inject_ajcp_decompress(bus, cpu, handle);
return Some(Ok(()));
}
if trace_menu_pict_enabled() && res_type == *b"PICT" {
eprintln!("[RSRC] GetResource('PICT', {}) -> not found", res_id);
}
if trace_sound_resource {
eprintln!("[RSRC] GetResource('snd ', {}) -> not found", res_id);
}
if res_type == *b"STR " {
if let Some(ptr) = self.synthesize_system_str(bus, res_id) {
let handle = self.get_or_create_resource_handle(bus, res_type, res_id, ptr);
cpu.write_reg(Register::A0, handle);
cpu.write_reg(Register::D0, 0);
bus.write_word(0x0A60, 0); // ResErr = noErr
bus.write_long(sp + 6, handle);
cpu.write_reg(Register::A7, sp + 6);
self.maybe_inject_ajcp_decompress(bus, cpu, handle);
return Some(Ok(()));
}
}
cpu.write_reg(Register::A0, 0);
cpu.write_reg(Register::D0, -192i32 as u32);
bus.write_word(0x0A60, (-192i16) as u16); // ResErr = resNotFound
bus.write_long(sp + 6, 0);
cpu.write_reg(Register::A7, sp + 6);
Ok(())
}
// Get1Resource ($A81F)
// FUNCTION Get1Resource(theType: ResType; theID: INTEGER): Handle;
// Searches only the current resource file (unlike GetResource which searches all).
// Inside Macintosh Volume I, I-119
// Get1Resource ($A81F): Searches current resource file only; writes ResErr per IM:I I-119
(true, 0x01F) => {
let sp = cpu.read_reg(Register::A7);
let id = bus.read_word(sp) as i16;
let raw_res_type = bus.read_long(sp + 2).to_be_bytes();
let res_type = super::TrapDispatcher::normalize_ostype(raw_res_type);
let trace_sound_resource = trace_sound_resource_enabled() && res_type == *b"snd ";
let trace_extended = self.should_trace_extended_resource_activity();
if trace_extended {
eprintln!(
"[RSRC] Get1Resource raw='{}' norm='{}' id={} current={} chain=[{}]",
format_ostype(raw_res_type),
format_ostype(res_type),
id,
self.current_resource_refnum(),
self.resource_trace_chain()
);
}
if trace_menu_pict_enabled() && res_type == *b"PICT" {
eprintln!("[RSRC] Get1Resource('PICT', {})", id);
}
if trace_sound_resource {
eprintln!("[RSRC] Get1Resource('snd ', {})", id);
}
let handle = if let Some((refnum, ptr)) = self.find_resource_current(res_type, id) {
if trace_menu_pict_enabled() && res_type == *b"PICT" {
eprintln!(
"[RSRC] Get1Resource('PICT', {}) -> ${:08X} (refnum {})",
id, ptr, refnum
);
}
if trace_sound_resource {
eprintln!(
"[RSRC] Get1Resource('snd ', {}) -> ${:08X} (refnum {})",
id, ptr, refnum
);
}
self.get_or_create_resource_handle_in_file(bus, res_type, id, ptr, refnum)
} else if res_type == *b"STR " {
match self.synthesize_system_str(bus, id) {
Some(ptr) => self.get_or_create_resource_handle(bus, res_type, id, ptr),
None => 0,
}
} else {
if trace_menu_pict_enabled() && res_type == *b"PICT" {
eprintln!("[RSRC] Get1Resource('PICT', {}) -> not found", id);
}
if trace_sound_resource {
eprintln!("[RSRC] Get1Resource('snd ', {}) -> not found", id);
}
0
};
// IM:I I-119 / Macintosh Revealed 1987 p. 285: both paths
// must write ResError ($0A60). A successful Get1Resource
// clears it to noErr (0); a miss sets it to resNotFound
// (-192). GetResource handles this correctly at $A9A0;
// before this fix Get1Resource forgot — games that call
// ResError() after Get1Resource saw stale values from
// the previous resource call.
//
// Regression coverage:
// get1resource_success_clears_reserror
// get1resource_miss_sets_resnotfound
if handle != 0 {
cpu.write_reg(Register::A0, handle);
cpu.write_reg(Register::D0, 0);
bus.write_word(0x0A60, 0); // ResErr = noErr
bus.write_long(sp + 6, handle);
cpu.write_reg(Register::A7, sp + 6);
self.maybe_inject_ajcp_decompress(bus, cpu, handle);
} else {
cpu.write_reg(Register::A0, 0);
cpu.write_reg(Register::D0, -192i32 as u32);
bus.write_word(0x0A60, (-192i16) as u16); // ResErr = resNotFound
bus.write_long(sp + 6, 0);
cpu.write_reg(Register::A7, sp + 6);
}
Ok(())
}
// DetachResource ($A992)
// Sets the resource's master pointer so the resource data won't be released.
// PROCEDURE DetachResource(theResource: Handle);
// Inside Macintosh Volume I, I-122
// DetachResource ($A992): Clones shared resource data into a detached private handle and stops treating that handle as resource-backed
(true, 0x192) => {
let sp = cpu.read_reg(Register::A7);
let handle = bus.read_long(sp);
if let Some((ptr, res_type, res_id)) = self.loaded_handles.get(&handle).copied() {
let attrs = self.resource_attributes_for_handle(handle).unwrap_or(0);
if (attrs & Self::RES_CHANGED_ATTR) != 0 {
bus.write_word(0x0A60, 0);
} else {
// DetachResource keeps the current handle/data valid but removes
// the link from the resource map. In this HLE, GetResource hands
// out per-call master pointers to shared guest data, so detaching
// must clone the block to keep subsequent SetHandleSize and
// DisposeHandle calls from mutating or freeing the shared resource.
// Macintosh Revealed 1987, p. 306
if handle != 0 && ptr != 0 {
if let Some(size) = bus.get_alloc_size(ptr) {
let detached_ptr = bus.alloc(size);
for offset in 0..size {
bus.write_byte(
detached_ptr + offset,
bus.read_byte(ptr + offset),
);
}
bus.write_long(handle, detached_ptr);
}
}
self.loaded_handles.remove(&handle);
if let Some(refnum) = self.resource_handle_files.remove(&handle) {
self.detached_handle_files.insert(handle, refnum);
}
self.detached_handles.insert(handle, (res_type, res_id));
bus.write_word(0x0A60, 0);
}
} else {
bus.write_word(0x0A60, Self::RES_NOT_FOUND as u16);
}
cpu.write_reg(Register::A7, sp + 4);
Ok(())
}
// LoadResource ($A9A2)
// Reads a resource into memory. No-op if already loaded.
// PROCEDURE LoadResource(theResource: Handle);
// Inside Macintosh: More Macintosh Toolbox 1993, 1-80
//
// ReleaseResource ($A9A3)
// Releases the memory occupied by a resource.
// PROCEDURE ReleaseResource(theResource: Handle);
// Inside Macintosh Volume I, I-121
// LoadResource ($A9A2): Populates empty SetResLoad(FALSE) handles and reloads resource-backed handles whose master pointer was later emptied/purged; BasiliskII leaves plain handles untouched and reports `noErr` there.
(true, 0x1A2) => {
let sp = cpu.read_reg(Register::A7);
let handle = bus.read_long(sp);
if handle == 0 {
bus.write_word(0x0A60, Self::RES_NOT_FOUND as u16);
} else if let Some((ptr, res_type, res_id)) =
self.loaded_handles.get(&handle).copied()
{
let type_str = String::from_utf8_lossy(&res_type);
eprintln!(
"[TRAP] LoadResource handle=${:08X} '{}' {} ptr=${:08X}",
handle, type_str, res_id, ptr
);
// Successful reload must canonicalize the master
// pointer again even if the caller had emptied or
// scribbled over it first.
bus.write_long(handle, ptr);
self.restore_loaded_resource_handle(handle, ptr);
bus.write_word(0x0A60, 0);
} else {
bus.write_word(0x0A60, 0);
}
cpu.write_reg(Register::A7, sp + 4);
Ok(())
}
// ReleaseResource ($A9A3)
// Releases the memory occupied by a resource and stops recognizing
// the given handle as a resource handle.
// PROCEDURE ReleaseResource(theResource: Handle);
// Inside Macintosh Volume I, I-120 to I-121
// ReleaseResource ($A9A3): Clears live resource handles and stops recognizing them as resource-backed; returns `resNotFound` for non-resource handles
(true, 0x1A3) => {
let sp = cpu.read_reg(Register::A7);
let handle = bus.read_long(sp);
if let Some((ptr, res_type, res_id)) = self.loaded_handles.get(&handle).copied() {
let type_str = String::from_utf8_lossy(&res_type);
eprintln!(
"[TRAP] ReleaseResource handle=${:08X} '{}' {} ptr=${:08X}",
handle, type_str, res_id, ptr
);
let attrs = self.resource_attributes_for_handle(handle).unwrap_or(0);
if (attrs & Self::RES_CHANGED_ATTR) == 0 {
bus.write_long(handle, 0);
self.loaded_handles.remove(&handle);
self.resource_handle_files.remove(&handle);
}
bus.write_word(0x0A60, 0);
} else {
bus.write_word(0x0A60, Self::RES_NOT_FOUND as u16);
}
cpu.write_reg(Register::A7, sp + 4);
Ok(())
}
// SizeResource ($A9A5)
// Returns the size (in bytes) of a resource in the resource file.
// FUNCTION SizeResource(theResource: Handle): LONGINT;
// Inside Macintosh Volume I, I-121
//
// Both paths must update ResErr: success → noErr, failure →
// resNotFound. Before this fix the success path left ResErr
// untouched, so a caller that ran a successful SizeResource
// after an earlier failed Resource Manager call would see a
// stale -192.
//
// Regression coverage:
// sizeresource_success_clears_reserror
// SizeResource ($A9A5): Returns data size; clears ResErr on success per IM:I I-121
(true, 0x1A5) => self.handle_resource_size_query(bus, cpu),
// RGetResource ($A80C)
// GetResource that includes ROM-resident resources in the
// search.
// FUNCTION RGetResource(theType: ResType; theID: INTEGER): Handle;
// Inside Macintosh: More Macintosh Toolbox 1993, 1-78
// (also Inside Macintosh Volume V, V-159)
//
// IM:MTb 1-78: "RGetResource first uses GetResource to
// search [the chain]. [...] If GetResource doesn't find
// the specified resource [...] RGetResource sets the
// global variable RomMapInsert to TRUE, then calls
// GetResource again. In response, GetResource [...]
// looks in the resource map of the ROM-resident resources
// before searching the resource map of the System file."
//
// Systemless's HLE has no ROM resource fork, so the second
// pass would walk an empty ROM map and produce the same
// NIL. Delegate the entire call to GetResource ($A9A0).
// The RomMapInsert global is documented as auto-cleared
// per call; since no other Systemless trap consumes it,
// modelling it would be cosmetic — skip.
//
// Stack frame is identical to GetResource:
// pre: SP -> [theID(2)][theType(4)][result-slot(4)]
// post: SP -> [result(4)]; pop 6, leave 4.
//
// Regression coverage:
// rgetresource_returns_handle_for_open_chain_match
// rgetresource_miss_returns_nil_with_resnotfound
// rgetresource_walks_chain_not_just_current_file
// RGetResource ($A80C): Delegates to GetResource ($A9A0) since Systemless HLE has no ROM resource fork; both passes produce the same observable handle/ResErr per IM:MTb 1-78.
(true, 0x00C) => return self.dispatch_resource(true, 0x1A0, cpu, bus),
// GetResFileAttrs ($A9F6)
// Returns the resource-map-level attribute INTEGER for an
// open resource file.
// FUNCTION GetResFileAttrs(refNum: INTEGER): INTEGER;
// Inside Macintosh Volume I, I-126
//
// IM:I-126: "If there's no resource file with the given
// reference number, GetResFileAttrs will do nothing and
// the ResError function will return the result code
// resFNotFound." — the result slot is left untouched on
// the error path (caller's pre-call value preserved).
//
// Stack frame:
// pre: SP -> [refNum(2)][result-slot(2)]
// post: SP -> [result(2)]; pop 2 leaves 2.
//
// Regression coverage:
// getresfileattrs_returns_default_attrs_for_open_file
// getresfileattrs_invalid_refnum_sets_resfnotfound
// setresfileattrs_then_get_roundtrips_per_im_i_126
// setresfileattrs_mapchanged_survives_additional_resource_install
// setresfileattrs_and_getresfileattrs_three_files_all_independent
// setresfileattrs_uses_explicit_refnum_not_current_resource_file
// GetResFileAttrs ($A9F6): Returns the documented resource-map
// attribute bits (mapReadOnly/mapCompact/mapChanged) per IM:I-126;
// resFNotFound on missing refnum, result slot untouched on error path.
(true, 0x1F6) => {
let sp = cpu.read_reg(Register::A7);
let refnum = bus.read_word(sp);
let attrs_opt = self
.resources
.as_ref()
.and_then(|resources| resources.files.get(&refnum))
.map(|file| file.map_attrs);
match attrs_opt {
Some(attrs) => {
bus.write_word(0x0A60, 0); // ResErr = noErr
bus.write_word(sp + 2, attrs & 0x00E0);
}
None => {
bus.write_word(0x0A60, (-193i16) as u16); // resFNotFound
}
}
cpu.write_reg(Register::A7, sp + 2);
Ok(())
}
// SetResFileAttrs ($A9F7)
// Sets the resource-map-level attribute INTEGER for an
// open resource file.
// PROCEDURE SetResFileAttrs(refNum: INTEGER; attrs: INTEGER);
// Inside Macintosh Volume I, I-126
//
// IM:I-126: "If there's no resource file with the given
// reference number, SetResFileAttrs will do nothing but
// the ResError function will return the result code
// noErr." — the only Resource Manager error path that
// resolves to noErr instead of a -19x code, and the IM
// wording is verbatim. Real-Mac BasiliskII confirms.
//
// Stack frame (Pascal — refNum pushed first, deepest):
// pre: SP -> [attrs(2)][refNum(2)]
// post: SP -> (args popped, A7 = pre + 4)
//
// Regression coverage:
// setresfileattrs_then_get_roundtrips_per_im_i_126
// setresfileattrs_invalid_refnum_returns_noerr
// setresfileattrs_mapchanged_survives_additional_resource_install
// setresfileattrs_and_getresfileattrs_three_files_all_independent
// setresfileattrs_uses_explicit_refnum_not_current_resource_file
// SetResFileAttrs ($A9F7): Stores only the documented
// resource-map attribute bits (mapReadOnly/mapCompact/mapChanged)
// per IM:I-126; missing refnum is a documented no-op that
// returns noErr (not resFNotFound — IM verbatim).
(true, 0x1F7) => {
let sp = cpu.read_reg(Register::A7);
let attrs = bus.read_word(sp);
let refnum = bus.read_word(sp + 2);
if let Some(file) = self
.resources
.as_mut()
.and_then(|resources| resources.files.get_mut(&refnum))
{
file.map_attrs = attrs & 0x00E0;
}
// Both branches set ResErr to noErr per IM:I-126.
bus.write_word(0x0A60, 0);
cpu.write_reg(Register::A7, sp + 4);
Ok(())
}
// MaxSizeRsrc ($A821)
// Returns the resource size from the resource map without
// reading from disk.
// FUNCTION MaxSizeRsrc(theResource: Handle): LONGINT;
// Inside Macintosh Volume IV, IV-16
//
// IM:IV-16: "MaxSizeRsrc is similar to SizeResource except
// that it does not cause the disk to be read; instead it
// determines the size (in bytes) of the resource from the
// offsets found in the resource map." In Systemless's HLE
// every resource we know about is already resolved to a
// guest-bus pointer (no offline-fork distinction), so the
// disk-vs-map nuance collapses and the two traps share an
// implementation. If a future change introduces a real
// on-disk simulation, the test
// `maxsizersrc_matches_sizeresource_for_loaded_resource`
// is the gate that pins the in-memory equivalence.
//
// Regression coverage:
// maxsizersrc_returns_max_resource_size
// maxsizersrc_nil_handle_sets_resnotfound
// maxsizersrc_matches_sizeresource_for_loaded_resource
// MaxSizeRsrc ($A821): Returns resource size from map without disk read per IM:IV-16. In our memory-resident HLE this collapses to the same data path as SizeResource ($A9A5).
(true, 0x021) => self.handle_resource_size_query(bus, cpu),
// ResourceDispatch ($A822) — partial-resource family
// PROCEDURE ReadPartialResource (theResource: Handle;
// offset: LongInt; buffer: UNIV Ptr; count: LongInt);
// PROCEDURE WritePartialResource(theResource: Handle;
// offset: LongInt; buffer: UNIV Ptr; count: LongInt);
// PROCEDURE SetResourceSize (theResource: Handle;
// newSize: LongInt);
// Inside Macintosh: More Macintosh Toolbox 1993, 1-69 .. 1-71
// Inside Macintosh Volume VI 1991, p. C-27 (selector table)
//
// Selector lives in D0; the dispatcher matches the low
// byte. IM:VI lists the selectors as $0001/$0002/$0003;
// MMTB 1993 lists them as $7001/$7002/$7003. Both forms
// route to the same routine because Apple's dispatcher
// consults only the low byte. The high byte is an MPW
// glue marker, not a parameter-bytes hint (param sizes
// here are 16/16/8 bytes — none of which equals
// 2 × 0x70 = 224).
//
// The Mac maintains a disk/memory dichotomy that makes
// the partial-resource workflow non-trivial there: a
// typical caller does SetResLoad(FALSE) so GetResource
// returns an empty handle, then uses the partial routines
// to stream data in/out of disk. Systemless's HLE has no
// separate disk fork — every loaded resource is a real
// guest-bus allocation — so all three traps act on the
// in-memory copy directly. ReadPartialResource is
// therefore byte-for-byte equivalent to a BlockMove from
// (*handle + offset). WritePartialResource is a BlockMove
// in the opposite direction; if the write would extend
// past the current resource, MMTB 1-70 says the Resource
// Manager grows the resource and returns the diagnostic
// result code writingPastEnd (-189). SetResourceSize
// resizes the in-memory allocation and returns noErr —
// MMTB's documented `resourceInMemory` (-188) success
// code is a real-Mac quirk that signals "disk size
// updated, memory unchanged"; reporting it here would
// break callers that treat any non-zero ResErr as a
// failure when the operation actually succeeded.
//
// Regression coverage:
// readpartialresource_copies_resource_subsection_to_buffer
// readpartialresource_offset_out_of_bounds_sets_inputoutofbounds_err
// readpartialresource_nil_handle_sets_resnotfound
// readpartialresource_pops_sixteen_arg_bytes
// writepartialresource_overwrites_resource_subsection_from_buffer
// writepartialresource_past_end_extends_with_writingpastend_err
// writepartialresource_nil_handle_sets_resnotfound
// setresourcesize_grows_resource_visible_to_sizeresource
// setresourcesize_shrinks_resource_visible_to_sizeresource
// setresourcesize_nil_handle_sets_resnotfound
// setresourcesize_pops_eight_arg_bytes
// partial_resource_roundtrip_via_setresourcesize_and_writepartial
// writepartialresource_then_readpartialresource_without_setresourcesize
// setresourcesize_expand_preserves_original_content_at_prefix
// readpartialresource_reads_entire_resource_from_zero_offset
// ResourceDispatch ($A822): D0-low-byte selectors: 1=ReadPartialResource, 2=WritePartialResource, 3=SetResourceSize per MMTB 1-69..1-71. HLE is memory-resident so all three operate on the in-memory allocation directly; reports inputOutOfBounds/writingPastEnd/resNotFound per IM.
(true, 0x022) => {
let sp = cpu.read_reg(Register::A7);
let routine = (cpu.read_reg(Register::D0) & 0xFF) as u8;
match routine {
// ReadPartialResource (selector 1) — 16 bytes args
// SP+12 theResource | SP+8 offset | SP+4 buffer | SP+0 count
0x01 => {
let count = bus.read_long(sp) as i32;
let buffer = bus.read_long(sp + 4);
let offset = bus.read_long(sp + 8) as i32;
let handle = bus.read_long(sp + 12);
let res_err =
self.read_partial_resource(bus, handle, offset, buffer, count);
bus.write_word(0x0A60, res_err as u16);
cpu.write_reg(Register::A7, sp + 16);
}
// WritePartialResource (selector 2) — 16 bytes args
// SP+12 theResource | SP+8 offset | SP+4 buffer | SP+0 count
0x02 => {
let count = bus.read_long(sp) as i32;
let buffer = bus.read_long(sp + 4);
let offset = bus.read_long(sp + 8) as i32;
let handle = bus.read_long(sp + 12);
let res_err =
self.write_partial_resource(bus, handle, offset, buffer, count);
bus.write_word(0x0A60, res_err as u16);
cpu.write_reg(Register::A7, sp + 16);
}
// SetResourceSize (selector 3) — 8 bytes args
// SP+4 theResource | SP+0 newSize
0x03 => {
let new_size = bus.read_long(sp) as i32;
let handle = bus.read_long(sp + 4);
let res_err = self.set_resource_size(bus, handle, new_size);
bus.write_word(0x0A60, res_err as u16);
cpu.write_reg(Register::A7, sp + 8);
}
_ => {
eprintln!(
"[TRAP] ResourceDispatch unknown selector ${:04X} (low byte ${:02X})",
cpu.read_reg(Register::D0) & 0xFFFF,
routine
);
// Per the unknown-selector convention used by
// DialogDispatch, we leave SP unchanged and set
// resNotFound so the caller surfaces the miss.
bus.write_word(0x0A60, Self::RES_NOT_FOUND as u16);
}
}
Ok(())
}
// ResError ($A9AF)
// Returns the result code from the most recent Resource Manager call.
// FUNCTION ResError: INTEGER;
// Inside Macintosh Volume I, I-118
// ResError ($A9AF): Returns last resource error from D0
(true, 0x1AF) => {
let sp = cpu.read_reg(Register::A7);
let res_err = bus.read_word(0x0A60) as i16;
let pc = cpu.read_reg(Register::PC);
eprintln!("[TRAP] ResError -> {} (PC=${:08X})", res_err, pc);
bus.write_word(sp, res_err as u16);
Ok(())
}
// Get1NamedResource ($A820)
// Returns a handle to the named resource of the given type in the current resource file.
// FUNCTION Get1NamedResource(theType: ResType; name: Str255): Handle;
// Inside Macintosh Volume VI, VI-13
//
// Like Get1Resource vs. GetResource, Get1NamedResource only
// searches the current resource file, while GetNamedResource
// walks the full resource chain. Both must write ResErr per
// IM:I I-119.
//
// Regression coverage:
// get1namedresource_miss_sets_resnotfound
// get_named_resource_pair_returns_handle_for_named_resource_in_current_file_parametric
// getnamedresource_walks_full_search_chain_while_get1namedresource_restricts_to_current_file_per_im_iv_15
// getnamedresource_is_case_insensitive_while_get1namedresource_is_exact_match_only_per_impl_fallback
// Get1NamedResource ($A820): Searches by Pascal name string; writes ResErr per IM:IV IV-15
(true, 0x020) => {
let sp = cpu.read_reg(Register::A7);
let name_ptr = bus.read_long(sp);
let raw_res_type = bus.read_long(sp + 4).to_be_bytes();
let res_type = super::TrapDispatcher::normalize_ostype(raw_res_type);
let type_str = std::str::from_utf8(&res_type).unwrap_or("????");
// Read Pascal string name
let name_len = bus.read_byte(name_ptr) as usize;
let mut name_bytes = vec![0u8; name_len];
for (i, byte) in name_bytes.iter_mut().enumerate() {
*byte = bus.read_byte(name_ptr + 1 + i as u32);
}
let name = String::from_utf8_lossy(&name_bytes).to_string();
eprintln!("[TRAP] Get1NamedResource('{}', \"{}\")", type_str, name);
// Look up by (type, name) in the named resources index
let handle =
self.find_named_resource_current(res_type, &name)
.map(|(refnum, id, ptr)| {
self.get_or_create_resource_handle_in_file(
bus, res_type, id, ptr, refnum,
)
});
if let Some(h) = handle {
eprintln!("[TRAP] Get1NamedResource -> handle ${:08X}", h);
bus.write_long(sp + 8, h);
cpu.write_reg(Register::A7, sp + 8);
cpu.write_reg(Register::D0, 0);
bus.write_word(0x0A60, 0); // ResErr = noErr
self.maybe_inject_ajcp_decompress(bus, cpu, h);
} else {
eprintln!("[TRAP] Get1NamedResource -> NULL (not found)");
bus.write_long(sp + 8, 0);
cpu.write_reg(Register::A7, sp + 8);
cpu.write_reg(Register::D0, -192i32 as u32);
bus.write_word(0x0A60, (-192i16) as u16); // ResErr = resNotFound
}
Ok(())
}
// CurResFile ($A994)
// Returns the file reference number of the current resource file.
// FUNCTION CurResFile: INTEGER;
// Inside Macintosh Volume I, I-116
// CurResFile ($A994): Returns the current resource-file refnum from the live Resource Manager search state
(true, 0x194) => {
let sp = cpu.read_reg(Register::A7);
let refnum = self.current_resource_refnum();
if super::dispatch::trace_resfile_enabled() {
eprintln!(
"[TRAP] CurResFile -> {} (PC=${:08X})",
refnum,
cpu.read_reg(Register::PC)
);
}
bus.write_word(sp, refnum);
Ok(())
}
// HomeResFile ($A9A4)
// Returns the file reference number of the resource file containing the resource.
// FUNCTION HomeResFile(theResource: Handle): INTEGER;
// Inside Macintosh Volume I, I-117
//
// Regression coverage:
// src/trap/resource.rs::tests::home_res_file_returns_loaded_resource_refnum
// src/trap/resource.rs::tests::home_res_file_returns_minus_one_for_detached_handle
// src/trap/resource.rs::tests::home_res_file_returns_minus_one_for_unknown_handle
// HomeResFile ($A9A4): Returns the owning resource-file refnum for live resource handles; detached or unknown handles return `-1` and `resNotFound`
(true, 0x1A4) => {
let sp = cpu.read_reg(Register::A7);
let handle = bus.read_long(sp);
let new_sp = sp + 4;
cpu.write_reg(Register::A7, new_sp);
if self.loaded_handles.contains_key(&handle) {
let refnum = self
.resource_handle_files
.get(&handle)
.copied()
.unwrap_or(0);
bus.write_word(new_sp, refnum);
bus.write_word(0x0A60, 0);
} else {
bus.write_word(new_sp, (-1i16) as u16);
bus.write_word(0x0A60, Self::RES_NOT_FOUND as u16);
}
Ok(())
}
// LoadSeg ($A9F0)
// Loads a CODE segment and patches its jump table entries.
// Inside Macintosh Volume II, II-60; Executor segment.cpp C_LoadSeg
//
// Two JT entry formats are supported:
//
// 1. Standard MPW format (Inside Macintosh Volume II, II-60):
// Unloaded: [offset(2), 3F3C(2), seg#(2), A9F0(2)]
// Loaded: [seg#(2), 4EF9(2), address(4) ]
// Calls enter at entry+2. LoadSeg seg# pushed on stack by MOVE.W.
//
// 2. Think C / CodeWarrior format (Processes 1994, 7-7 note):
// Unloaded: [A9F0(2), 0000(2), offset(2), seg#(2)]
// Loaded: [seg#(2), 4EF9(2), address(4) ]
// Calls enter at entry+0. No MOVE.W; seg# read from JT entry.
// The word popped from stack is the high word of the JSR return
// address, not the actual segment number.
//
// Regression coverage:
// resource::tests::loadseg_mpw_call_consumes_segment_number_word_argument
// resource::tests::loadseg_patches_calling_jump_table_entry_to_jmp_loaded_code
// LoadSeg ($A9F0): Loads CODE resource, caches in segment_map
(true, 0x1F0) => {
let sp = cpu.read_reg(Register::A7);
let pc = cpu.read_reg(Register::PC); // past A9F0
let a9f0_addr = pc.wrapping_sub(2);
// Detect format: in standard format, [A9F0-4] = 3F3C (MOVE.W).
// In Think C format, A9F0 is at entry+0 and [A9F0+2] = 0x0000.
let word_before_seg = bus.read_word(a9f0_addr.wrapping_sub(4));
let is_standard = word_before_seg == 0x3F3C;
let (seg_num, entry_addr) = if is_standard {
// Standard: seg# was pushed by MOVE.W, pop it
let sn = bus.read_word(sp) as i16;
cpu.write_reg(Register::A7, sp + 2);
// Entry starts 6 bytes before A9F0
(sn, a9f0_addr.wrapping_sub(6))
} else {
// Think C: A9F0 at entry+0, seg# at entry+6, offset at entry+4
// Stack has JSR return address (don't pop segment number)
let entry = a9f0_addr; // A9F0 IS entry+0
let sn = bus.read_word(entry + 6) as i16;
(sn, entry)
};
let trace_loadseg = trace_loadseg_enabled();
if trace_loadseg {
eprintln!(
"[TRAP] LoadSeg(seg={}, fmt={}) entry=${:08X}",
seg_num,
if is_standard { "mpw" } else { "thinkc" },
entry_addr
);
}
if let Some(&seg_addr) = self.segment_map.get(&{ seg_num }) {
let first_word = bus.read_word(seg_addr);
let header_size: u32 = if first_word == 0xFFFF { 40 } else { 4 };
// Read segment header: taboff and nentries
// Inside Macintosh Volume II, II-61
if first_word != 0xFFFF {
let tab_off = bus.read_word(seg_addr) as u32;
let n_entries = bus.read_word(seg_addr + 2) as u32;
let a5 = cpu.read_reg(Register::A5);
let cur_jt_offset = bus.read_word(0x0934) as u32;
let ptr_base = a5 + tab_off + cur_jt_offset;
if trace_loadseg {
eprintln!(
"[TRAP] LoadSeg seg={}: taboff={} n={} ptr=${:08X}",
seg_num, tab_off, n_entries, ptr_base
);
}
let mut patched = 0u32;
for i in 0..n_entries {
let p = ptr_base + i * 8;
// Skip already-loaded entries (JMP.L = 0x4EF9 at +2)
if bus.read_word(p + 2) == 0x4EF9 {
continue;
}
Self::patch_loadseg_entry(bus, seg_num, seg_addr, header_size, p);
patched += 1;
}
if trace_loadseg {
eprintln!(
"[TRAP] LoadSeg: patched {}/{} entries for seg {}",
patched, n_entries, seg_num
);
}
}
// Ensure the calling entry itself was patched.
// In Think C format, the calling entry may lie outside the
// segment header's taboff range (entries for a given segment
// can be scattered across the JT).
if bus.read_word(entry_addr + 2) != 0x4EF9 {
let code_addr = Self::patch_loadseg_entry(
bus,
seg_num,
seg_addr,
header_size,
entry_addr,
);
if trace_loadseg {
eprintln!(
"[TRAP] LoadSeg: patched calling entry at ${:08X} -> ${:08X}",
entry_addr, code_addr
);
}
}
// Apply environment-driven byte patches now that this
// segment's code is in RAM. Format:
// SYSTEMLESS_PATCH_BYTES="0xADDR=HEXBYTES,0xADDR2=BYTES2"
// Used to short-circuit shareware-validation entry
// points that we don't have the keys for. POD MARS
// Master's "I'm evaluating" / "Register now" buttons
// share a click-validation function at $00258440 that
// wants a 16-char Name+Reg key combo we can't compute;
// patching the entry to `MOVEQ #1,D0 ; UNLK A6 ; RTS`
// (704E5E4E75) lets the click flow through to gameplay.
if let Ok(spec) = std::env::var("SYSTEMLESS_PATCH_BYTES") {
for chunk in spec.split(',') {
let chunk = chunk.trim();
if chunk.is_empty() {
continue;
}
let Some((addr_str, bytes_str)) = chunk.split_once('=') else {
continue;
};
let addr_str = addr_str
.trim()
.trim_start_matches("0x")
.trim_start_matches("0X");
let Ok(addr) = u32::from_str_radix(addr_str, 16) else {
continue;
};
let bytes_str = bytes_str.trim();
if bytes_str.len() % 2 != 0 {
continue;
}
let mut written = 0u32;
for i in 0..(bytes_str.len() / 2) {
let hex = &bytes_str[i * 2..i * 2 + 2];
let Ok(byte) = u8::from_str_radix(hex, 16) else {
continue;
};
bus.write_byte(addr + written, byte);
written += 1;
}
eprintln!(
"[PATCH] Wrote {} bytes at ${:08X} (post-LoadSeg seg={})",
written, addr, seg_num
);
}
}
// Re-execute the calling JT entry.
// After patching, entry+2 = JMP.L instruction.
let jmp_addr = entry_addr + 2;
if trace_loadseg {
eprintln!("[TRAP] LoadSeg: re-executing JT entry at ${:08X}", jmp_addr);
}
cpu.write_reg(Register::PC, jmp_addr);
Ok(())
} else {
eprintln!("ERROR: LoadSeg unknown segment {}", seg_num);
Err(Error::Halted)
}
}
// GetResInfo ($A9A8)
// Returns the resource ID, type, and name for a resource-backed handle.
// PROCEDURE GetResInfo (theResource: Handle; VAR theID: Integer;
// VAR theType: ResType; VAR name: Str255);
// More Macintosh Toolbox 1993, 1-81 to 1-82
// GetResInfo ($A9A8): Pops 16 bytes; returns resource ID, type, and Pascal name for resource-backed handles
(true, 0x1A8) => {
let sp = cpu.read_reg(Register::A7);
let name_ptr = bus.read_long(sp);
let type_ptr = bus.read_long(sp + 4);
let id_ptr = bus.read_long(sp + 8);
let handle = bus.read_long(sp + 12);
if let Some((res_type, res_id, name)) = self.resource_metadata_for_handle(handle) {
if id_ptr != 0 {
bus.write_word(id_ptr, res_id as u16);
}
if type_ptr != 0 {
bus.write_long(type_ptr, u32::from_be_bytes(res_type));
}
if name_ptr != 0 {
bus.write_pstring(name_ptr, name.as_deref().unwrap_or("").as_bytes());
}
bus.write_word(0x0A60, 0);
} else {
bus.write_word(0x0A60, (-192i16) as u16);
}
cpu.write_reg(Register::A7, sp + 16);
Ok(())
}
// GetResAttrs ($A9A6)
// Returns the resource attributes for a live resource handle.
// FUNCTION GetResAttrs(theResource: Handle): INTEGER;
// Inside Macintosh Volume I, I-121
//
// Regression coverage:
// tests::get_res_attrs_returns_attribute_bits_for_live_resource_handle
// tests::get_res_attrs_returns_res_changed_for_unknown_handle
// GetResAttrs ($A9A6): Returns resource-map attribute bits for live resource handles; non-resource handles report `resChanged` and set `resNotFound`
(true, 0x1A6) => {
let sp = cpu.read_reg(Register::A7);
let handle = bus.read_long(sp);
if let Some(attrs) = self.resource_attributes_for_handle(handle) {
bus.write_word(sp + 4, attrs);
bus.write_word(0x0A60, 0);
} else {
bus.write_word(sp + 4, Self::RES_CHANGED_ATTR);
bus.write_word(0x0A60, Self::RES_NOT_FOUND as u16);
}
cpu.write_reg(Register::A7, sp + 4);
Ok(())
}
// InitPack ($A9E5)
// PROCEDURE InitPack(packID: INTEGER);
// Inside Macintosh Volume I, I-483
// Prior implementation lumped this with TECut as a shared Ok(()) and
// silently left the 2-byte packID on the stack — latent bug.
// InitPack ($A9E5): Pops 2-byte packID and returns per IM:I I-483.
// Per-package loading (Floating Point, Math, etc.) is not modelled;
// Systemless keeps the call as a no-op but records the last pack ID so
// future pack-specific heuristics have a cheap hook.
// Note: CountTypes / Count1Types live at `$A99E` (Toolbox, `0x19E`),
// not `$A9E5`.
(true, 0x1E5) => {
let sp = cpu.read_reg(Register::A7);
self.last_init_pack_id = Some(bus.read_word(sp) as i16);
cpu.write_reg(Register::A7, sp + 2);
Ok(())
}
// TECut ($A9D6) is handled in dispatch_dialog because it needs
// the TERec accessors. Leaving the arm out here lets the
// dispatch chain fall through to dispatch_dialog.
// NOTE: DisposePalette ($AA93, 0x293) is handled by the Palette
// Manager in quickdraw.rs.
// NOTE: PutScrap ($A9FE, 0x1FE) moved to toolbox.rs Scrap Manager.
// ========== Script Manager phantom-trap arms ==========
//
// $AA87 GetScript / $AA88 SetScript / $AA89 GetEnvirons
// are PHANTOM TRAP WORDS: per IM:VI Table C-1 master
// trap dispatch table + IM:V V-291 master trap
// dispatch table, the trap-word range $AA87..$AA89 is
// NOT allocated to any documented Apple trap (the
// documented range goes ... $AA52 _HighLevelFSDispatch
// ... $AA90 _InitPalettes ... with a gap covering
// $AA53..$AA8F that includes our three phantom arms
// — verified via `grep -nE "AA8[7-9]\b"
// systemless-inside-macintosh-md/` returning zero matches
// in the master dispatch tables).
//
// The canonical Script Manager dispatch is via
// $A8B5 _ScriptUtil (a selector-based dispatcher
// already implemented at quickdraw.rs:5903 with
// selectors 0..22 covering FontScript, IntlScript,
// KeyScript, Font2Script, GetEnvirons, SetEnvirons,
// GetScript, SetScript, CharByte, CharType,
// Char2Pixel etc.) — apps using MPW Pascal call
// `GetScript(script, selector)` which the compiler
// expands to `_ScriptUtil` ($A8B5) with D0 set to
// the GetScript selector (4 per IM:VI 14-23 + Text
// 1993 6-11). There is no separate $AA87 trap-word
// dispatch on real Mac.
//
// ## Why these phantom arms exist in Systemless
//
// Some 68k binaries (especially older System-6 or
// System-7 .0 era apps that linked against
// pre-Universal-Headers MPW glue) emit $AA87 / $AA88
// / $AA89 directly as a courtesy to the kernel's
// dispatch table (Apple kept many such word-aliases
// allocated for binary compatibility). On real Mac
// these would either land on the unimplemented-trap
// handler ($A89F) or on a glue-layer hook. Systemless's
// arms collapse them to noErr / 0 / no-op so apps
// don't crash.
//
// ## Stack frames (best-effort, no IM cite)
//
// - $AA87 GetScript: 6 args (script INTEGER + selector
// INTEGER) + 2 result. Pop 6. Returns 0 INTEGER as
// "script not installed" sentinel per IM:VI 14-29
// GetScript-via-ScriptUtil "Returns 0 if the script
// isn't installed".
// - $AA88 SetScript: 6 args (script INTEGER + selector
// INTEGER + value INTEGER), no result. Pop 6.
// - $AA89 GetEnvirons: 4 args (selector INTEGER) + 2
// result. Pop 6 = 4 args + 2 result-slot? Actually
// the existing impl pops 2 args + 2 result writeback
// = 4-byte total stack window. Wait, current pop is
// sp+2 which is 2 bytes — `bus.write_word(sp+2, 0);
// cpu.write_reg(Register::A7, sp+2)`. That's pop=2
// on a frame that the trap-doc claims is "Pops 6
// bytes". Trap-doc lies; existing impl correctly
// pops 2 + writes 2 result. The 6 in the trap-doc
// is the FRAME WINDOW (2 args + 2 result + 2 slack
// pre-pollution?) not the pop count.
//
// ## Manager classification corrected
//
// Was "Resource Manager — Toolbox Traps" (an artifact
// of where the arm landed in dispatch_resource).
// Per the manager-classification audit pattern: the
// canonical manager is whatever IM
// chapter heading the trap is documented under, NOT
// what dispatch arm it lands in. Per IM:VI 14-1
// "Script Manager" chapter heading + Text 1993 6-1
// "Script Manager" chapter, the canonical manager
// for these phantom GetScript/SetScript/GetEnvirons
// aliases is Script Manager (even though they're
// phantom — the IM-documented routine names live in
// the Script Manager chapter).
// GetScript ($AA87)
// PHANTOM trap-word — see Script Manager phantom-trap
// family rationale block above. Arm matches the
// ScriptUtil ($A8B5) selector-4 GetScript behaviour
// exactly: returns 0 INTEGER as "script not installed"
// sentinel per IM:VI 14-29 (the ScriptUtil-GetScript
// selector documents this fallback).
// GetScript ($AA87): Phantom trap word — verified absent from IM:VI Table C-1 + IM:V V-291 master dispatch tables. Stack: SP+0 selector(2), SP+2 script(2), SP+4 result(2). Pops 4 + writes 0 INTEGER to result slot (pre-pop SP+4) per IM:VI 14-29 ScriptUtil-GetScript "Returns 0 if the script isn't installed" fallback. Canonical dispatch is via $A8B5 _ScriptUtil (selector 4) — apps that emit $AA87 directly are using non-public Apple-allocated word-aliases.
(true, 0x287) => {
// GetScript: FUNCTION (script, selector): Int
// SP+0 selector(2), SP+2 script(2), SP+4 result(2).
let sp = cpu.read_reg(Register::A7);
bus.write_word(sp + 4, 0);
cpu.write_reg(Register::A7, sp + 4);
cpu.write_reg(Register::D0, 0);
Ok(())
}
// SetScript ($AA88)
// PHANTOM trap-word — see Script Manager phantom-trap
// family rationale block above. Arm matches the
// ScriptUtil ($A8B5) selector-6 SetScript behaviour
// (PROCEDURE no-op since Systemless doesn't maintain
// per-script Manager state).
// SetScript ($AA88): Phantom trap word — verified absent from IM:VI Table C-1 + IM:V V-291 master dispatch tables. Stack: SP+0 value(2), SP+2 selector(2), SP+4 script(2). Pops 6 PROCEDURE no-result per IM:VI 14-29 ScriptUtil-SetScript sig — Systemless doesn't maintain per-script-system local-variable state so the set is a no-op. Canonical dispatch is via $A8B5 _ScriptUtil (selector 6).
(true, 0x288) => {
// SetScript: PROCEDURE (script, selector, value). Pops 6.
let sp = cpu.read_reg(Register::A7);
cpu.write_reg(Register::A7, sp + 6);
cpu.write_reg(Register::D0, 0);
Ok(())
}
// GetEnvirons ($AA89)
// PHANTOM trap-word — see Script Manager phantom-trap
// family rationale block above. Arm matches the
// ScriptUtil ($A8B5) selector-4 GetEnvirons behaviour:
// returns 0 INTEGER as "verb not recognised" sentinel
// per IM:VI 14-30 ScriptUtil-GetEnvirons fallback.
// Apps that probe `GetEnvirons(smVersion)` to detect
// Script Manager version see 0 — they should fall
// back to Gestalt('scri') which Systemless handles in
// its main dispatcher.
// GetEnvirons ($AA89): Phantom trap word — verified absent from IM:VI Table C-1 + IM:V V-291 master dispatch tables. Stack: SP+0 selector(2), SP+2 result(2). Pops 2 + writes 0 INTEGER to result slot per IM:VI 14-30 ScriptUtil-GetEnvirons "Returns 0 if the script isn't installed or the verb isn't recognized". Apps that probe smVersion via GetEnvirons see 0 → they fall back to Gestalt('scri'). Canonical dispatch is via $A8B5 _ScriptUtil (selector 4 — same selector index but "GetEnvirons" not "GetScript").
(true, 0x289) => {
// GetEnvirons: FUNCTION (selector): Int
// SP+0 selector(2), SP+2 result(2).
let sp = cpu.read_reg(Register::A7);
bus.write_word(sp + 2, 0);
cpu.write_reg(Register::A7, sp + 2);
cpu.write_reg(Register::D0, 0);
Ok(())
}
// ChangedResource ($A9AA)
// Marks a resource as changed so its data and map entry will be
// written to disk when the resource file is updated.
// PROCEDURE ChangedResource(theResource: Handle);
// Inside Macintosh Volume IV, p. IV-16
//
// Regression coverage:
// tests::changedresource_sets_reschanged_attribute_for_unprotected_resource
// tests::changedresource_protected_resource_is_noop_with_resattrerr
// ChangedResource ($A9AA): Sets resChanged attribute on resource; resProtected returns resAttrErr per IM:IV IV-16 / MTB 1-88
(true, 0x1AA) => {
let sp = cpu.read_reg(Register::A7);
let handle = bus.read_long(sp);
if let Some((refnum, res_type, res_id)) = self.resource_record_for_handle(handle) {
let attrs = self.resource_attributes_for_handle(handle).unwrap_or(0);
// resProtected (bit 3) blocks the operation with resAttrErr
if (attrs & 0x0008) != 0 {
bus.write_word(0x0A60, Self::RES_ATTR_ERR as u16);
} else {
// Set the resChanged attribute (bit 1) on the resource
if let Some(resources) = self.resources.as_mut() {
if let Some(file) = resources.files.get_mut(&refnum) {
let entry = file.attrs.entry((res_type, res_id)).or_insert(0);
*entry |= Self::RES_CHANGED_ATTR as u8;
file.map_attrs |= Self::RES_MAP_CHANGED_ATTR;
}
}
bus.write_word(0x0A60, 0); // noErr
}
} else {
bus.write_word(0x0A60, Self::RES_NOT_FOUND as u16);
}
cpu.write_reg(Register::A7, sp + 4);
Ok(())
}
// SetResInfo ($A9A9)
// Changes the resource ID and name of a resource in the resource
// map in memory. The change is not written to disk until the file
// is updated (after ChangedResource is called).
// PROCEDURE SetResInfo(theResource: Handle; theID: INTEGER; name: Str255);
// Inside Macintosh Volume IV, p. IV-16
//
// Regression coverage:
// setresinfo_sets_resource_id_and_name
// setresinfo_nil_name_skips_name_change
// setresinfo_invalid_handle_sets_reserr
// setresinfo_protected_resource_is_noop_with_resattrerr
// setresinfo_pops_ten_bytes
// setresinfo_then_getresinfo_round_trip_writes_new_id_and_name
// setresinfo_does_not_mutate_other_resources_in_same_file
// setresinfo_nil_name_preserves_named_lookup_while_updating_id
// setresinfo_new_name_enables_new_getnamedresource_and_removes_old_name
// setresinfo_does_not_mutate_other_registers_or_caller_stack
// SetResInfo ($A9A9): Updates resource ID and name in memory map; resProtected returns resAttrErr per IM:IV IV-16 / MTB 1-82
(true, 0x1A9) => {
let sp = cpu.read_reg(Register::A7);
// Pascal LTR push order: theResource first (deepest), theID,
// then name (shallowest, at SP+0).
let name_ptr = bus.read_long(sp);
let new_id = bus.read_word(sp + 4) as i16;
let handle = bus.read_long(sp + 6);
if let Some((refnum, res_type, old_id)) = self.resource_record_for_handle(handle) {
let attrs = self.resource_attributes_for_handle(handle).unwrap_or(0);
// resProtected (bit 3) blocks the operation with resAttrErr
if (attrs & 0x0008) != 0 {
bus.write_word(0x0A60, Self::RES_ATTR_ERR as u16);
} else {
let home_refnum = self.resource_handle_files.get(&handle).copied();
// Update the resource ID in loaded_handles for the live
// home record only. System-reference shadow entries live
// in the current file's map and must not re-home the
// source handle.
if home_refnum == Some(refnum) {
if let Some(entry) = self.loaded_handles.get_mut(&handle) {
entry.2 = new_id;
}
}
if let Some(resources) = self.resources.as_mut() {
if let Some(file) = resources.files.get_mut(&refnum) {
// Move the loaded entry from old_id to new_id
if let Some(ptr) = file.loaded.remove(&(res_type, old_id)) {
file.loaded.insert((res_type, new_id), ptr);
}
// Move the attrs entry
if let Some(a) = file.attrs.remove(&(res_type, old_id)) {
file.attrs.insert((res_type, new_id), a);
}
// Update name if name_ptr != 0 (assembly-language note)
if name_ptr != 0 {
// Remove old named entry for this resource
file.named.retain(|(t, _), (id, _)| {
!(*t == res_type && *id == old_id)
});
// Read Pascal string from name_ptr
let name_bytes = bus.read_pstring(name_ptr);
if let Ok(name_str) = String::from_utf8(name_bytes) {
if !name_str.is_empty() {
let ptr_val = file
.loaded
.get(&(res_type, new_id))
.copied()
.unwrap_or(0);
file.named.insert(
(res_type, name_str),
(new_id, ptr_val),
);
}
}
} else {
// name_ptr == 0: update existing named entries to new ID
for ((t, _), (id, _)) in file.named.iter_mut() {
if *t == res_type && *id == old_id {
*id = new_id;
}
}
}
}
}
bus.write_word(0x0A60, 0); // noErr
}
} else {
bus.write_word(0x0A60, Self::RES_NOT_FOUND as u16);
}
cpu.write_reg(Register::A7, sp + 10);
Ok(())
}
// AddResource ($A9AB)
// Adds a resource reference to the current resource file for data
// already in memory. Sets the resChanged attribute so the data
// will be written when the file is updated.
// PROCEDURE AddResource(theData: Handle; theType: ResType;
// theID: INTEGER; name: Str255);
// Inside Macintosh: More Macintosh Toolbox 1993, 1-90.
//
// Regression coverage:
// addresource_adds_resource_to_current_file
// addresource_nil_handle_sets_reserr
// addresource_existing_resource_handle_sets_reserr
// addresource_pops_fourteen_bytes
// Golden coverage:
// a9ab_addresource_reserror
// AddResource ($A9AB): Adds data handle to current resource file with resChanged; validates NIL/existing handles per MMTB 1-90.
(true, 0x1AB) => {
let sp = cpu.read_reg(Register::A7);
// Pascal pushes the last formal nearest SP:
// SP+0 name: Str255 pointer
// SP+4 theID: Integer
// SP+6 theType: ResType
// SP+10 theData: Handle
let name_ptr = bus.read_long(sp);
let res_id = bus.read_word(sp + 4) as i16;
let res_type_raw = bus.read_long(sp + 6);
let res_type = res_type_raw.to_be_bytes();
let handle = bus.read_long(sp + 10);
const ADD_RES_FAILED: i16 = -194;
// Fail if handle is NIL or already a resource handle
if handle == 0 || self.loaded_handles.contains_key(&handle) {
bus.write_word(0x0A60, ADD_RES_FAILED as u16);
} else {
let ptr = bus.read_long(handle);
let refnum = self.current_resource_refnum();
// Register in loaded_handles and resource_handle_files
self.loaded_handles.insert(handle, (ptr, res_type, res_id));
self.resource_handle_files.insert(handle, refnum);
if let Some(resources) = self.resources.as_mut() {
if let Some(file) = resources.files.get_mut(&refnum) {
// Add to loaded map
file.loaded.insert((res_type, res_id), ptr);
// Set resChanged attribute
let entry = file.attrs.entry((res_type, res_id)).or_insert(0);
*entry |= Self::RES_CHANGED_ATTR as u8;
file.map_attrs |= Self::RES_MAP_CHANGED_ATTR;
// Add named entry if name_ptr != 0
if name_ptr != 0 {
let name_bytes = bus.read_pstring(name_ptr);
if let Ok(name_str) = String::from_utf8(name_bytes) {
if !name_str.is_empty() {
file.named.insert((res_type, name_str), (res_id, ptr));
}
}
}
}
}
bus.write_word(0x0A60, 0); // noErr
}
cpu.write_reg(Register::A7, sp + 14);
Ok(())
}
// AddReference ($A9AC)
// PROCEDURE AddReference(theResource: Handle; theID: INTEGER;
// name: Str255);
// Public MPW trap declaration: `_AddReference = $A9AC`
// (Resource Manager trap table / Traps.h).
// Inside Macintosh: Promotional Edition (1985), Resource
// Manager Programmer's Guide, "Modifying System References".
//
// Given a handle to a system resource, AddReference adds a
// system reference to the current resource file, gives it the
// supplied ID and name, and sets resChanged so the map will be
// written out on update. That current-file system reference is
// observable through GetResInfo / GetResAttrs on the source
// handle while HomeResFile remains the system file. The
// documented failure cases are:
// - the current resource file is the system file,
// - the handle is not a handle to a system resource,
// - the current file already contains a system reference to the
// same resource.
// ResError returns addRefFailed (-195) for these branches.
//
// Stack layout (Pascal left-to-right push order):
// SP+0 name pointer (Str255)
// SP+4 theID (INTEGER)
// SP+6 theResource (Handle)
// Pops 10 bytes. No function result.
(true, 0x1AC) => {
let sp = cpu.read_reg(Register::A7);
let name_ptr = bus.read_long(sp);
let new_id = bus.read_word(sp + 4) as i16;
let handle = bus.read_long(sp + 6);
let _ = self.add_resource_reference(bus, handle, new_id, name_ptr);
cpu.write_reg(Register::A7, sp + 10);
Ok(())
},
// WriteResource ($A9B0)
// Writes the resource data for the given resource to the resource
// file on disk. Does nothing (returning noErr) when resProtected
// is set or resChanged is not set. In Systemless's HLE, there is no
// physical resource file to write to, so this is a contract-
// conformant no-op that clears resChanged after "writing".
// PROCEDURE WriteResource(theResource: Handle);
// Inside Macintosh Volume I, I-124
//
// Regression coverage:
// writeresource_clears_changed_flag
// writeresource_invalid_handle_sets_reserr
// writeresource_protected_resource_is_noop
// writeresource_unchanged_resource_is_noop
// writeresource_pops_four_bytes
// WriteResource ($A9B0): Respects resProtected/resChanged flags; clears resChanged after write per IM:I I-124
(true, 0x1B0) => {
let sp = cpu.read_reg(Register::A7);
let handle = bus.read_long(sp);
if self.live_resource_identity_for_handle(handle).is_some() {
let attrs = self.resource_attributes_for_handle(handle).unwrap_or(0);
// resProtected (bit 3): do nothing, return noErr
// resChanged not set (bit 1): do nothing, return noErr
if (attrs & 0x0008) != 0 || (attrs & Self::RES_CHANGED_ATTR) == 0 {
bus.write_word(0x0A60, 0);
} else {
// In a real Mac, this would write data to disk.
// In Systemless HLE we just clear the resChanged flag
// to honor the contract.
let _ = self.write_resource_backing_if_changed(handle);
bus.write_word(0x0A60, 0);
}
} else {
bus.write_word(0x0A60, Self::RES_NOT_FOUND as u16);
}
cpu.write_reg(Register::A7, sp + 4);
Ok(())
}
// CreateResFile ($A9B1)
// PROCEDURE CreateResFile(fileName: Str255);
// More Macintosh Toolbox (1993), pp. 1-57 and 1-66;
// Inside Macintosh Volume I (1985), p. I-115.
//
// Semantics:
// - Creates a zero-length data fork plus empty resource fork when
// no matching file exists in the OpenRF search namespace.
// - If a data-fork file exists but has no resource fork, creates
// the empty resource fork.
// - If a file already has a non-empty resource fork map, does
// nothing and reports dupFNErr via ResErr.
// - If the data fork exists and the resource fork is present but
// zero-length, creates the empty resource map in place.
// - Pop 4 bytes (fileName pointer), no function result.
//
// Systemless HLE models this with `vfs` (data fork) and `vfs_rsrc`
// (resource fork). Search-path edge cases (root/System folder)
// are collapsed to the normalized VFS namespace.
(true, 0x1B1) => {
let sp = cpu.read_reg(Register::A7);
let name_ptr = bus.read_long(sp);
let name = if name_ptr != 0 {
String::from_utf8_lossy(&bus.read_pstring(name_ptr)).into_owned()
} else {
String::new()
};
let res_err: i16 = if name.is_empty() {
-37 // bdNamErr
} else {
let vfs_key = self
.find_vfs_rsrc_file(&name)
.or_else(|| self.find_vfs_file(&name))
.unwrap_or_else(|| super::TrapDispatcher::normalize_vfs_path(&name));
let existing_rsrc_has_map = self
.vfs_rsrc
.get(&vfs_key)
.map(|fork| !fork.is_empty())
.unwrap_or(false);
if existing_rsrc_has_map {
// MTb 1993 p. 1-57: existing resource fork with a
// resource map -> no-op + dupFNErr.
-48 // dupFNErr
} else {
// If a data fork already exists, add or initialise its
// zero-length resource fork map. Otherwise create both
// forks.
self.vfs.entry(vfs_key.clone()).or_default();
self.vfs_rsrc
.insert(vfs_key.clone(), Self::empty_resource_fork_bytes());
self.touch_vfs_entry(&vfs_key);
if let Some(ref dir) = self.output_dir {
let host_path = dir.join(&vfs_key);
if let Some(parent) = host_path.parent() {
let _ = std::fs::create_dir_all(parent);
}
let _ = std::fs::write(host_path, []);
}
0 // noErr
}
};
bus.write_word(0x0A60, res_err as u16);
cpu.write_reg(Register::A7, sp + 4);
Ok(())
}
// SystemEvent ($A9B2)
// Per IM:I I-441: "SystemEvent is called only by the
// Toolbox Event Manager function GetNextEvent when it
// receives an event, to determine whether the event
// should be handled by the application or by the system.
// If the given event should be handled by the
// application, SystemEvent returns FALSE; otherwise, it
// calls the appropriate system code to handle the event
// and returns TRUE."
//
// Per IM:I I-441 dispatch matrix:
// - null / mouse-down events: returns FALSE (lets app
// run FindWindow then optionally call SystemClick).
// - mouse-up / keyboard events: TRUE iff active window
// is DA-owned and DA can handle it; else FALSE.
// - activate / update events: TRUE iff event window is
// DA-owned and DA can handle it; else FALSE.
// Systemless's HLE has no DA-owned windows so EVERY path
// resolves to FALSE — apps correctly receive every event
// and perform their own dispatch.
// FUNCTION SystemEvent(theEvent: EventRecord): BOOLEAN;
// Inside Macintosh Volume I, I-441
//
// Stack: SP+0..15 theEvent EventRecord by VALUE (16
// bytes — IM:I I-251 layout: what(2) + message(4) +
// when(4) + where Point(4) + modifiers(2)), SP+16
// result BOOLEAN (2-byte FUNCTION result slot pre-pushed
// by caller). Pop 16; result at post-pop A7 = pre-call
// SP+16. Returns FALSE per IM:I I-441 "app handles event"
// path — corpus games' GetNextEvent → SystemEvent →
// app-event-loop sequence proceeds correctly.
//
// Manager classification corrected: was "Resource Manager
// — Toolbox Traps" (an artifact of where the arm landed
// in the dispatch chain) but per IM:I-441 chapter heading
// "Handling Events in Desk Accessories" this trap belongs
// to the Desk Accessory family — same family rationale
// block at toolbox.rs $A9B6 OpenDeskAcc applies.
// SystemEvent ($A9B2): Pops 16-byte EventRecord by VALUE per IM:I I-251 layout + writes FALSE to 2-byte BOOLEAN result slot at post-pop A7 per IM:I I-441 PROCEDURE sig; HLE has no DA-owned windows so FALSE matches the IM-documented "event should be handled by the application" path for every event class (null/mouse-down/mouse-up/keyboard/activate/update).
(true, 0x1B2) => {
let sp = cpu.read_reg(Register::A7);
// EventRecord = 16 bytes by value, result = 2 bytes
bus.write_word(sp + 16, 0); // FALSE
cpu.write_reg(Register::D0, 0);
cpu.write_reg(Register::A7, sp + 16);
Ok(())
}
// Gestalt family: _Gestalt ($A1AD), _NewGestalt ($A3AD),
// _ReplaceGestalt ($A5AD). All three OS traps share the
// low byte $AD and so reach the same dispatch arm — the
// dispatcher only forwards bits 0..7 for OS traps. We
// distinguish them by reading the original 16-bit trap
// word from `current_trap_word` (set by the dispatcher
// before each handler runs).
//
// Trap word table per Inside Macintosh Volume VI, p. 6-308.
// FUNCTION/Pascal signatures and register conventions per
// Inside Macintosh: Operating System Utilities 1994,
// 1-31..1-35.
//
// Gestalt ($A0AD): 24 documented selectors (sysv/evnt/cput/proc/mach/qd/qdrw/ram/fpu/mmu/snd/tmgr/alis/fs/fold/qtim/os/powr/appr/addr/sdev/stdf/help/vm); guest-installed selector functions (NewGestalt/ReplaceGestalt) are registered but not invokable from a trap handler
(false, 0xAD) => {
let selector = cpu.read_reg(Register::D0);
let sel = selector.to_be_bytes();
match self.current_trap_word {
// _NewGestalt ($A3AD)
//
// FUNCTION NewGestalt (selector: OSType;
// gestaltFunction:
// SelectorFunctionUUP): OSErr;
//
// Inside Macintosh: Operating System Utilities 1994,
// pp. 1-31..1-34 (description) and p. 1-50 (trap-word
// table).
//
// OS-bit FUNCTION (bit 11 clear) with register-only ABI
// per IM:OSUtils 1994 p. 1-32:
//
// Registers on entry:
// A0 Address of new selector function
// D0 Selector code (4-char OSType)
//
// Registers on exit:
// D0 Result code
//
// Result codes:
// noErr (0)
// memFullErr (-108) Ran out of memory
// gestaltDupSelectorErr (-5552) Selector already exists
// gestaltLocationErr (-5553) Function not in system heap
//
// MPW Universal Headers Gestalt.h:
// #pragma parameter __D0 NewGestalt(__D0, __A0)
// EXTERN_API(OSErr) NewGestalt(OSType selector,
// SelectorFunctionUPP gestaltFunction)
// ONEWORDINLINE(0xA3AD);
//
// Systemless HLE compromise: records the (selector → guest
// fn) tuple in `gestalt_registry`; rejects duplicates of
// any built-in or already-registered selector with
// `gestaltDupSelectorErr` (-5552). Does NOT validate
// system-heap residency, so stack-local function pointers
// that BasiliskII would reject with `gestaltLocationErr`
// (-5553) are accepted. Subsequent Gestalt queries of
// registry-only selectors still return
// `gestaltUndefSelectorErr` because guest function
// pointers are not invokable from a trap handler.
//
// BII-vs-Systemless divergence: the absolute OSErr is
// engines-divergent (BII enforces system-heap residency
// with gestaltLocationErr; Systemless HLE accepts any
// address). Both engines obey the documented register-
// only OS-bit FUNCTION calling convention.
//
// Strict bake fixture:
// a3ad_a5ad_newgestalt_replacegestalt_strict
// Engines-agree assertion witnessed:
// A3AD:newgestalt_register_only_calling_convention_preserves_stack
// In-Rust contract test (mirrors B1 + B2 of the bake):
// newgestalt_register_only_calling_convention_preserves_stack
0xA3AD => {
let already_known = is_builtin_gestalt_selector(&sel)
|| self.gestalt_registry.contains_key(&selector);
if already_known {
cpu.write_reg(Register::D0, GESTALT_DUP_SELECTOR_ERR);
} else {
let fn_ptr = cpu.read_reg(Register::A0);
self.gestalt_registry.insert(selector, fn_ptr);
cpu.write_reg(Register::D0, 0);
}
return Some(Ok(()));
}
// _ReplaceGestalt ($A5AD)
//
// FUNCTION ReplaceGestalt (selector: OSType;
// gestaltFunction:
// SelectorFunctionUUP;
// VAR oldGestaltFunction:
// SelectorFunctionUUP)
// : OSErr;
//
// Inside Macintosh: Operating System Utilities 1994,
// pp. 1-31..1-35.
//
// OS-bit FUNCTION (bit 11 clear) with register-only ABI
// per IM:OSUtils 1994 p. 1-35:
//
// Registers on entry:
// A0 Address of new selector function
// D0 Selector code (4-char OSType)
//
// Registers on exit:
// A0 Address of old selector function
// (undefined on error)
// D0 Result code
//
// Result codes:
// noErr (0)
// gestaltUndefSelectorErr (-5551) Undefined selector
// gestaltLocationErr (-5553) Function not in
// system heap
//
// Per IM:OSUtils 1994 p. 1-35: "If ReplaceGestalt
// returns an error of any type, then the value of
// oldGestaltFunction is undefined" — so on
// gestaltUndefSelectorErr we leave A0 unchanged.
//
// MPW Universal Headers Gestalt.h:
// #pragma parameter __D0 ReplaceGestalt(__D0, __A0, __A1)
// EXTERN_API(OSErr) ReplaceGestalt(OSType selector,
// SelectorFunctionUPP gestaltFunction,
// SelectorFunctionUPP *oldGestaltFunction)
// FOURWORDINLINE(0x2F09, 0xA5AD, 0x225F, 0x2288);
//
// The four-word glue saves A1 around the trap and writes
// the post-trap A0 register through *oldGestaltFunction;
// the bare trap word itself is still register-only.
//
// Systemless HLE compromise: swaps a previously-registered
// guest selector fn and returns the old pointer in A0;
// for built-in selectors there is no original guest fn
// to return so A0=0; unknown selectors yield
// gestaltUndefSelectorErr (-5551) with A0 preserved.
//
// Strict bake fixture:
// a3ad_a5ad_newgestalt_replacegestalt_strict
// Engines-agree assertion witnessed:
// A5AD:replacegestalt_register_only_calling_convention_preserves_stack
// In-Rust contract test (mirrors B3 + B4 of the bake):
// replacegestalt_register_only_calling_convention_preserves_stack
0xA5AD => {
let new_fn = cpu.read_reg(Register::A0);
if let Some(old_fn) = self.gestalt_registry.insert(selector, new_fn) {
cpu.write_reg(Register::A0, old_fn);
cpu.write_reg(Register::D0, 0);
} else if is_builtin_gestalt_selector(&sel) {
cpu.write_reg(Register::A0, 0);
cpu.write_reg(Register::D0, 0);
} else {
// Selector is genuinely unknown. Per IM the swap
// does not happen — unwind the speculative insert
// before returning the error so subsequent
// ReplaceGestalt/NewGestalt see a clean registry.
self.gestalt_registry.remove(&selector);
cpu.write_reg(Register::D0, GESTALT_UNDEF_SELECTOR_ERR);
}
return Some(Ok(()));
}
// _Gestalt ($A0AD/$A1AD): fall through to the query
// handler below.
_ => {}
}
let s = std::str::from_utf8(&sel).unwrap_or("????");
eprintln!(
"[GESTALT] selector='{}' (${:08X}) PC=${:08X}",
s,
selector,
cpu.read_reg(Register::PC)
);
match &sel {
// gestaltSystemVersion ('sysv') -> System 7.5.3
b"sysv" => {
cpu.write_reg(
Register::A0,
ORACLE_MACHINE_PROFILE.system_version_bcd as u32,
);
cpu.write_reg(Register::D0, 0);
}
// gestaltAppleEventsAttr ('evnt') -> AppleEvents present
b"evnt" => {
cpu.write_reg(Register::A0, 0x0001);
cpu.write_reg(Register::D0, 0);
}
// gestaltNativeCPUtype ('cput') -> 68040
b"cput" => {
cpu.write_reg(Register::A0, ORACLE_MACHINE_PROFILE.gestalt_native_cpu_type);
cpu.write_reg(Register::D0, 0);
}
// gestaltProcessorType ('proc') -> 68040
b"proc" => {
cpu.write_reg(Register::A0, ORACLE_MACHINE_PROFILE.gestalt_processor_type);
cpu.write_reg(Register::D0, 0);
}
// gestaltMachineType ('mach') -> Quadra 900
// Inside Macintosh: Operating System Utilities 1994, 1-58
b"mach" => {
cpu.write_reg(
Register::A0,
ORACLE_MACHINE_PROFILE.gestalt_machine_type as u32,
);
cpu.write_reg(Register::D0, 0);
}
// gestaltQuickdrawVersion ('qd ') -> 32-bit QD (0x0200)
b"qd " => {
cpu.write_reg(Register::A0, 0x0200);
cpu.write_reg(Register::D0, 0);
}
// gestaltQuickdrawFeatures ('qdrw') -> hasColor | hasDeepGWorlds
b"qdrw" => {
cpu.write_reg(Register::A0, 0x000F);
cpu.write_reg(Register::D0, 0);
}
// gestaltPhysicalRAMSize ('ram ') -> 32MB
b"ram " => {
cpu.write_reg(Register::A0, ORACLE_MACHINE_PROFILE.ram_size_bytes);
cpu.write_reg(Register::D0, 0);
}
// gestaltFPUType ('fpu ') -> 68040 FPU
b"fpu " => {
cpu.write_reg(Register::A0, ORACLE_MACHINE_PROFILE.gestalt_fpu_type);
cpu.write_reg(Register::D0, 0);
}
// gestaltMMUType ('mmu ') -> 68040 MMU
b"mmu " => {
cpu.write_reg(Register::A0, ORACLE_MACHINE_PROFILE.gestalt_mmu_type);
cpu.write_reg(Register::D0, 0);
}
// gestaltSoundAttr ('snd ') -> advertise a full late-68k
// color Mac sound profile:
// Bit 0: gestaltStereoCapability
// Bit 1: gestaltStereoMixing
// Bit 3: gestaltSoundIOMgrPresent
// Bit 4: gestaltBuiltInSoundOutput
// Bit 5: gestaltHasPCMMixerType
// Bit 6: gestaltHasDSPMixerType
// Bit 7: gestalt16BitSoundIO
// Bit 10: gestaltSndPlayDoubleBuffer
// Bit 11: gestaltMultiChannels
// Bit 12: gestalt16BitAudioSupport
// Sound 1994, 1-14.
//
// Earlier 0x0C83 subset omitted bits 3..6 and bit 12.
// Some Sound-Manager-3-aware titles refuse to start when
// those capability bits are missing.
b"snd " => {
cpu.write_reg(Register::A0, 0x1CFB);
cpu.write_reg(Register::D0, 0);
}
// gestaltTimeMgrVersion ('tmgr') -> revised Timer Manager
b"tmgr" => {
cpu.write_reg(Register::A0, 2);
cpu.write_reg(Register::D0, 0);
}
// gestaltDisplayMgrVers ('dplv') -> Display Manager
// 2.0.6, matching the BasiliskII System 7.5.3 oracle.
// Operating System Utilities 1994 lists 'dplv' as the
// Display Manager version selector. Abuse probes it before
// walking screen devices through DisplayDispatch.
b"dplv" => {
cpu.write_reg(Register::A0, 0x0002_0006);
cpu.write_reg(Register::D0, 0);
}
// gestaltDisplayMgrAttr ('dply') -> Display Manager attrs.
// Bit 0 is gestaltDisplayMgrPresent; BasiliskII's System
// 7.5.3 oracle returns bits 0..2 set for this profile.
b"dply" => {
cpu.write_reg(Register::A0, 0x0000_0007);
cpu.write_reg(Register::D0, 0);
}
// gestaltAliasMgrAttr ('alis') -> alias manager present
b"alis" => {
cpu.write_reg(Register::A0, 1);
cpu.write_reg(Register::D0, 0);
}
// gestaltFSAttr ('fs ') -> has FSSpec calls
b"fs " => {
cpu.write_reg(Register::A0, 1);
cpu.write_reg(Register::D0, 0);
}
// gestaltFindFolderAttr ('fold') -> FindFolder present
// Inside Macintosh Volume VI, 9-28
b"fold" => {
cpu.write_reg(Register::A0, 1);
cpu.write_reg(Register::D0, 0);
}
// gestaltQuickTime ('qtim') -> QuickTime version.
// Inside Macintosh: QuickTime 1993, p. 2-29. The
// response field (A0) is the QuickTime version
// packed as 0xMMMMNNRR where MMMM=major, NN=minor,
// RR=bug-fix. Report 0x06_00_00_00 (QuickTime 6.0
// — the latest classic-Mac release) so games that
// gate themselves on a present-and-recent QT
// (Souls in the System, Marathon Infinity) can
// proceed past their "requires QuickTime" alert
// and exercise the actual Movie Toolbox surface.
// Returning gestaltUndefSelectorErr previously
// halted those titles before any usable frame.
b"qtim" => {
cpu.write_reg(Register::A0, 0x0600_0000);
cpu.write_reg(Register::D0, 0); // noErr
}
// gestaltOSAttr ('os ') -> Mac OS attributes.
// Inside Macintosh: Operating System Utilities 1994,
// page 1-22. Bit assignments:
// 0: gestaltSysZoneGrowable
// 1: gestaltLaunchCanReturn
// 2: gestaltLaunchFullFileSpec
// 3: gestaltLaunchControl
// 4: gestaltTempMemSupport
// 5: gestaltRealTempMemory
// 6: gestaltTempMemTracked
// 7: gestaltIPCSupport
// 8: gestaltSysDebuggerSupport
//
// Steel Fighters and similar titles probe 'os '
// during init and ExitToShell via _Debugger if the
// selector returns gestaltUndefSelectorErr. Reporting
// a System 7-class capability set (bits 0-7) keeps
// them on the standard launch path. Bit 8
// (debuggerSupport) is left clear since Systemless does
// not host a guest-side debugger.
b"os " => {
cpu.write_reg(Register::A0, 0xFF);
cpu.write_reg(Register::D0, 0);
}
// gestaltPowerMgrAttr ('powr') -> no Power Manager.
// Inside Macintosh Volume VI, 31-9. Bit 0 = present.
// Returning 0 with noErr signals "selector recognized,
// not a portable Mac" — desktop-class titles probe
// this on launch and fall through to the desktop
// power path (no battery polling, no sleep hooks).
b"powr" => {
cpu.write_reg(Register::A0, 0);
cpu.write_reg(Register::D0, 0);
}
// gestaltAppearanceAttr ('appr') -> Appearance Manager
// present. Mac Toolbox: Appearance Manager (Apple, 1997),
// Gestalt selectors:
// Bit 0 = gestaltAppearanceExists
// Bit 1 = gestaltAppearanceCompatMode
// Some mid-90s titles (e.g. Meteor Storm) treat absence
// of the Appearance Manager as a hard failure and emit a
// misleading "Couldn't get the sound manager version"
// alert before exiting. Reporting bit 0 set says
// "Appearance Mgr is here"; the title's NewFeaturesDialog
// glue path then proceeds normally even though our
// DialogDispatch routes back through the standard
// Dialog Manager (no theming).
b"appr" => {
cpu.write_reg(Register::A0, 0x0001);
cpu.write_reg(Register::D0, 0);
}
// gestaltAddressingModeAttr ('addr') -> 32-bit clean.
// Inside Macintosh Volume VI, 28-9. Bit 0: 32-bit
// addressing currently active. Bit 1: 32-bit-clean
// system zone. Bit 2: machine is 32-bit capable.
// Apps like Bonkheads that hard-require 32-bit
// addressing read this and ExitToShell with a
// "needs 32-bit addressing" alert if bit 0 is clear.
b"addr" => {
cpu.write_reg(Register::A0, 0b111);
cpu.write_reg(Register::D0, 0);
}
// gestaltSoundDeviceAttr ('sdev') -> not present.
// Inside Macintosh Sound 1994, 1-15. Reports the
// attributes of a specific sound output device. With
// no device selected the spec-correct response is
// gestaltUndefSelectorErr, but several apps (e.g.
// Bonkheads) probe this without first selecting a
// device and crash on the error. Returning 0 (no
// attributes set, no error) lets them proceed.
b"sdev" => {
cpu.write_reg(Register::A0, 0);
cpu.write_reg(Register::D0, 0);
}
// gestaltStandardFileAttr ('stdf') -> Standard File Mgr 5.8 present.
// Inside Macintosh: More Macintosh Toolbox 1993, 1-86.
// Bit 0: gestaltStandardFile58 (StandardFile Mgr ≥5.8 features
// — CustomGetFile / CustomPutFile / StandardFileReply).
// Steel Fighters probes this and ExitToShells if D0 returns
// gestaltUndefSelectorErr. Reporting bit 0 set keeps the
// game on the standard launch path.
b"stdf" => {
cpu.write_reg(Register::A0, 1);
cpu.write_reg(Register::D0, 0);
}
// gestaltHelpMgrAttr ('help') -> Help Manager present.
// Inside Macintosh: More Macintosh Toolbox 1993, 11-22.
// Bit 0: gestaltHelpMgrPresent.
// Steel Fighters and similar mid-90s titles probe this in
// the same gestalt sweep that gates ExitToShell.
b"help" => {
cpu.write_reg(Register::A0, 1);
cpu.write_reg(Register::D0, 0);
}
// gestaltVMAttr ('vm ') -> Virtual Memory not present.
// Inside Macintosh: Memory 1992, 3-29. Bit 0:
// gestaltVMPresent (1 = VM in use). Systemless does not
// emulate VM (the entire heap is real RAM), so report
// 0 — but with noErr in D0 so probes don't ExitToShell.
b"vm " => {
cpu.write_reg(Register::A0, 0);
cpu.write_reg(Register::D0, 0);
}
_ => {
let s = std::str::from_utf8(&sel).unwrap_or("????");
eprintln!("[GESTALT] Unknown selector '{}' (${:08X})", s, selector);
cpu.write_reg(Register::D0, 0xFFFFEA51u32); // gestaltUndefSelectorErr
}
}
Ok(())
}
// ========== File Manager ==========
// PBOpen / HOpen ($A000 / $A200)
// PBOpen ($A000): Opens files from VFS, assigns refnum
(false, 0x00) => {
let pb = cpu.read_reg(Register::A0);
let name_ptr = bus.read_long(pb + 18);
let filename = Self::read_pb_filename(bus, name_ptr);
eprintln!("[TRAP] PBOpen(\"{}\")", filename);
// Clear ioRefNum upfront so the failure path produces a
// deterministic *refNum = 0 (IM:II-92 leaves this undefined
// on failure, but games that read *refNum after an error
// expect 0).
bus.write_word(pb + 24, 0);
// Look up in VFS (try exact match, then basename match)
if let Some(vfs_name) = self.find_vfs_file(&filename) {
let refnum = self.next_refnum;
self.next_refnum += 1;
self.open_files.insert(refnum, vfs_name.clone());
self.file_positions.insert(refnum, 0);
bus.write_word(pb + 24, refnum);
bus.write_word(pb + 16, 0); // noErr
cpu.write_reg(Register::D0, 0);
eprintln!("[TRAP] PBOpen -> refnum={} vfs=\"{}\"", refnum, vfs_name);
} else {
eprintln!("[TRAP] PBOpen: file not found in VFS");
bus.write_word(pb + 16, (-43i16) as u16); // fnfErr
cpu.write_reg(Register::D0, (-43i32) as u32);
}
Ok(())
}
// FSRead ($A002) - with position tracking
// FSRead ($A002): Reads from VFS data fork via refnum
(false, 0x02) => {
let pb = cpu.read_reg(Register::A0);
let ref_num = bus.read_word(pb + 24);
let buffer = bus.read_long(pb + 32);
let request_count = bus.read_long(pb + 36) as usize;
let pos_mode = bus.read_word(pb + 44);
let pos_offset = bus.read_long(pb + 46) as i32;
eprintln!(
"[TRAP] FSRead ref={} buf=${:08X} count={} posMode={} posOff={}",
ref_num, buffer, request_count, pos_mode, pos_offset
);
if let Some(filename) = self.open_files.get(&ref_num).cloned() {
if let Some(file_buf) = self.vfs.get(&filename) {
let file_len = file_buf.len();
// Determine read position based on posMode
let cur_pos = *self.file_positions.get(&ref_num).unwrap_or(&0);
let start = match pos_mode & 0x03 {
0 => cur_pos, // fsAtMark
1 => pos_offset as usize, // fsFromStart
2 => (file_len as i32 + pos_offset) as usize, // fsFromLEOF
3 => (cur_pos as i32 + pos_offset) as usize, // fsFromMark
_ => cur_pos,
};
let start = start.min(file_len);
let avail = file_len - start;
let bytes_read = request_count.min(avail);
bus.write_bytes(buffer, &file_buf[start..start + bytes_read]);
self.file_positions.insert(ref_num, start + bytes_read);
bus.write_long(pb + 40, bytes_read as u32);
bus.write_long(pb + 46, (start + bytes_read) as u32);
if bytes_read < request_count {
eprintln!(
"[TRAP] FSRead -> eofErr (read {} of {} from pos {})",
bytes_read, request_count, start
);
bus.write_word(pb + 16, (-39i16) as u16); // eofErr
cpu.write_reg(Register::D0, (-39i32) as u32);
} else {
bus.write_word(pb + 16, 0);
cpu.write_reg(Register::D0, 0);
}
} else {
eprintln!("[TRAP] FSRead -> fnfErr (file not in VFS)");
bus.write_word(pb + 16, (-43i16) as u16); // fnfErr
bus.write_long(pb + 40, 0);
cpu.write_reg(Register::D0, (-43i32) as u32);
}
} else {
eprintln!("[TRAP] FSRead -> rfNumErr (refnum {} not open)", ref_num);
bus.write_word(pb + 16, (-51i16) as u16); // rfNumErr
bus.write_long(pb + 40, 0);
cpu.write_reg(Register::D0, (-51i32) as u32);
}
Ok(())
}
// FSWrite ($A003)
// FSWrite ($A003): Writes to output_dir or VFS
(false, 0x03) => {
let pb = cpu.read_reg(Register::A0);
let ref_num = bus.read_word(pb + 24);
let buffer = bus.read_long(pb + 32);
let request_count = bus.read_long(pb + 36) as usize;
let pos_mode = bus.read_word(pb + 44);
let pos_offset = bus.read_long(pb + 46) as i32;
eprintln!(
"[TRAP] FSWrite ref={} buf=${:08X} count={} posMode={} posOff={}",
ref_num, buffer, request_count, pos_mode, pos_offset
);
let Some(filename) = self.open_files.get(&ref_num).cloned() else {
eprintln!("[TRAP] FSWrite -> rfNumErr (refnum {} not open)", ref_num);
bus.write_long(pb + 40, 0);
bus.write_word(pb + 16, (-51i16) as u16); // rfNumErr
cpu.write_reg(Register::D0, (-51i32) as u32);
return Some(Ok(()));
};
let (new_pos, host_sync_bytes) = {
let file_buf = self.vfs.entry(filename.clone()).or_default();
let file_len = file_buf.len();
let cur_pos = *self.file_positions.get(&ref_num).unwrap_or(&0);
let start = match pos_mode & 0x03 {
0 => cur_pos as i64, // fsAtMark
1 => pos_offset as i64, // fsFromStart
2 => file_len as i64 + pos_offset as i64, // fsFromLEOF
3 => cur_pos as i64 + pos_offset as i64, // fsFromMark
_ => cur_pos as i64,
}
.max(0) as usize;
if start > file_buf.len() {
file_buf.resize(start, 0);
}
let end = start.saturating_add(request_count);
if end > file_buf.len() {
file_buf.resize(end, 0);
}
bus.read_bytes_into(buffer, &mut file_buf[start..start + request_count]);
let sync = if self.output_dir.is_some() {
Some(file_buf.clone())
} else {
None
};
(end, sync)
};
self.file_positions.insert(ref_num, new_pos);
bus.write_long(pb + 40, request_count as u32);
bus.write_long(pb + 46, new_pos as u32);
if let (Some(dir), Some(bytes)) = (&self.output_dir, host_sync_bytes) {
let host_path = dir.join(&filename);
if let Some(parent) = host_path.parent() {
let _ = std::fs::create_dir_all(parent);
}
if let Err(e) = std::fs::write(&host_path, bytes) {
eprintln!(
"[FSWrite] failed to sync {} to host ({}): {}",
filename,
host_path.display(),
e
);
}
}
self.touch_vfs_entry(&filename);
// Resource forks live in self.vfs_rsrc; PBOpenRF mirrors the
// bytes into self.vfs under "__rsrc__<name>" so FSRead/FSWrite
// share the data-fork code path. Mirror writes back so a later
// OpenRFPerm/PBOpenRF reads the latest data (Mars Rising's
// installer copies its rsrc fork to a temp file then re-opens it).
if let Some(real_name) = filename.strip_prefix("__rsrc__") {
if let Some(buf) = self.vfs.get(&filename).cloned() {
self.vfs_rsrc.insert(real_name.to_string(), buf);
}
}
bus.write_word(pb + 16, 0);
cpu.write_reg(Register::D0, 0);
Ok(())
}
// FSClose ($A001)
// FUNCTION FSClose(refNum: INTEGER): OSErr;
// Inside Macintosh Volume II, II-92. Result codes:
// noErr (0) — file closed
// rfNumErr (-51) — reference number specifies a nonexistent
// access path (sibling of FSRead / FSWrite
// / PBGetEOF, which all validate refnum)
// FSClose ($A001): Removes from open_files table
(false, 0x01) => {
let pb = cpu.read_reg(Register::A0);
let ref_num = bus.read_word(pb + 24);
let err: i16 = if self.open_files.remove(&ref_num).is_some() {
self.file_positions.remove(&ref_num);
self.write_refnums.remove(&ref_num);
0
} else {
-51
};
bus.write_word(pb + 16, err as u16);
cpu.write_reg(Register::D0, err as i32 as u32);
Ok(())
}
// PBGetVol / PBHGetVol ($A014 / $A214)
// Returns the default volume reference number (a WDRefNum for the app folder).
// FUNCTION PBGetVol (paramBlock: ParmBlkPtr; async: BOOLEAN): OSErr;
// FUNCTION PBHGetVol (paramBlock: WDPBPtr; async: BOOLEAN): OSErr;
// PBHGetVol additionally returns ioWDDirID (directory ID) at offset 48.
// Inside Macintosh Volume IV, IV-96 / Files 1992, 2-160
// PBHGetVol subsection begins IM:Files 1992 line 8305; trap macro
// `_HGetVol` per IM:Files line 8331 + table line 14488. The OS-trap
// dispatcher masks `trap & 0x00FF`, so $A214 PBHGetVol and $A014
// PBGetVol land on the same low byte and share this arm.
//
// Regression coverage:
// pb_vol ($A014 non-H variant)
// pbh_get_set_vol ($A214 HFS variant — trace-mode
// histogram bucket $214 pins
// trap-word dispatch via
// poisoned-ioResult indicator)
// trap-doc: $A014 | PBGetVol | Partial | File Manager & Gestalt — OS Traps | Fills ioVRefNum (-1) and ioNamePtr from boot volume (IM:Files 1992, 2-162)
// PBHGetVol ($A214): HFS variant aliased onto $A014
(false, 0x14) => {
let pb = cpu.read_reg(Register::A0);
bus.write_word(pb + 22, self.app_wd_refnum as u16);
let name_ptr = bus.read_long(pb + 18);
if name_ptr != 0 {
Self::write_pstring(bus, name_ptr, super::TrapDispatcher::boot_volume_name());
}
// HGetVol fields: ioWDProcID and ioWDVRefNum and ioWDDirID
// Always populate these so both PBGetVol and HGetVol callers work.
bus.write_long(pb + 28, 0); // ioWDProcID
bus.write_word(pb + 32, super::TrapDispatcher::boot_volume_ref_num_u16()); // ioWDVRefNum
bus.write_long(pb + 48, self.default_dir_id); // ioWDDirID
bus.write_word(pb + 16, 0); // noErr
cpu.write_reg(Register::D0, 0);
Ok(())
}
// PBGetVInfo / PBHGetVInfo ($A007 / $A207)
// FUNCTION PBGetVInfo (paramBlock: ParmBlkPtr; async: Boolean): OSErr;
// FUNCTION PBHGetVInfo (paramBlock: HParmBlkPtr; async: Boolean): OSErr;
// Inside Macintosh Volume II, II-104; HFS variant Files 1992,
// 2-144 (subsection begins line 8018; trap macro `_HGetVolInfo`
// per line 8077).
// The OS-trap dispatcher masks `trap & 0x00FF`, so $A207
// PBHGetVInfo and $A007 PBGetVInfo land on the same low
// byte and share this arm.
//
// Regression coverage:
// pb_vinfo ($A007 non-H variant)
// pbh_get_vinfo ($A207 HFS variant — trace-mode
// histogram bucket $207 pins
// trap-word dispatch via
// poisoned-ioResult indicator)
// PBGetVInfo ($A007): Returns hardcoded volume info, fake allocation block sizes
// PBHGetVInfo ($A207): HFS variant aliased onto $A007; BasiliskII-proven via pbh_get_vinfo
(false, 0x07) => {
let pb = cpu.read_reg(Register::A0);
let name_ptr = bus.read_long(pb + 18);
if name_ptr != 0 {
Self::write_pstring(bus, name_ptr, super::TrapDispatcher::boot_volume_name());
}
bus.write_word(pb + 22, super::TrapDispatcher::boot_volume_ref_num_u16());
bus.write_long(pb + 30, 0); // ioVCrDate
bus.write_long(pb + 34, 0); // ioVLsMod
bus.write_word(pb + 38, 0); // ioVAtrb
bus.write_word(pb + 40, 100); // ioVNmFls (files on volume)
bus.write_word(pb + 42, 0); // ioVBitMap
bus.write_word(pb + 44, 0); // ioAllocPtr
bus.write_word(pb + 46, 1024); // ioVNmAlBlks
bus.write_long(pb + 48, 512); // ioVAlBlkSiz
bus.write_long(pb + 52, 512 * 1024); // ioVClpSiz
bus.write_word(pb + 56, 0); // ioAlBlSt
bus.write_long(pb + 58, 0); // ioVNxtCNID
bus.write_word(pb + 62, 512); // ioVFrBlk (free blocks)
bus.write_word(pb + 16, 0); // noErr
cpu.write_reg(Register::D0, 0);
Ok(())
}
// PBGetEOF ($A011)
// PBGetEOF ($A011): Returns file length from VFS
(false, 0x11) => {
let pb = cpu.read_reg(Register::A0);
let ref_num = bus.read_word(pb + 24);
if let Some(filename) = self.open_files.get(&ref_num) {
if let Some(file_buf) = self.vfs.get(filename) {
bus.write_long(pb + 28, file_buf.len() as u32); // ioMisc = logical EOF
bus.write_word(pb + 16, 0);
cpu.write_reg(Register::D0, 0);
} else {
bus.write_word(pb + 16, (-43i16) as u16);
cpu.write_reg(Register::D0, (-43i32) as u32);
}
} else {
bus.write_word(pb + 16, (-51i16) as u16);
cpu.write_reg(Register::D0, (-51i32) as u32);
}
Ok(())
}
// PBSetFPos ($A044)
// Sets the file mark (position) for an open file.
// FUNCTION PBSetFPos(paramBlock: ParmBlkPtr; async: BOOLEAN): OSErr;
// Files 1992, 2-214
// PBSetFPos ($A044): Sets file mark position; supports fsAtMark, fsFromStart, fsFromLEOF, fsFromMark
(false, 0x44) => {
let pb = cpu.read_reg(Register::A0);
let ref_num = bus.read_word(pb + 24);
let pos_mode = bus.read_word(pb + 44) & 0x03;
let pos_offset = bus.read_long(pb + 46) as i32;
if let Some(filename) = self.open_files.get(&ref_num).cloned() {
let file_len = self.vfs.get(&filename).map(|f| f.len()).unwrap_or(0);
let cur_pos = *self.file_positions.get(&ref_num).unwrap_or(&0);
let new_pos = match pos_mode {
0 => cur_pos, // fsAtMark
1 => pos_offset as usize, // fsFromStart
2 => (file_len as i32 + pos_offset) as usize, // fsFromLEOF
3 => (cur_pos as i32 + pos_offset) as usize, // fsFromMark
_ => cur_pos,
};
self.file_positions.insert(ref_num, new_pos);
bus.write_long(pb + 46, new_pos as u32);
bus.write_word(pb + 16, 0);
cpu.write_reg(Register::D0, 0);
} else {
bus.write_word(pb + 16, (-51i16) as u16); // rfNumErr
cpu.write_reg(Register::D0, (-51i32) as u32);
}
Ok(())
}
// PBFlushFile ($A045)
// Per IM:II II-114: "Writes the contents of the access
// path buffer indicated by the file reference number
// ioRefNum to the volume, and updates the file's entry
// in the file directory."
// FUNCTION PBFlushFile (paramBlock: ParmBlkPtr;
// async: BOOLEAN): OSErr;
// Inside Macintosh Volume II, II-114
//
// Register convention (OS trap): A0 = paramBlock ptr;
// result in D0 + ioResult slot at pb+16.
//
// Result codes per IM:II II-114:
// noErr (0) — buffer flushed
// rfNumErr (-51) — reference number specifies a nonexistent
// access path
//
// ## Status: Stub → Partial promotion
//
// Systemless's VFS has no per-file write-back buffer
// (all FSWrite goes directly to the in-memory VFS
// map; no deferred writes that need flushing). So the
// "flush buffer to volume" semantic is trivially
// satisfied. BUT the impl DOES validate the refnum
// against self.open_files and returns the IM-correct
// rfNumErr (-51) for a bad refnum — that's substantive
// refnum-tracking behaviour, not a no-op stub. The
// prior trap-doc Status was "Stub" with terse Notes
// "Returns noErr" which lied about the refnum
// validation. Promoted Stub -> Partial during the
// stub-with-substantive-body audit; same status issue as
// PlotIcon $A94B / SetItemCmd $A84F / DisposPalette $AA93 /
// UpdtControl $A953 / Draw1Control $A96D.
// PBFlushFile ($A045): Validates ioRefNum (pb+24) against self.open_files + returns rfNumErr (-51) for bad refnum OR noErr (0) for valid; writes result to ioResult (pb+16) AND D0 per IM:II II-114 result-code table. VFS has no per-file write-back buffer so the noErr path is a successful no-op (FSWrite goes directly to the in-memory VFS map).
(false, 0x45) => {
let pb = cpu.read_reg(Register::A0);
let ref_num = bus.read_word(pb + 24);
let err: i16 = if self.open_files.contains_key(&ref_num) {
0
} else {
-51
};
bus.write_word(pb + 16, err as u16);
cpu.write_reg(Register::D0, err as i32 as u32);
Ok(())
}
// PBGetFPos ($A018)
// FUNCTION PBGetFPos (paramBlock: ParmBlkPtr; async: BOOLEAN): OSErr;
// Inside Macintosh Volume II, II-117. Result codes:
// noErr (0); rfNumErr (-51) for a bad reference number.
// PBGetFPos ($A018): Returns current file position
(false, 0x18) => {
let pb = cpu.read_reg(Register::A0);
let ref_num = bus.read_word(pb + 24);
if let Some(pos) = self.file_positions.get(&ref_num).copied() {
bus.write_long(pb + 46, pos as u32); // ioPosOffset
bus.write_word(pb + 44, 0); // ioPosMode
bus.write_word(pb + 16, 0);
cpu.write_reg(Register::D0, 0);
} else {
bus.write_long(pb + 46, 0);
bus.write_word(pb + 44, 0);
bus.write_word(pb + 16, (-51i16) as u16);
cpu.write_reg(Register::D0, (-51i32) as u32);
}
Ok(())
}
// PBFlushVol ($A013)
// Writes the contents of the volume buffer to the volume.
// FUNCTION PBFlushVol (paramBlock: ParmBlkPtr; async: BOOLEAN): OSErr;
// IM:Files 1992, 2-142. Result codes include noErr (0) and
// nsvErr (-35) when no such volume exists.
// trap-doc: $A013 | PBFlushVol | Partial | File Manager & Gestalt — OS Traps | No-op flush for known HLE volume specs; nsvErr for unrecognised vRefNum (IM:Files 1992, 2-142)
(false, 0x13) => {
let pb = cpu.read_reg(Register::A0);
let name = Self::read_pb_filename(bus, bus.read_long(pb + 18));
let vref = bus.read_word(pb + 22) as i16;
let known_by_vref = vref == 0
|| vref == Self::boot_volume_ref_num()
|| self.working_directories.contains_key(&vref);
let known_by_name = !name.is_empty()
&& name.eq_ignore_ascii_case(super::TrapDispatcher::boot_volume_name());
let err: i16 = if known_by_vref || known_by_name {
0
} else {
-35
};
bus.write_word(pb + 16, err as u16);
cpu.write_reg(Register::D0, err as i32 as u32);
Ok(())
}
// PBUnmountVol ($A00E) / PBUnmountVolImmed ($A20E)
// Unmounts a volume.
// FUNCTION PBUnmountVol (paramBlock: ParmBlkPtr): OSErr;
// FUNCTION PBUnmountVolImmed (paramBlock: ParmBlkPtr): OSErr;
// Inside Macintosh: Files (1992), p. 2-148 (PBUnmountVol).
//
// The $A20E variant is declared in MPW Universal Headers
// Files.h as `PBUnmountVolImmed(ParmBlkPtr)` and is not
// separately documented in IM:Files 1992. Its MPW Traps.h
// macro name is `_HUnmountVol` (the historical name used
// in the Systemless catalog row "PBHUnmountVol"). Both trap
// words take ParmBlkPtr — not HParmBlkPtr — and reach this
// arm via the OS-trap dispatcher's `trap & 0x00FF` mask.
// The "Immed" variant differs from PBUnmountVol in that it
// does not call PBFlushVol before unmounting.
//
// Register convention (OS-bit FUNCTION; IM:Files 1992
// p. 2-148 + IM:II 1985 p. II-114):
// A0 entry: ParmBlkPtr (paramBlock)
// D0 exit: OSErr result code (also mirrored into
// pb.ioResult at pb+16 per the basic File
// Manager parameter block dispatcher
// convention, IM:II 1985 p. II-114)
//
// Parameter block fields used (IM:II 1985 p. II-178 IOParam
// layout):
// → ioCompletion (ProcPtr) — NIL for synchronous form
// ← ioResult (OSErr) — result code (mirrors D0)
// → ioNamePtr (StringPtr) — volume name (NIL means
// use ioVRefNum)
// → ioVRefNum (Integer) — volume reference number
//
// Documented result codes (IM:Files 1992 p. 2-148):
// noErr 0 No error
// nsvErr -35 No such volume
// ioErr -36 I/O error
// bdNamErr -37 Bad filename
// fBsyErr -47 File(s) still open on the volume
// paramErr -50 Bad parameter
//
// MPW Universal Headers (Files.h):
// #pragma parameter __D0 PBUnmountVol(__A0)
// EXTERN_API(OSErr) PBUnmountVol(ParmBlkPtr paramBlock)
// ONEWORDINLINE(0xA00E);
// #pragma parameter __D0 PBUnmountVolImmed(__A0)
// EXTERN_API(OSErr) PBUnmountVolImmed(ParmBlkPtr paramBlock)
// ONEWORDINLINE(0xA20E);
//
// Systemless HLE compromise: the single-volume VFS has no
// volume table to unmount from; the boot volume is
// permanently mounted and any other vRefNum is silently
// a no-op. The trap collapses to writing noErr (0) into
// both D0 and pb.ioResult unconditionally. BasiliskII
// dispatches the real ROM PB trap and is expected to
// return nsvErr (-35) or paramErr (-50) for an ioVRefNum
// that does not appear in the VCB queue. The absolute
// OSErr value is therefore engines-divergent; the engines-
// agree subset is (a) the dispatcher convention writing
// the same OSErr into both D0 and ioResult, and (b) the
// register-only OS-bit FUNCTION calling convention
// preserving A7.
//
// Strict bake: `a00e_a20e_pbunmountvol_pbunmountvolimmed_strict`
// witnesses all four engines-agree assertions:
// A00E:pbunmountvol_writes_same_oserr_to_d0_and_ioresult
// A00E:pbunmountvol_register_only_calling_convention_preserves_stack
// A20E:pbunmountvolimmed_writes_same_oserr_to_d0_and_ioresult
// A20E:pbunmountvolimmed_register_only_calling_convention_preserves_stack
//
// Contract tests: src/trap/resource.rs mod tests
// pbunmountvol_writes_same_oserr_to_d0_and_ioresult_preserving_stack
(false, 0x0E) => {
let pb = cpu.read_reg(Register::A0);
bus.write_word(pb + 16, 0); // noErr
cpu.write_reg(Register::D0, 0);
Ok(())
}
// -----------------------------------------------------------------
// Volume / I/O-queue family — collapse to noErr no-ops in Systemless's
// single-volume, synchronous-I/O HLE.
//
// These traps mediate state that the HLE does not model:
// • PBKillIO ($A006) — abort I/O on a driver refnum. There is
// no async I/O queue to walk.
// • PBMountVol ($A00F) — mount a volume by drive number. A
// single VFS volume is permanently mounted; the trap rewrites
// `ioVRefNum` with the boot vRefNum and returns noErr.
// • PBAllocate ($A010) / PBAllocContig ($A210) — preallocate
// blocks on a file fork. The unbounded heap-backed VFS has no
// fragmentation, so every request fully succeeds.
// • FInitQueue ($A016) — clear the file I/O queue. There is no
// queue; PROCEDURE returns via D0 = noErr.
// • PBEject ($A017) — eject removable media. None exists.
// • PBOffLine ($A035) — take a volume offline. The boot
// volume cannot be brought offline.
// • PBSetFVers ($A043) — set MFS-era file version number. HFS
// files are always version 0; the trap is a documented HFS
// no-op (IM:II II-117 warns nonzero versions break the
// Resource Manager / Segment Loader).
// • AddDrive ($A04E) — append a DrvQEl to the drive queue.
// There is no drive queue.
// PBKillIO ($A006)
// Terminates the active I/O request and removes all pending I/O
// requests for the driver whose reference number is in the
// ioRefNum field of the parameter block.
// FUNCTION PBKillIO (paramBlock: ParmBlkPtr; async: BOOLEAN): OSErr;
// Inside Macintosh Volume II (1985), p. II-187 + Inside
// Macintosh: Devices (1994), pp. 1-45 to 1-46.
//
// Register convention (OS-bit FUNCTION; IM:II 1985 p. II-187
// trap macro `_KillIO`):
// A0 entry: ParmBlkPtr (paramBlock)
// D0 exit: OSErr result code (also mirrored into pb.ioResult
// at pb+16 per Device Manager dispatcher convention,
// IM:II 1985 p. II-114; applies to the entire $A0xx
// PB family)
//
// Parameter block fields used (IM:II 1985 p. II-178 IOParam
// layout):
// → ioCompletion (ProcPtr) — completion routine (NIL for
// the synchronous form)
// ← ioResult (OSErr) — result code (mirrors D0)
// → ioRefNum (Integer) — driver reference number
//
// Documented behavior (IM:II 1985 p. II-187): the abort applies
// to the driver named by ioRefNum; the active call's completion
// routine, and the completion routines of any pending calls,
// execute with the result code abortErr (-27). The function
// result itself is whatever the driver's KillIO routine returns.
//
// MPW Universal Headers (Devices.h):
// #pragma parameter __D0 PBKillIOSync(__A0)
// EXTERN_API(OSErr) PBKillIOSync(ParmBlkPtr paramBlock)
// ONEWORDINLINE(0xA006);
// #define PBKillIO(pb, async) ((async) ? PBKillIOAsync(pb)
// : PBKillIOSync(pb))
//
// Systemless HLE compromise: the synchronous-I/O HLE has no async
// I/O queue to walk and no driver-side KillIO routines to
// invoke, so the trap collapses to a no-op that writes noErr
// (0) into both D0 and pb.ioResult. BasiliskII dispatches the
// real ROM PB trap and is expected to return whatever the
// unit-table lookup produces for a refNum that does not map
// to an installed driver (typically badUnitErr -21 or
// unitEmptyErr -22) or whatever the driver's KillIO routine
// returns for a real one. The absolute return value is
// therefore engines-divergent; the engines-agree subset is
// (a) the dispatcher convention writing the same OSErr into
// both D0 and ioResult, and (b) the register-only OS-bit
// FUNCTION calling convention preserving A7.
//
// Strict bake: `a006_pbkillio_strict` witnesses both engines-
// agree assertions:
// A006:pbkillio_writes_same_oserr_to_d0_and_ioresult
// A006:pbkillio_register_only_calling_convention_preserves_stack
(false, 0x06) => {
let pb = cpu.read_reg(Register::A0);
bus.write_word(pb + 16, 0); // noErr → pb.ioResult
cpu.write_reg(Register::D0, 0); // noErr → D0
Ok(())
}
// PBMountVol ($A00F)
// Mounts the volume in the drive supplied via ioVRefNum (input
// drive number) and rewrites ioVRefNum with the assigned volume
// reference number (output).
// FUNCTION PBMountVol (paramBlock: ParmBlkPtr): OSErr;
// Inside Macintosh: Files (1992), p. 2-139.
//
// Register convention (OS-bit FUNCTION; IM:Files 1992 p. 2-139
// trap macro `_MountVol`):
// A0 entry: ParmBlkPtr (paramBlock)
// D0 exit: OSErr result code (also mirrored into pb.ioResult
// at pb+16 per File Manager dispatcher convention,
// IM:II 1985 p. II-114; applies to the entire $A0xx
// PB family)
//
// Parameter block fields used (IM:II 1985 p. II-178 IOParam
// layout):
// → ioCompletion (ProcPtr) — NIL (PBMountVol is always
// synchronous per IM:Files
// 1992 p. 2-139)
// ← ioResult (OSErr) — result code (mirrors D0)
// ↔ ioVRefNum (Integer) — input: drive number;
// output: assigned vRefNum
//
// Documented result codes (IM:Files 1992 p. 2-140):
// noErr (0) — success
// ioErr (-36) — I/O error
// tmfoErr (-42) — too many files open
// paramErr (-50) — bad drive number
//
// MPW Universal Headers (Files.h):
// #pragma parameter __D0 PBMountVol(__A0)
// EXTERN_API(OSErr) PBMountVol(ParmBlkPtr paramBlock)
// ONEWORDINLINE(0xA00F);
//
// Systemless HLE behavior: the single-volume VFS collapses any
// mount request to the permanently-mounted boot volume. The
// trap rewrites ioVRefNum with BOOT_VOLUME_REF_NUM (-1) and
// writes noErr (0) into both D0 and pb.ioResult. BasiliskII
// dispatches the real ROM PB trap and is expected to return
// paramErr (-50) for a bogus drive number that does not map
// to any installed drive queue entry. The absolute OSErr
// value (and the rewritten ioVRefNum) are therefore engines-
// divergent; the engines-agree subset is (a) the dispatcher
// convention writing the same OSErr into both D0 and ioResult,
// and (b) the register-only OS-bit FUNCTION calling convention
// preserving A7.
//
// Strict bake: `a00f_pbmountvol_strict` witnesses both engines-
// agree assertions:
// A00F:pbmountvol_writes_same_oserr_to_d0_and_ioresult
// A00F:pbmountvol_register_only_calling_convention_preserves_stack
(false, 0x0F) => {
let pb = cpu.read_reg(Register::A0);
bus.write_word(pb + 22, super::TrapDispatcher::boot_volume_ref_num_u16());
bus.write_word(pb + 16, 0); // noErr
cpu.write_reg(Register::D0, 0);
Ok(())
}
// PBAllocate ($A010) / PBAllocContig ($A210)
// Preallocates ioReqCount bytes on the open file referenced by
// ioRefNum. ioActCount on output is the bytes actually allocated
// (rounded up to allocation-block size on real Mac; Systemless
// matches the request exactly).
// FUNCTION PBAllocate (paramBlock: ParmBlkPtr; async: Boolean): OSErr;
// FUNCTION PBAllocContig (paramBlock: ParmBlkPtr; async: Boolean): OSErr;
// Inside Macintosh: Files (1992), pp. 2-130 to 2-131. The HFS
// variant ($A210) shares this arm via the OS-trap low-byte mask;
// both behave identically in Systemless's flat unbounded VFS where
// contiguous-vs-best-fit is moot.
//
// Per IM:Files 1992 p. 2-130 the trap word $A010 is an OS-bit
// FUNCTION (bit 11 of the trap word is clear) with a strict
// register-only calling convention:
//
// Entry: A0 = ParmBlkPtr (pointer to the I/O parameter
// block; see IM:Files 1992 p. 2-130
// for the field layout — ioCompletion
// at +12, ioResult at +16, ioRefNum
// at +24, ioReqCount at +36, ioActCount
// at +40)
// Exit: D0 = OSErr (mirrored into pb.ioResult at pb+16
// per the File Manager dispatcher
// convention, IM:II 1985 p. II-114)
//
// Documented result codes (IM:Files 1992, pp. 2-130 to 2-131):
// noErr 0 No error
// ioErr -36 I/O error
// dskFulErr-34 Disk full
// fnfErr -43 File not found
// wPrErr -44 Hardware volume lock
// fLckdErr -45 File is locked
// vLckdErr -46 Software volume lock
// rfNumErr -51 Bad reference number
//
// MPW Universal Headers `Files.h` declares the trap as:
// #pragma parameter __D0 PBAllocateSync(__A0)
// EXTERN_API(OSErr) PBAllocateSync(ParmBlkPtr paramBlock)
// ONEWORDINLINE(0xA010);
// #define PBAllocate(pb, async) \
// ((async) ? PBAllocateAsync(pb) : PBAllocateSync(pb))
//
// Systemless HLE compromise: the unbounded-VFS HLE skips refnum
// validation and treats every PBAllocate as a success that
// writes ioActCount = ioReqCount (the requested byte count)
// and noErr (0) to both D0 and pb.ioResult. There is no real
// file table or allocation-block fragmentation to validate
// against, so any caller request succeeds. BasiliskII System
// 7.5.3 ROM dispatches the real PB trap and is expected to
// return rfNumErr (-51) for ioRefNum 9999 since no such open
// file exists in the BasiliskII session.
//
// Engines-agree subset: regardless of the absolute OSErr value,
// both engines obey the File Manager dispatcher convention
// (IM:II 1985 p. II-114) writing the SAME value to BOTH D0
// and pb.ioResult at pb+16; both engines also preserve A7
// across the call since the OS-bit FUNCTION ABI takes no
// Pascal stack frame.
//
// Catalogue proofs:
// a010_pballocate_strict witnesses both
// engines-agree subset assertions for $A010 with a pre-
// poisoned ioResult sentinel (0x3FFF) and a bogus ioRefNum
// (9999): D0 == ioResult after the call AND ioResult !=
// pre-poison sentinel; sp_pre == sp_post.
// a210_pballoccontig_strict witnesses
// the same engines-agree subset for $A210 PBAllocContig
// against the shared HLE arm; the contiguous-allocation
// variant differs from PBAllocate only in that the real
// Apple ROM returns dskFulErr (-34) on contiguous-fit
// failure rather than performing a partial allocation,
// but both traps obey the same dispatcher convention so
// the bakeable witnesses are identical.
// Contract tests: see `pballocate_nominal_call_returns_noerr_and_sets_ioactcount`,
// `pballocate_writes_ioresult_noerr_when_paramblock_present`,
// `pballocate_writes_same_oserr_to_d0_and_ioresult_preserving_stack`,
// `pballoccontig_nominal_call_returns_noerr_and_sets_ioactcount`,
// `pballoccontig_overwrites_ioresult_field_with_function_result`,
// and `pballoccontig_writes_same_oserr_to_d0_and_ioresult_preserving_stack`
// in this file's tests module for the in-Rust witnesses.
(false, 0x10) => {
let pb = cpu.read_reg(Register::A0);
let req_count = bus.read_long(pb + 36); // ioReqCount
bus.write_long(pb + 40, req_count); // ioActCount
bus.write_word(pb + 16, 0); // noErr
cpu.write_reg(Register::D0, 0);
Ok(())
}
// FInitQueue ($A016)
// Clears all queued File Manager calls except the current one.
// PROCEDURE FInitQueue;
// Inside Macintosh Volume II (1985), p. II-103; Inside
// Macintosh Volume IV (1986), p. IV-128.
//
// Per IM:II 1985 p. II-103 FInitQueue is a parameterless
// PROCEDURE that clears all queued File Manager calls
// except the one currently in progress. The trap word
// $A016 is OS-bit (bit 11 of the trap word is clear); the
// calling convention is register-only with no inputs and
// no Pascal FUNCTION result slot.
//
// MPW Universal Headers `Files.h` declares the trap as:
// EXTERN_API(void) FInitQueue(void) ONEWORDINLINE(0xA016);
//
// Systemless HLE compromise: the File Manager I/O queue is
// permanently empty in the single-volume, synchronous-I/O
// HLE (all File Manager calls complete inline before
// returning to the caller), so FInitQueue has nothing to
// clear. The HLE writes D0=0 (noErr, by Memory Manager
// dispatcher convention for OS-bit PROCEDUREs) and
// returns without consuming any stack bytes.
//
// Engines-agree subset: both BasiliskII System 7.5.3 ROM
// and Systemless HLE preserve A7 across the call (no Pascal
// stack frame is consumed) and leave the queue in an
// empty state on exit.
//
// Catalogue proof: a016_finitqueue_strict
// witnesses sp_pre == sp_post for both a single call
// and a 5-call composition (per-call pop discipline +
// cumulative drift detection).
// Contract test: see `finitqueue_has_no_parameters_and_preserves_stack_pointer`
// in this file's tests module for the in-Rust witness.
(false, 0x16) => {
cpu.write_reg(Register::D0, 0);
Ok(())
}
// PBEject ($A017)
// Flushes the specified volume, places it offline, and ejects it.
// FUNCTION PBEject (paramBlock: ParmBlkPtr): OSErr;
// Inside Macintosh: Files (1992), p. 2-141.
//
// Register convention (OS-bit FUNCTION; IM:Files 1992 p. 2-141
// trap macro `_Eject`):
// A0 entry: ParmBlkPtr (paramBlock)
// D0 exit: OSErr result code (also mirrored into pb.ioResult
// at pb+16 per File Manager basic parameter block
// dispatcher convention, IM:II 1985 p. II-114)
//
// Parameter block fields used (IM:Files 1992 p. 2-141):
// → ioCompletion (ProcPtr) — pointer to a completion routine
// ← ioResult (OSErr) — result code (mirrors D0)
// → ioNamePtr (StringPtr) — pointer to a pathname
// → ioVRefNum (Integer) — volume specification
//
// Always executes synchronously.
//
// Documented result codes (IM:Files 1992 p. 2-141):
// noErr 0 No error
// nsvErr -35 No such volume
// ioErr -36 I/O error
// bdNamErr -37 Bad volume name
// paramErr -50 No default volume
// nsDrvErr -56 No such drive
// extFSErr -58 External file system
//
// MPW Universal Headers Files.h:
// #pragma parameter __D0 PBEject(__A0)
// EXTERN_API(OSErr) PBEject(ParmBlkPtr paramBlock)
// ONEWORDINLINE(0xA017);
//
// Systemless HLE compromise: the single-volume HLE has no
// removable media to eject, so this stub returns noErr (0)
// unconditionally and writes 0 to pb.ioResult.
//
// Engines-agree subset (witnessed by a017_pbeject_strict):
// • D0 == pb.ioResult (pb+16) after the call (dispatcher
// convention IM:II 1985 p. II-114) regardless of the
// absolute OSErr value;
// • Register-only OS-bit FUNCTION calling convention; no
// Pascal stack frame consumed (A7 preserved).
//
// Apple-vs-BasiliskII divergence on absolute OSErr: BII
// System 7.5.3 ROM is expected to return nsvErr (-35) or a
// related error for a bogus vRefNum (e.g. 9999), while
// Systemless returns noErr unconditionally. Both engines obey
// the dispatcher convention (D0 == ioResult).
//
// Contract tests: see `pbeject_returns_noerr_when_hle_has_no_removable_media`
// and `pbeject_writes_same_oserr_to_d0_and_ioresult_preserving_stack`
// below.
(false, 0x17) => {
let pb = cpu.read_reg(Register::A0);
bus.write_word(pb + 16, 0); // noErr
cpu.write_reg(Register::D0, 0);
Ok(())
}
// PBOffLine ($A035)
// Places the specified volume offline by calling PBFlushVol to
// flush the volume and releasing all the memory used for the
// volume except for the volume control block.
// FUNCTION PBOffLine (paramBlock: ParmBlkPtr): OSErr;
// Inside Macintosh: Files (1992), pp. 2-141 to 2-142.
//
// Register convention (OS-bit FUNCTION; IM:Files 1992 p. 2-142
// trap macro `_OffLine`):
// A0 entry: ParmBlkPtr (paramBlock)
// D0 exit: OSErr result code (also mirrored into pb.ioResult
// at pb+16 per File Manager dispatcher convention,
// IM:II 1985 p. II-114)
//
// Parameter block fields used (IM:Files 1992 p. 2-141):
// → ioCompletion (ProcPtr) — pointer to a completion routine
// ← ioResult (OSErr) — result code (mirrors D0)
// → ioNamePtr (StringPtr) — pointer to a pathname
// → ioVRefNum (Integer) — volume specification
//
// Always executes synchronously.
//
// Documented result codes (IM:Files 1992 p. 2-142):
// noErr 0 No error
// nsvErr -35 No such volume
// ioErr -36 I/O error
// bdNamErr -37 Bad volume name
// paramErr -50 No default volume
// nsDrvErr -56 No such drive
// extFSErr -58 External file system
//
// MPW Universal Headers (Files.h):
// #pragma parameter __D0 PBOffLine(__A0)
// EXTERN_API(OSErr) PBOffLine(ParmBlkPtr paramBlock)
// ONEWORDINLINE(0xA035);
//
// Systemless HLE compromise: the single-volume HLE has no offline-
// able volume (the boot volume is permanently online in our
// VFS), so the trap collapses to a no-op that writes noErr (0)
// into both D0 and pb.ioResult. BasiliskII dispatches the real
// ROM PB trap and may return a non-zero OSErr (typically
// nsvErr -35) for a bogus vRefNum. The absolute return value
// is therefore engines-divergent; the engines-agree subset is
// (a) the dispatcher convention writing the same OSErr into
// both D0 and ioResult, and (b) the register-only OS-bit
// FUNCTION calling convention preserving A7.
//
// Strict bake: `a035_pboffline_strict` witnesses both engines-
// agree assertions:
// A035:pboffline_writes_same_oserr_to_d0_and_ioresult
// A035:pboffline_register_only_calling_convention_preserves_stack
(false, 0x35) => {
let pb = cpu.read_reg(Register::A0);
bus.write_word(pb + 16, 0); // noErr → pb.ioResult
cpu.write_reg(Register::D0, 0); // noErr → D0
Ok(())
}
// PBSetFVers / _SetFilType ($A043)
// Changes the version number of a file (high byte of ioMisc).
// FUNCTION PBSetFVers (paramBlock: ParmBlkPtr; async: BOOLEAN): OSErr;
// Inside Macintosh Volume II, II-117 / Volume IV, IV-153
//
// Register convention (IM:II 1985 p. II-114 File Manager basic-PB
// dispatcher; IM:II 1985 p. II-117 PBSetFVers entry):
// A0 (entry) = ParmBlkPtr — pointer to the parameter block.
// D0 (exit) = OSErr — function result, also mirrored to
// pb.ioResult at pb+16 by the OS dispatcher.
//
// OS-bit FUNCTION ABI: no Pascal stack argument frame; A7 is
// preserved across the call. The MPW Universal Headers Files.h
// declaration confirms the register-only convention:
// #pragma parameter __D0 PBSetFVersSync(__A0)
// EXTERN_API(OSErr) PBSetFVersSync(ParmBlkPtr paramBlock)
// ONEWORDINLINE(0xA043);
//
// IOParam field map (IM:II 1985 p. II-117):
// → 12 ioCompletion pointer (NIL for synchronous calls)
// ← 16 ioResult word (mirrored from D0)
// → 18 ioNamePtr pointer (Pascal-string file name)
// → 22 ioVRefNum word (volume reference number)
// → 26 ioFVersNum byte (current MFS version number)
// → 28 ioMisc byte (new version in high-order byte)
//
// Documented result codes (IM:II 1985 p. II-117):
// noErr ( 0) — call succeeded (and on HFS volumes always)
// bdNamErr (-37) — bad file name
// dupFNErr (-48) — duplicate file name and version
// extFSErr (-58) — external file system
// fLckdErr (-45) — file locked
// fnfErr (-43) — file not found
// nsvErr (-35) — no such volume
// wPrErr (-44) — hardware volume lock
//
// Per IM:IV 1986 p. IV-153 (HFS update): "PBSetFVers has no
// effect on hierarchical volumes." Per IM:II 1985 p. II-117
// verbatim warning: "The Resource Manager, the Segment Loader,
// and the Standard File Package operate only on files with
// version number 0; changing the version number of a file to
// a nonzero number will prevent them from operating on it."
//
// Systemless HLE compromise: the host VFS is HFS-only, so every
// file is implicitly at version 0 and PBSetFVers is a no-op.
// We write 0 (noErr) to both pb.ioResult @ pb+16 and D0,
// honoring the basic-PB dispatcher convention.
//
// Engines-agree subset (witnessed by a043_pbsetfvers_strict):
// 1. Dispatcher convention: D0 == pb.ioResult after the call
// (both registers receive the same OSErr).
// 2. Register-only ABI: A0 is the sole input, D0 the sole
// output; A7 unchanged.
//
// BasiliskII parity note: BII System 7.5.3 ROM dispatches the
// real PB trap against an HFS volume mounted via extfs. Per
// IM:IV 1986 p. IV-153 PBSetFVers is a documented no-op there,
// so the absolute OSErr is expected to be noErr (0) as well.
// Even where engines diverge on the absolute OSErr value, both
// engines agree to mirror that value into both D0 and ioResult.
//
// Regression coverage (BasiliskII goldens):
// - a043_pbsetfvers_strict — 2 bands:
// A043:pbsetfvers_writes_same_oserr_to_d0_and_ioresult
// A043:pbsetfvers_register_only_calling_convention_preserves_stack
//
// PBSetFVers ($A043): HFS no-op per IM:IV 1986 p. IV-153;
// mirrors noErr to D0 and pb.ioResult per IM:II 1985 p. II-114
// dispatcher convention; preserves A7 per the register-only
// OS-bit FUNCTION ABI.
(false, 0x43) => {
let pb = cpu.read_reg(Register::A0);
bus.write_word(pb + 16, 0); // noErr → pb.ioResult
cpu.write_reg(Register::D0, 0); // noErr → D0
Ok(())
}
// AddDrive ($A04E)
// Adds a drive queue element to the drive queue. Register-form:
// D0.W = drvNum, D1.W = drvrRefNum, A0 = qEl.
// PROCEDURE AddDrive (drvrRefNum, drvNum: Integer; qEl: DrvQElPtr);
// Inside Macintosh: Files (1992), p. 2-236; Technical Note #108
// "AddDrive, DrvrInstall and DrvrRemove" documents the register
// calling convention and noErr return.
// AddDrive ($A04E): No drive queue in HLE; PROCEDURE returns via D0 = noErr.
(false, 0x4E) => {
cpu.write_reg(Register::D0, 0);
Ok(())
}
// PBRename / PBHRename ($A00B / $A20B)
// Renames a file or directory. PBHRename is the HFS variant.
// FUNCTION PBRename (paramBlock: ParmBlkPtr; async: Boolean): OSErr;
// FUNCTION PBHRename (paramBlock: HParmBlkPtr; async: Boolean): OSErr;
// Inside Macintosh: Files (1992), p. 2-118
//
// Register convention (IM:Files 1992 p. 2-118):
// A0 entry: ParmBlkPtr / HParmBlkPtr (pb)
// D0 exit: OSErr (mirrored to pb.ioResult at pb+16 by the
// File Manager basic-PB dispatcher convention,
// IM:II 1985 p. II-114).
// No Pascal stack frame is consumed (OS-bit FUNCTION ABI).
//
// Parameter block fields (IM:Files 1992 p. 2-118):
// pb+12 ioCompletion (→) IOCompletionUPP, may be NIL on Sync
// pb+16 ioResult (←) OSErr, mirror of D0
// pb+18 ioNamePtr (→) StringPtr, OLD file name
// pb+22 ioVRefNum (→) volume reference number
// pb+28 ioMisc (→) StringPtr, NEW file name
// pb+48 ioDirID (→) HFS variant only; directory ID
//
// Documented result codes (IM:Files 1992 pp. 2-118, 10222..10234):
// noErr 0 No error
// nsvErr -35 No such volume
// ioErr -36 I/O error
// bdNamErr -37 Bad name
// fnfErr -43 File not found (source missing)
// wPrErr -44 Diskette write-protected
// fLckdErr -45 File locked
// vLckdErr -46 Volume locked
// dupFNErr -48 Duplicate file name (destination exists)
// paramErr -50 Empty name
//
// MPW Universal Headers (Files.h):
// #pragma parameter __D0 PBRenameSync(__A0)
// EXTERN_API(OSErr) PBRenameSync(ParmBlkPtr paramBlock)
// ONEWORDINLINE(0xA00B);
// #pragma parameter __D0 PBHRenameSync(__A0)
// EXTERN_API(OSErr) PBHRenameSync(HParmBlkPtr paramBlock)
// ONEWORDINLINE(0xA20B);
// Sibling Async forms at $A40B and $A60B respectively.
//
// Engines-agree subset (witnessed by strict bake
// a20b_pbhrename_strict):
// - A20B:pbhrename_writes_same_oserr_to_d0_and_ioresult
// (dispatcher convention D0 == ioResult @ pb+16 with
// a 0x3FFF pre-poison sentinel).
// - A20B:pbhrename_register_only_calling_convention_preserves_stack
// (StackSpace() sandwich witnesses A7 preserved).
//
// Systemless HLE: maintains VFS state in self.vfs / self.vfs_rsrc /
// self.vfs_metadata / self.locked_files / self.open_files /
// self.output_dir; the same (false, 0x0B) arm services both
// $A00B and $A20B since the HFS variant differs only in the
// ioDirID field which Systemless does not branch on.
//
// Regression coverage:
// - pbh_open_rf_rename — $A20A + $A20B fnfErr paths
// - a20b_pbhrename_strict — engines-agree dispatcher convention
(false, 0x0B) => {
let pb = cpu.read_reg(Register::A0);
let old_name_ptr = bus.read_long(pb + 18);
let new_name_ptr = bus.read_long(pb + 28);
let old_name = Self::read_pb_filename(bus, old_name_ptr);
let new_name = Self::read_pb_filename(bus, new_name_ptr);
eprintln!("[TRAP] PBRename(\"{}\" -> \"{}\")", old_name, new_name);
if old_name.is_empty() || new_name.is_empty() {
bus.write_word(pb + 16, (-50i16) as u16); // paramErr
cpu.write_reg(Register::D0, (-50i32) as u32);
return Some(Ok(()));
}
let Some(old_key) = self.find_vfs_file(&old_name) else {
bus.write_word(pb + 16, (-43i16) as u16); // fnfErr
cpu.write_reg(Register::D0, (-43i32) as u32);
return Some(Ok(()));
};
// Compute new key by replacing the basename of old_key.
// Preserve any directory prefix so a rename of
// "Pref/MyFile" -> "Renamed" lands as "Pref/Renamed".
let normalized_new = super::TrapDispatcher::normalize_vfs_path(&new_name);
let new_key = match old_key.rsplit_once('/') {
Some((parent, _)) => format!("{parent}/{normalized_new}"),
None => normalized_new.clone(),
};
if new_key == old_key {
// Rename to the same name is a noErr no-op per Files
// 1992, 2-118 (the file is unaffected).
bus.write_word(pb + 16, 0);
cpu.write_reg(Register::D0, 0);
return Some(Ok(()));
}
if self.vfs.contains_key(&new_key) || self.vfs_rsrc.contains_key(&new_key) {
bus.write_word(pb + 16, (-48i16) as u16); // dupFNErr
cpu.write_reg(Register::D0, (-48i32) as u32);
return Some(Ok(()));
}
if let Some(data) = self.vfs.remove(&old_key) {
self.vfs.insert(new_key.clone(), data);
}
if let Some(rsrc) = self.vfs_rsrc.remove(&old_key) {
self.vfs_rsrc.insert(new_key.clone(), rsrc);
}
if let Some(metadata) = self.vfs_metadata.remove(&old_key) {
self.vfs_metadata.insert(new_key.clone(), metadata);
}
if self.locked_files.remove(&old_key) {
self.locked_files.insert(new_key.clone());
}
// Open access paths must follow the rename so a
// subsequent FSRead/FSWrite still resolves to the file.
let open_refnums: Vec<u16> = self
.open_files
.iter()
.filter_map(|(refnum, name)| {
if name == &old_key {
Some(*refnum)
} else {
None
}
})
.collect();
for refnum in open_refnums {
self.open_files.insert(refnum, new_key.clone());
}
if let Some(ref dir) = self.output_dir {
let old_path = dir.join(&old_key);
let new_path = dir.join(&new_key);
if let Some(parent) = new_path.parent() {
let _ = std::fs::create_dir_all(parent);
}
let _ = std::fs::rename(old_path, new_path);
}
bus.write_word(pb + 16, 0); // noErr
cpu.write_reg(Register::D0, 0);
Ok(())
}
// PBSetFLock / PBHSetFLock ($A041 / $A241)
// Locks a file by setting bit 0 of ioFlAttrib (visible via
// subsequent PBGetFInfo / PBHGetFInfo).
// FUNCTION PBSetFLock (paramBlock: ParmBlkPtr; async: Boolean): OSErr;
// FUNCTION PBHSetFLock (paramBlock: HParmBlkPtr; async: Boolean): OSErr;
// Inside Macintosh: Files 1992, pp. 2-110 to 2-111
//
// Register convention (IM:Files 1992 p. 2-110):
// A0 entry: ParmBlkPtr (pb)
// D0 exit: OSErr (mirrored to pb.ioResult at pb+16 by the
// File Manager basic-PB dispatcher convention,
// IM:II 1985 p. II-114).
// No Pascal stack frame is consumed.
//
// Parameter block fields (IM:Files 1992 p. 2-110):
// pb+12 ioCompletion (→) IOCompletionUPP, may be NIL on Sync
// pb+16 ioResult (←) OSErr, mirror of D0
// pb+18 ioNamePtr (→) StringPtr, file name (Pascal string)
// pb+22 ioVRefNum (→) volume reference number (0 = default)
//
// Documented result codes (IM:Files 1992 pp. 2-110, 10141..10150):
// noErr 0 No error
// fnfErr -43 File not found
// ioErr -36 I/O error
// nsvErr -35 Volume not found
// bdNamErr -37 Bad filename
// paramErr -50 ioVRefNum bad / ioNamePtr is NIL
//
// MPW Universal Headers Files.h:
// #pragma parameter __D0 PBSetFLockSync(__A0)
// EXTERN_API(OSErr) PBSetFLockSync(ParmBlkPtr paramBlock)
// ONEWORDINLINE(0xA041);
// #define PBSetFLock(pb,async) (async ? PBSetFLockAsync(pb) : PBSetFLockSync(pb))
//
// Systemless HLE: maintains lock state in self.locked_files and
// returns -43 fnfErr for files not in the VFS, matching IM
// verbatim.
//
// BasiliskII divergence note: real System 7.5.3 + extfs
// returns noErr (0) from PBSetFLock / PBHSetFLock on a
// missing file, not fnfErr (-43) — pinned by the prior bake
// fixtures/file/A241_pbh_set_rst_flock. Systemless keeps the
// IM-documented absolute OSErr; both engines obey the
// dispatcher convention writing the same value to BOTH D0
// and pb.ioResult @ pb+16.
//
// Catalogue-proof references (engines-agree subset):
// a041_a042_pbsetflock_pbrstflock_strict
// A041:pbsetflock_writes_same_oserr_to_d0_and_ioresult
// A041:pbsetflock_register_only_calling_convention_preserves_stack
// A042:pbrstflock_writes_same_oserr_to_d0_and_ioresult
// A042:pbrstflock_register_only_calling_convention_preserves_stack
// a241_a242_pbhsetflock_pbhrstflock_strict
// A241:pbhsetflock_writes_same_oserr_to_d0_and_ioresult
// A241:pbhsetflock_register_only_calling_convention_preserves_stack
// A242:pbhrstflock_writes_same_oserr_to_d0_and_ioresult
// A242:pbhrstflock_register_only_calling_convention_preserves_stack
// Contract tests in this file's `mod tests`:
// pbsetflock_existing_file_returns_noerr_and_sets_ioflattrib_locked_bit
// pbsetflock_missing_file_returns_fnferr_in_d0_and_ioresult
// pbsetflock_writes_same_oserr_to_d0_and_ioresult_preserving_stack
// pbsetflock_pbrstflock_write_same_oserr_to_d0_and_ioresult_preserving_stack
// pbhsetflock_pbhrstflock_write_same_oserr_to_d0_and_ioresult_preserving_stack
// pbrstflock_existing_file_returns_noerr_and_clears_ioflattrib_locked_bit
// pbrstflock_missing_file_returns_fnferr_in_d0_and_ioresult
// pbhsetflock_existing_file_returns_noerr_and_sets_ioflattrib_locked_bit
// pbhrstflock_existing_file_returns_noerr_and_clears_ioflattrib_locked_bit
(false, 0x41) => {
let pb = cpu.read_reg(Register::A0);
let name_ptr = bus.read_long(pb + 18);
let filename = Self::read_pb_filename(bus, name_ptr);
eprintln!("[TRAP] PBSetFLock(\"{}\")", filename);
if let Some(vfs_name) = self.find_vfs_file(&filename) {
self.locked_files.insert(vfs_name);
bus.write_word(pb + 16, 0); // noErr
cpu.write_reg(Register::D0, 0);
} else {
bus.write_word(pb + 16, (-43i16) as u16); // fnfErr
cpu.write_reg(Register::D0, (-43i32) as u32);
}
Ok(())
}
// PBRstFLock / PBHRstFLock ($A042 / $A242)
// Unlocks a file by clearing bit 0 of ioFlAttrib.
// FUNCTION PBRstFLock (paramBlock: ParmBlkPtr; async: Boolean): OSErr;
// FUNCTION PBHRstFLock (paramBlock: HParmBlkPtr; async: Boolean): OSErr;
// Inside Macintosh: Files 1992, pp. 2-110 to 2-111
//
// Register convention, parameter-block field map, result-
// code table, MPW Universal Headers declaration, Systemless
// HLE compromise, and BasiliskII divergence note are all
// identical to the $A041 PBSetFLock arm above (just swap
// PBSetFLock → PBRstFLock and ONEWORDINLINE(0xA041) →
// ONEWORDINLINE(0xA042)).
//
// Catalogue-proof reference: see the $A041 arm above —
// a041_a042_pbsetflock_pbrstflock_strict witnesses both
// sibling traps in a single combined fixture.
(false, 0x42) => {
let pb = cpu.read_reg(Register::A0);
let name_ptr = bus.read_long(pb + 18);
let filename = Self::read_pb_filename(bus, name_ptr);
eprintln!("[TRAP] PBRstFLock(\"{}\")", filename);
if let Some(vfs_name) = self.find_vfs_file(&filename) {
self.locked_files.remove(&vfs_name);
bus.write_word(pb + 16, 0); // noErr
cpu.write_reg(Register::D0, 0);
} else {
bus.write_word(pb + 16, (-43i16) as u16); // fnfErr
cpu.write_reg(Register::D0, (-43i32) as u32);
}
Ok(())
}
// PBCreate / PBHCreate (0xA008 / 0xA208)
// Creates a new file.
// FUNCTION PBCreate (paramBlock: ParmBlkPtr; async: BOOLEAN): OSErr;
// FUNCTION PBHCreate (paramBlock: HParmBlkPtr; async: BOOLEAN): OSErr;
// Files 1992, 2-89 / 9279 (PBCreate) and 2-191 / 9700 (PBHCreate).
// The OS-trap dispatcher masks `trap & 0x00FF`, so $A208
// PBHCreate lands on the same low byte and shares this arm.
//
// Regression coverage (BasiliskII golden):
// pb_create ← $A008 dispatch path
// pbh_create ← $A208 dispatch via poisoned-
// ioResult indicator with
// dirID=999999 (BasiliskII
// writes dirNFErr per IM:Files
// 9743; Systemless writes noErr
// because impl doesn't
// validate dirID; trace
// bucket $208 pins dispatch)
// PBCreate ($A008): Creates file in VFS with metadata, writes to output_dir if set
// PBHCreate ($A208): HFS variant aliased onto $A008
(false, 0x08) => {
let pb = cpu.read_reg(Register::A0);
let name_ptr = bus.read_long(pb + 18);
let filename = Self::read_pb_filename(bus, name_ptr);
let v_ref = bus.read_word(pb + 22) as i16;
let dir_id = bus.read_long(pb + 48);
eprintln!(
"[TRAP] PBCreate(\"{}\") vref={} dirID={}",
filename, v_ref, dir_id
);
if filename.is_empty() {
bus.write_word(pb + 16, (-50i16) as u16); // paramErr
cpu.write_reg(Register::D0, (-50i32) as u32);
return Some(Ok(()));
}
// Per IM Files 1992, 2-89, PBCreate returns dupFNErr when the
// file already exists. Some shareware/demo titles ship a
// marker file (e.g. Meteor Storm's "MS UserKey") inside their
// install folder yet still call HCreate at launch without an
// intervening HDelete, treating any error as fatal. Real Mac
// installs were typically run from a freshly-extracted copy
// where the file did not exist, so the bug never surfaced.
// Systemless models the .sit-extracted folder directly, so the
// marker is always pre-existing on first launch. To keep
// these titles bootable without breaking apps that *do*
// handle dupFNErr, truncate-on-exists: clear both forks of
// the existing entry and report noErr.
if let Some(existing) = self.find_vfs_file(&filename) {
// Preserve any existing resource-fork content — some
// shareware titles ship a key file with registration
// resources baked into the resource fork (the data
// fork is the marker, the resource fork carries the
// actual templates). Truncating both forks would
// destroy the resources the game then tries to read
// back via FSpOpenResFile + Get1Resource. Truncating
// only the data fork is enough to satisfy the
// "create fresh" expectation that triggers the
// dupFNErr fatal-launch path.
let rsrc_len = self.vfs_rsrc.get(&existing).map(|v| v.len()).unwrap_or(0);
eprintln!(
"[TRAP] PBCreate truncate-on-exists for \"{}\" -> noErr (rsrc preserved: {} bytes)",
existing, rsrc_len
);
self.vfs.insert(existing.clone(), Vec::new());
self.vfs_rsrc.entry(existing.clone()).or_default();
self.touch_vfs_entry(&existing);
if let Some(ref dir) = self.output_dir {
let host_path = dir.join(&existing);
if let Some(parent) = host_path.parent() {
let _ = std::fs::create_dir_all(parent);
}
let _ = std::fs::write(host_path, []);
}
bus.write_word(pb + 16, 0);
cpu.write_reg(Register::D0, 0);
return Some(Ok(()));
}
let normalized = super::TrapDispatcher::normalize_vfs_path(&filename);
self.vfs.insert(normalized.clone(), Vec::new());
// Real Mac files always have both forks. PBCreate must seed
// an empty resource fork too, otherwise a subsequent
// PBOpenRF on the new file (e.g. Mars Rising's installer
// pattern: PBCreate "Installer Temp (delete)" then
// PBOpenRF that file's resource fork) returns fnfErr and
// the installer halts via _ExitToShell.
// Files 1992, 1-58 (each file has data + resource fork)
self.vfs_rsrc.insert(normalized.clone(), Vec::new());
self.touch_vfs_entry(&normalized);
if let Some(ref dir) = self.output_dir {
let host_path = dir.join(&normalized);
if let Some(parent) = host_path.parent() {
let _ = std::fs::create_dir_all(parent);
}
let _ = std::fs::write(host_path, []);
}
bus.write_word(pb + 16, 0);
cpu.write_reg(Register::D0, 0);
Ok(())
}
// PBDelete / PBHDelete (0xA009 / 0xA209)
// Deletes a file.
// FUNCTION PBDelete (paramBlock: ParmBlkPtr; async: BOOLEAN): OSErr;
// FUNCTION PBHDelete (paramBlock: HParmBlkPtr; async: BOOLEAN): OSErr;
// Files 1992, 2-102 / 9286 (PBDelete) and 2-189 / 9797 (PBHDelete).
// The HFS variant lands on the same low byte after
// `trap & 0x00FF` masking and shares this arm.
//
// Regression coverage (BasiliskII golden):
// pb_delete ← $A009 fnfErr/dispatch path
// pbh_delete ← $A209 dispatch via poisoned-
// ioResult indicator (real Mac
// extfs returns noErr for
// missing file; trace bucket
// $209 pins the dispatch)
// PBDelete ($A009): Deletes file from VFS, cleans up metadata and open file refs
// PBHDelete ($A209): HFS variant aliased onto $A009
(false, 0x09) => {
let pb = cpu.read_reg(Register::A0);
let name_ptr = bus.read_long(pb + 18);
let filename = Self::read_pb_filename(bus, name_ptr);
eprintln!("[TRAP] PBDelete(\"{}\")", filename);
if filename.is_empty() {
bus.write_word(pb + 16, (-50i16) as u16); // paramErr
cpu.write_reg(Register::D0, (-50i32) as u32);
return Some(Ok(()));
}
if let Some(vfs_name) = self.find_vfs_file(&filename) {
self.vfs.remove(&vfs_name);
self.vfs_rsrc.remove(&vfs_name);
self.remove_vfs_entry_metadata(&vfs_name);
let stale_refs: Vec<u16> = self
.open_files
.iter()
.filter_map(|(refnum, open_name)| {
if open_name == &vfs_name {
Some(*refnum)
} else {
None
}
})
.collect();
for refnum in stale_refs {
self.open_files.remove(&refnum);
self.file_positions.remove(&refnum);
}
if let Some(ref dir) = self.output_dir {
let host_path = dir.join(&vfs_name);
let _ = std::fs::remove_file(host_path);
}
bus.write_word(pb + 16, 0);
cpu.write_reg(Register::D0, 0);
} else {
bus.write_word(pb + 16, (-43i16) as u16); // fnfErr
cpu.write_reg(Register::D0, (-43i32) as u32);
}
Ok(())
}
// PBGetFInfo / PBHGetFInfo (0xA00C / 0xA20C)
// Gets Finder and catalog information for a file.
// FUNCTION PBGetFInfo (paramBlock: ParmBlkPtr; async: BOOLEAN): OSErr;
// FUNCTION PBHGetFInfo (paramBlock: HParmBlkPtr; async: BOOLEAN): OSErr;
// Files 1992, 2-170 / 9296. The HFS variant ($A20C) is
// aliased onto this arm via the OS-trap `trap & 0x00FF`
// mask.
//
// Regression coverage (BasiliskII golden):
// pb_finfo ← $A00C/$A00D fnfErr path
// pbh_get_set_finfo ← $A20C/$A20D fnfErr path
// PBGetFInfo ($A00C): Returns catalog info with file type, creator, finder flags, and ioFlAttrib lock bit from VFS metadata
// PBHGetFInfo ($A20C): HFS variant aliased onto $A00C
(false, 0x0C) => {
let pb = cpu.read_reg(Register::A0);
let name_ptr = bus.read_long(pb + 18);
let filename = Self::read_pb_filename(bus, name_ptr);
eprintln!("[TRAP] PBGetFInfo(\"{}\")", filename);
if let Some(vfs_name) = self.find_vfs_file(&filename) {
if let Some(metadata) = self.vfs_file_metadata(&vfs_name) {
self.fill_file_catalog_info(bus, pb, &vfs_name, metadata);
Self::write_pstring(
bus,
name_ptr,
super::TrapDispatcher::vfs_basename(&vfs_name),
);
}
bus.write_word(pb + 16, 0); // noErr
cpu.write_reg(Register::D0, 0);
} else {
bus.write_word(pb + 16, (-43i16) as u16); // fnfErr
cpu.write_reg(Register::D0, (-43i32) as u32);
}
Ok(())
}
// PBSetFInfo / PBHSetFInfo (0xA00D / 0xA20D)
// Sets Finder information for a file.
// FUNCTION PBSetFInfo (paramBlock: ParmBlkPtr; async: BOOLEAN): OSErr;
// FUNCTION PBHSetFInfo (paramBlock: HParmBlkPtr; async: BOOLEAN): OSErr;
// Files 1992, 2-205 / 9298. The HFS variant ($A20D) is the
// same low byte after `trap & 0x00FF` masking and shares
// this arm.
//
// Per Files 1992, 2-205: returns fnfErr (-43) if the named
// file does not exist. The previous HLE implementation
// silently returned noErr for missing files, masking bugs in
// games that rely on this error to detect missing saves.
//
// Regression coverage:
// pbsetfinfo_missing_file_returns_fnferr
// pbsetfinfo_existing_file_updates_finder_info
// pb_finfo ← $A00C/$A00D fnfErr path
// pbh_get_set_finfo ← $A20C/$A20D fnfErr path
// PBSetFInfo ($A00D): Stores file type, creator, and Finder flags in VFS metadata
// PBHSetFInfo ($A20D): HFS variant aliased onto $A00D
(false, 0x0D) => {
let pb = cpu.read_reg(Register::A0);
let name_ptr = bus.read_long(pb + 18);
let filename = Self::read_pb_filename(bus, name_ptr);
eprintln!("[TRAP] PBSetFInfo(\"{}\")", filename);
if let Some(vfs_name) = self.find_vfs_file(&filename) {
let file_type = bus.read_long(pb + 32);
let creator = bus.read_long(pb + 36);
let finder_flags = bus.read_word(pb + 40);
self.set_vfs_entry_finfo(&vfs_name, file_type, creator, finder_flags);
bus.write_word(pb + 16, 0); // noErr
cpu.write_reg(Register::D0, 0);
} else {
bus.write_word(pb + 16, (-43i16) as u16); // fnfErr
cpu.write_reg(Register::D0, (-43i32) as u32);
}
Ok(())
}
// PBSetVol / PBHSetVol ($A015 / $A215)
// Sets the default volume and optionally the default directory.
// FUNCTION PBSetVol (paramBlock: ParmBlkPtr; async: Boolean): OSErr;
// FUNCTION PBHSetVol (paramBlock: WDPBPtr; async: Boolean): OSErr;
// Inside Macintosh: Files (1992), pp. 2-151 to 2-154.
//
// PBSetVol subsection: IM:Files 1992 pp. 2-151 to 2-153.
// PBHSetVol subsection: IM:Files 1992 pp. 2-153 to 2-154 (HFS
// variant; trap macro `_HSetVol`, ONEWORDINLINE(0xA215)).
// The OS-trap dispatcher masks `trap & 0x00FF`, so $A215
// PBHSetVol and $A015 PBSetVol land on the same low byte and
// share this arm.
//
// Register convention (OS-bit FUNCTION, IM:II 1985 p. II-114):
// On entry: A0 = ParmBlkPtr / WDPBPtr (paramBlock)
// On exit: D0 = OSErr (also mirrored to pb.ioResult @ pb+16)
//
// Parameter block field map (basic ParamBlockRec / WDPBRec):
// pb+12 ioCompletion (→) IOCompletionUPP, NIL for sync
// pb+16 ioResult (←) OSErr (also returned in D0)
// pb+18 ioNamePtr (→) StringPtr; NIL for vRefNum-only call
// pb+22 ioVRefNum (→) Vol / working-directory refnum
// pb+48 ioWDDirID (→) HFS dirID (WDPBRec only)
//
// Result codes (IM:Files 1992 p. 2-137 + pp. 2-153..2-154):
// noErr 0 No error
// nsvErr -35 Volume not found
// bdNamErr -37 Bad filename
// paramErr -50 Bad ioVRefNum / ioWDDirID combination
//
// MPW Universal Headers Files.h:
// #pragma parameter __D0 PBHSetVolSync(__A0)
// EXTERN_API(OSErr) PBHSetVolSync(WDPBPtr paramBlock)
// ONEWORDINLINE(0xA215);
// #define PBHSetVol(pb, async) (async ? PBHSetVolAsync(pb)
// : PBHSetVolSync(pb))
//
// Systemless HLE behaviour (this arm):
// * Resolves ioVRefNum via working-directory table, then
// dirID lookup, then default-directory fallback, then
// boot-volume fallback, else returns nsvErr (-35) per
// IM:Files 2-137 and 2-153..2-154 documented contract.
// * On the success path, updates self.default_dir_id and
// self.app_wd_refnum, and keeps the Standard File globals
// (CurDirStore @ $0398, SFSaveDisk @ $0214) aligned with
// the new default per IM:Files 1992 p. 3-65.
// * Writes the OSErr to BOTH D0 AND pb.ioResult per the
// File Manager basic-PB dispatcher convention, regardless
// of whether the absolute value is noErr or nsvErr.
//
// Engines-agree subset (baked):
// * Dispatcher convention: D0 == ioResult after the call,
// and ioResult overwrites any pre-call sentinel. Both
// engines obey this regardless of the absolute OSErr.
// * Register-only OS-bit FUNCTION calling convention:
// A0 input, D0 output, no Pascal stack frame consumed;
// A7 preserved across the call.
//
// Engines-divergent absolute OSErr (Systemless pins via contract
// tests in this module):
// * Systemless returns nsvErr (-35) on the unrecognised-volume
// path; BasiliskII may return -35 or another OSErr
// depending on its volume table state. The dispatcher-
// convention witness holds for both engines.
//
// Catalogue proof:
// a215_pbhsetvol_strict — 544x40 pixmap
// fixture witnessing:
// A215:pbhsetvol_writes_same_oserr_to_d0_and_ioresult
// A215:pbhsetvol_register_only_calling_convention_preserves_stack
//
// Contract tests (this module):
// pbhsetvol_sets_default_directory_from_iowddirid_for_volume_refnum_calls
// pbhsetvol_invalid_vrefnum_returns_nsverr
// pbhsetvol_writes_same_oserr_to_d0_and_ioresult_preserving_stack
//
// Regression coverage:
// pb_vol ($A015 non-H variant)
// pbh_get_set_vol ($A215 HFS variant — trace-mode
// histogram bucket $215 pins
// trap-word dispatch via
// poisoned-ioResult indicator;
// phantom ioVRefNum = -999
// sidesteps default-volume
// mutation per IM:Files 8376
// nsvErr documented path)
// trap-doc: $A015 | PBSetVol | Partial | File Manager & Gestalt — OS Traps | Updates default volume/directory; nsvErr for unrecognised vRefNum (IM:Files 1992, 2-162)
// PBHSetVol ($A215): HFS variant aliased onto $A015
(false, 0x15) => {
let pb = cpu.read_reg(Register::A0);
let name_ptr = bus.read_long(pb + 18);
let name = Self::read_pb_filename(bus, name_ptr);
let requested_vref = bus.read_word(pb + 22) as i16;
let requested_dir_id = bus.read_long(pb + 48);
let mut target_volume_ref_num = self.resolve_volume_ref_num(requested_vref);
let mut target_dir_id = if let Some(working_directory) =
self.working_directories.get(&requested_vref)
{
target_volume_ref_num = working_directory.volume_ref_num;
working_directory.dir_id
} else if requested_dir_id != 0
&& self.directory_entry_for_id(requested_dir_id).is_some()
{
requested_dir_id
} else if requested_vref == 0 {
self.default_dir_id
} else if requested_vref == Self::boot_volume_ref_num() {
// ioVRefNum = -1: boot volume, use root directory (dirID 2).
2
} else {
// Unrecognised volume reference number: nsvErr (-35).
// IM:Files 1992, 2-162: "nsvErr — No such volume."
let nsverr: i16 = -35;
bus.write_word(pb + 16, nsverr as u16);
cpu.write_reg(Register::D0, nsverr as u32);
return Some(Ok(()));
};
if !name.is_empty() {
if let Some(path) = self.find_vfs_directory_in_directory(target_dir_id, &name) {
if let Some(directory) = self.vfs_directories.get(&path) {
target_dir_id = directory.dir_id;
}
} else if let Some(path) = self.find_vfs_file_in_directory(target_dir_id, &name)
{
if let Some(metadata) = self.vfs_file_metadata(&path) {
target_dir_id = metadata.parent_dir_id;
}
}
}
self.default_dir_id = target_dir_id;
self.app_wd_refnum = if target_dir_id == 2 {
target_volume_ref_num
} else {
self.open_working_directory(target_volume_ref_num, target_dir_id, 0)
.unwrap_or(target_volume_ref_num)
};
// Keep the Standard File globals aligned with the current
// default directory and volume. Files 1992, 3-65.
bus.write_long(addr::CUR_DIR_STORE, target_dir_id);
bus.write_word(addr::SF_SAVE_DISK, (-target_volume_ref_num) as u16);
eprintln!(
"[TRAP] PBSetVol name='{}' request_vRefNum={} request_dirID={} -> current_vRefNum={} current_dirID={}",
name,
requested_vref,
requested_dir_id,
self.app_wd_refnum,
self.default_dir_id
);
bus.write_word(pb + 16, 0);
cpu.write_reg(Register::D0, 0);
Ok(())
}
// _CmpString ($A03C) — underlying trap for EqualString
// FUNCTION EqualString (aStr, bStr: Str255; caseSens, diacSens: BOOLEAN): BOOLEAN;
// Inside Macintosh Volume II, II-377
//
// On entry: A0 = ptr to first string's first character
// A1 = ptr to second string's first character
// D0 high word = length of first string
// D0 low word = length of second string
// On exit: D0 = 0 if equal, 1 if not equal (long word).
//
// The default trap word $A03C is case-INSENSITIVE for ASCII
// (per IM:II II-377 the bare _CmpString has caseSens=FALSE).
// The MARKS / CASE trap-word bits 9 and 10 select different
// sensitivities; Systemless currently handles only the default.
//
// Regression coverage:
// cmpstring_returns_zero_for_identical_ascii
// cmpstring_returns_one_for_different_lengths
// cmpstring_returns_one_for_different_chars_same_length
// cmpstring_default_is_case_insensitive_for_ascii
// EqualString / CmpString ($A03C): String comparison per IM:II II-377
// Trap word bit 10 (0x0400): caseSens=TRUE → case-sensitive comparison.
// Default $A03C (bit 10 clear): caseSens=FALSE → case-insensitive.
// Trap word bit 9 (0x0200): diacSens flag (strip diacriticals when set).
// D0=0 means equal; D0=1 means not equal (EqualString Pascal glue inverts).
(false, 0x3C) => {
let a_ptr = cpu.read_reg(Register::A0);
let b_ptr = cpu.read_reg(Register::A1);
let d0 = cpu.read_reg(Register::D0);
let a_len = (d0 >> 16) & 0xFFFF;
let b_len = d0 & 0xFFFF;
let case_sens = (self.current_trap_word & 0x0400) != 0;
let result: u32 = if a_len != b_len {
1
} else {
let a_bytes = bus.read_bytes(a_ptr, a_len as usize);
let b_bytes = bus.read_bytes(b_ptr, b_len as usize);
if case_sens {
// Case-sensitive: exact byte comparison.
if a_bytes == b_bytes {
0
} else {
1
}
} else {
// Case-insensitive: fold ASCII letters before comparing.
if a_bytes.eq_ignore_ascii_case(&b_bytes) {
0
} else {
1
}
}
};
cpu.write_reg(Register::D0, result);
Ok(())
}
// PBSetEOF / HSetEOF ($A012)
// Sets the logical end-of-file of an open file.
// FUNCTION PBSetEOF (paramBlock: ParmBlkPtr; async: BOOLEAN): OSErr;
// Inside Macintosh Volume II, II-113
// PBSetEOF ($A012): Sets file length, truncates/extends VFS data
(false, 0x12) => {
let pb = cpu.read_reg(Register::A0);
let ref_num = bus.read_word(pb + 24);
let new_eof = bus.read_long(pb + 28) as usize; // ioMisc
let Some(filename) = self.open_files.get(&ref_num).cloned() else {
bus.write_word(pb + 16, (-51i16) as u16); // rfNumErr
cpu.write_reg(Register::D0, (-51i32) as u32);
return Some(Ok(()));
};
let host_sync_bytes = {
let file_buf = self.vfs.entry(filename.clone()).or_default();
file_buf.resize(new_eof, 0);
if self.output_dir.is_some() {
Some(file_buf.clone())
} else {
None
}
};
if let Some(pos) = self.file_positions.get_mut(&ref_num) {
if *pos > new_eof {
*pos = new_eof;
}
}
if let (Some(dir), Some(bytes)) = (&self.output_dir, host_sync_bytes) {
let host_path = dir.join(&filename);
if let Some(parent) = host_path.parent() {
let _ = std::fs::create_dir_all(parent);
}
if let Err(e) = std::fs::write(&host_path, bytes) {
eprintln!(
"[PBSetEOF] failed to sync {} to host ({}): {}",
filename,
host_path.display(),
e
);
}
}
self.touch_vfs_entry(&filename);
bus.write_word(pb + 16, 0); // noErr
cpu.write_reg(Register::D0, 0);
Ok(())
}
// OSDispatch / Process Manager (0xA88F)
// Handles Process Manager selectors needed by classic game utilities.
// FUNCTION GetCurrentProcess(VAR PSN: ProcessSerialNumber): OSErr;
// Processes 1994, 2-21 to 2-24
//
// MPW emits each ProcessMgr selector as
// MOVE.W #<sel>, -(SP) ; push selector word on stack
// _OSDispatch
// So selector is the TOP-OF-STACK word at trap entry. A few
// pre-existing direct-asm callers instead pre-load D0 before
// the trap -- if the top-of-stack word isn't a recognised
// selector, fall back to D0 for compatibility.
// OSDispatch ($A88F): Selectors $0037 GetCurrentProcess, $0038 GetNextProcess, $0039 GetFrontProcess, $003A GetProcessInformation (full ProcessInfoRec), $003B SetFrontProcess (no-op), $003C WakeUpProcess (no-op), $003D SameProcess (PSN compare). Selector table: IM:Processes 1994 p. 2-31.
(true, 0x08F) => {
let sp_entry = cpu.read_reg(Register::A7);
let stack_sel = bus.read_word(sp_entry) as u32 & 0xFFFF;
let d0_sel = cpu.read_reg(Register::D0) & 0xFFFF;
let (selector, sp) = match stack_sel {
0x0037..=0x003D => {
// Pop the selector word pushed by MOVE.W #sel,-(SP).
cpu.write_reg(Register::A7, sp_entry + 2);
(stack_sel, sp_entry + 2)
}
_ => (d0_sel, sp_entry),
};
match selector {
0x0037 => {
// GetCurrentProcess (0xA88F selector $0037)
// Returns serial number of current process.
// FUNCTION GetCurrentProcess(VAR PSN: ProcessSerialNumber): OSErr;
// Processes 1994, p. 2-21
let psn_ptr = bus.read_long(sp);
bus.write_long(psn_ptr, CURRENT_PROCESS_PSN_HIGH); // highLongOfPSN
bus.write_long(psn_ptr + 4, CURRENT_PROCESS_PSN_LOW); // lowLongOfPSN = kCurrentProcess
bus.write_word(sp + 4, 0); // noErr
cpu.write_reg(Register::A7, sp + 4);
Ok(())
}
0x0038 => {
// GetNextProcess (0xA88F selector $0038)
// Enumerates processes; returns procNotFound at end of list.
// FUNCTION GetNextProcess(VAR PSN: ProcessSerialNumber): OSErr;
// Processes 1994, p. 2-22
let psn_ptr = bus.read_long(sp);
let psn_high = bus.read_long(psn_ptr);
let psn_low = bus.read_long(psn_ptr.wrapping_add(4));
let err: i16 = if psn_high == 0 && psn_low == 0 {
bus.write_long(psn_ptr, CURRENT_PROCESS_PSN_HIGH);
bus.write_long(psn_ptr.wrapping_add(4), CURRENT_PROCESS_PSN_LOW);
0
} else if psn_high == CURRENT_PROCESS_PSN_HIGH
&& psn_low == CURRENT_PROCESS_PSN_LOW
{
bus.write_long(psn_ptr, CURRENT_PROCESS_PSN_HIGH);
bus.write_long(psn_ptr.wrapping_add(4), 0);
-600
} else {
-50
};
bus.write_word(sp + 4, err as u16);
cpu.write_reg(Register::A7, sp + 4);
Ok(())
}
0x0039 => {
// GetFrontProcess (0xA88F selector $0039)
// Returns foreground process serial number.
// FUNCTION GetFrontProcess(VAR PSN: ProcessSerialNumber): OSErr;
// Processes 1994, pp. 2-25 to 2-26
//
// MPW glue (disassembled from fixture binary):
// MOVEQ #-1, D0 / MOVE.L D0, -(SP) ; 4-byte init slot ($FFFFFFFF)
// PEA <psn> ; psn_ptr (4 bytes)
// MOVE.W #$0039, -(SP) ; selector
// DC.W $A88F / MOVE.W (SP)+, D0 ; trap → result in D0
// After selector pop: sp+0=init(4B), sp+4=psn_ptr(4B).
// Result word at sp+6; A7 = sp+6 so MOVE.W (SP)+ restores stack.
let psn_ptr = bus.read_long(sp + 4);
bus.write_long(psn_ptr, CURRENT_PROCESS_PSN_HIGH);
bus.write_long(psn_ptr.wrapping_add(4), CURRENT_PROCESS_PSN_LOW);
bus.write_word(sp + 6, 0);
cpu.write_reg(Register::A7, sp + 6);
Ok(())
}
0x003A => {
// GetProcessInformation (0xA88F selector $003A)
// Returns information about the specified process.
// FUNCTION GetProcessInformation(PSN: ProcessSerialNumber;
// VAR info: ProcessInfoRec): OSErr;
// Processes 1994, pp. 2-23 to 2-24
let info_ptr = bus.read_long(sp);
let psn_ptr = bus.read_long(sp + 4);
let psn_high = bus.read_long(psn_ptr);
let psn_low = bus.read_long(psn_ptr + 4);
let valid_psn = psn_high == CURRENT_PROCESS_PSN_HIGH
&& psn_low == CURRENT_PROCESS_PSN_LOW;
if !valid_psn {
bus.write_word(sp + 8, (-600i16) as u16); // procNotFound
cpu.write_reg(Register::A7, sp + 8);
return Some(Ok(()));
}
let app_path = self.launched_app_path.clone().unwrap_or_default();
let app_name = if app_path.is_empty() {
"Application".to_string()
} else {
super::TrapDispatcher::vfs_basename(&app_path).to_string()
};
let (app_type, app_creator, app_parent_dir_id) =
if let Some(metadata) = self.vfs_file_metadata(&app_path) {
(metadata.file_type, metadata.creator, metadata.parent_dir_id)
} else {
(
u32::from_be_bytes(*b"APPL"),
u32::from_be_bytes(*b"????"),
2,
)
};
let name_ptr = bus.read_long(info_ptr + 4);
if name_ptr != 0 {
Self::write_pstring(bus, name_ptr, &app_name);
}
let app_spec_ptr = bus.read_long(info_ptr + 56);
if app_spec_ptr != 0 {
bus.write_word(
app_spec_ptr,
super::TrapDispatcher::boot_volume_ref_num_u16(),
);
bus.write_long(app_spec_ptr + 2, app_parent_dir_id);
Self::write_pstring(bus, app_spec_ptr + 6, &app_name);
}
bus.write_long(info_ptr + 8, CURRENT_PROCESS_PSN_HIGH); // processNumber.highLongOfPSN
bus.write_long(info_ptr + 12, CURRENT_PROCESS_PSN_LOW); // processNumber.lowLongOfPSN
bus.write_long(info_ptr + 16, app_type);
bus.write_long(info_ptr + 20, app_creator);
bus.write_long(info_ptr + 24, 0); // processMode
bus.write_long(info_ptr + 28, 0x0010_0000); // processLocation
bus.write_long(info_ptr + 32, bus.ram_size()); // processSize
bus.write_long(info_ptr + 36, bus.ram_size() / 2); // processFreeMem
bus.write_long(info_ptr + 40, 0); // processLauncher.highLongOfPSN
bus.write_long(info_ptr + 44, 0); // processLauncher.lowLongOfPSN
bus.write_long(info_ptr + 48, 0); // processLaunchDate
bus.write_long(info_ptr + 52, bus.read_long(0x016A)); // processActiveTime
bus.write_word(sp + 8, 0); // noErr
cpu.write_reg(Register::A7, sp + 8);
Ok(())
}
0x003B => {
// SetFrontProcess (OSDispatch $003B)
// FUNCTION SetFrontProcess(PSN: ProcessSerialNumber): OSErr;
// Processes 1994, 2-26. The HLE runs a single app
// and cannot switch foreground processes; report
// success unconditionally.
bus.write_word(sp + 4, 0);
cpu.write_reg(Register::A7, sp + 4);
Ok(())
}
0x003C => {
// WakeUpProcess (OSDispatch $003C)
// Makes a suspended process eligible to receive CPU time.
// FUNCTION WakeUpProcess(PSN: ProcessSerialNumber): OSErr;
// Processes 1994, 2-27
// PSN is passed as const pointer (4 bytes), result is OSErr (2 bytes)
bus.write_word(sp + 4, 0); // noErr
cpu.write_reg(Register::A7, sp + 4);
Ok(())
}
0x003D => {
// SameProcess (OSDispatch $003D)
// FUNCTION SameProcess(PSN1, PSN2: ProcessSerialNumber;
// VAR result: BOOLEAN): OSErr;
// Processes 1994, 2-28. Compares two PSN records;
// sets result and returns noErr.
let result_ptr = bus.read_long(sp);
let psn2_ptr = bus.read_long(sp + 4);
let psn1_ptr = bus.read_long(sp + 8);
let p1_high = bus.read_long(psn1_ptr);
let p1_low = bus.read_long(psn1_ptr + 4);
let p2_high = bus.read_long(psn2_ptr);
let p2_low = bus.read_long(psn2_ptr + 4);
let same = p1_high == p2_high && p1_low == p2_low;
if result_ptr != 0 {
bus.write_byte(result_ptr, if same { 1 } else { 0 });
}
bus.write_word(sp + 12, 0);
cpu.write_reg(Register::A7, sp + 12);
Ok(())
}
_ => {
eprintln!(
"[PROCESS] OSDispatch unhandled selector ${:04X} (stack=${:04X} d0=${:04X})",
selector, stack_sel, d0_sel
);
return None;
}
}
}
// HighLevelFSDispatch ($AA52)
// HighLevelFSDispatch ($AA52): Selectors: 1=FSMakeFSSpec, 2=FSpOpenDF, 4=FSpCreate, 6=FSpDelete, 9=FSpSetFLock, 10=FSpRstFLock
(true, 0x252) => {
let selector = cpu.read_reg(Register::D0) & 0xFFFF;
eprintln!("[TRAP] HighLevelFSDispatch Selector={}", selector);
match selector {
1 => {
// FSMakeFSSpec ($AA52, selector 1)
// Fills in an FSSpec record. Returns fnfErr if file doesn't exist.
// FUNCTION FSMakeFSSpec(vRefNum: INTEGER; dirID: LONGINT;
// fileName: Str255; VAR spec: FSSpec): OSErr;
// Files 1992, 2-166
let sp = cpu.read_reg(Register::A7);
let spec_ptr = bus.read_long(sp);
let name_ptr = bus.read_long(sp + 4);
let dir_id = bus.read_long(sp + 8);
let requested_v_ref_num = bus.read_word(sp + 12) as i16;
let (resolved_v_ref_num, effective_dir_id) =
self.resolve_volume_and_directory(requested_v_ref_num, dir_id);
let filename = if name_ptr != 0 {
let bytes = bus.read_pstring(name_ptr);
let n = bytes.len().min(63);
bus.write_byte(spec_ptr + 6, n as u8);
bus.write_bytes(spec_ptr + 7, &bytes[..n]);
String::from_utf8_lossy(&bytes).to_string()
} else {
bus.write_byte(spec_ptr + 6, 0);
String::new()
};
bus.write_word(spec_ptr, resolved_v_ref_num as u16);
bus.write_long(spec_ptr + 2, effective_dir_id);
// dirNFErr when the resolved dirID is not a known directory.
// IM:Files 8920: "FSMakeFSSpec can return a number of other File
// Manager error codes." BasiliskII returns -120 (dirNFErr) for
// phantom dirIDs even though IM:Files 8930 lists only fnfErr as
// the documented not-found code. BasiliskII is ground truth.
// IM:Files 1992, lines 8918..8965
if !filename.is_empty()
&& effective_dir_id > 1
&& self.directory_entry_for_id(effective_dir_id).is_none()
{
bus.write_word(sp + 14, (-120i16) as u16); // dirNFErr
cpu.write_reg(Register::A7, sp + 14);
return Some(Ok(()));
}
// FSMakeFSSpec returns fnfErr if file/directory doesn't exist.
// The spec is still valid and can be used for FSpCreate.
// Files 1992, 2-166
let exists = if filename.is_empty() {
self.directory_entry_for_id(effective_dir_id).is_some()
} else {
self.find_vfs_file_for_hfs_lookup(
requested_v_ref_num,
dir_id,
&filename,
)
.is_some()
|| self
.find_vfs_rsrc_file_for_hfs_lookup(
requested_v_ref_num,
dir_id,
&filename,
)
.is_some()
|| self
.find_vfs_directory_for_hfs_lookup(
requested_v_ref_num,
dir_id,
&filename,
)
.is_some()
};
eprintln!(
"[FSSPEC] FSMakeFSSpec request_vRefNum={} resolved_vRefNum={} request_dirID={} resolved_dirID={} name='{}' exists={}",
requested_v_ref_num,
resolved_v_ref_num,
dir_id,
effective_dir_id,
filename,
exists
);
if trace_fsspec_enabled() && filename.is_empty() {
eprintln!(
"[FSSPEC] FSMakeFSSpec created directory spec for dirID={}",
effective_dir_id
);
}
if exists {
bus.write_word(sp + 14, 0); // noErr
} else {
bus.write_word(sp + 14, (-43i16) as u16); // fnfErr
}
cpu.write_reg(Register::A7, sp + 14);
Ok(())
}
2 => {
// FSpOpenDF ($AA52, selector 2)
// Opens the data fork of a file specified by an FSSpec.
// FUNCTION FSpOpenDF(spec: FSSpec; permission: SignedByte;
// VAR refNum: INTEGER): OSErr;
// Files 1992, 2-326 (FSpOpenDF subsection lines 2342..2380;
// trap macro / routine selector at lines 2818..2823;
// result codes 2369..2380 with fnfErr -43 at line 2376).
//
// Regression coverage: fsp_open_df (the
// BasiliskII-baked fnfErr golden — strong-assertion
// pattern + refNum-poison band; trace-mode pinning
// histogram bucket $A52). Real BasiliskII writes
// to *refNum on the failure path while Systemless leaves
// the VAR-out untouched. The strong-assertion err==-43
// band is the load-bearing indicator (TRUE on both);
// the refNum-poison band records the divergence.
let sp = cpu.read_reg(Register::A7);
let ref_num_ptr = bus.read_long(sp);
let permission = bus.read_word(sp + 4) as i16;
let spec_ptr = bus.read_long(sp + 6);
let filename = read_fsspec_name(bus, spec_ptr);
eprintln!(
"[TRAP] FSpOpenDF(\"{}\", perm={}) ref_num_ptr=${:08X}",
filename, permission, ref_num_ptr
);
let vref = bus.read_word(spec_ptr) as i16;
let dir_id = bus.read_long(spec_ptr + 2);
if let Some(vfs_name) =
self.find_vfs_file_for_hfs_lookup(vref, dir_id, &filename)
{
// IM:Files 9578: "If you request exclusive read/write
// permission but another access path is already open,
// PBHOpenDF returns … opWrErr as its function result."
// fsRdWrPerm=3, fsWrPerm=2, fsCurPerm=0
let wants_write = permission == 2 || permission == 3;
if wants_write {
let already_open_for_write = self
.write_refnums
.iter()
.any(|rn| self.open_files.get(rn) == Some(&vfs_name));
if already_open_for_write {
eprintln!(
"[TRAP] FSpOpenDF(\"{}\") -> opWrErr (-49)",
filename
);
bus.write_word(sp + 10, (-49i16) as u16);
cpu.write_reg(Register::A7, sp + 10);
return Some(Ok(()));
}
}
let refnum = self.next_refnum;
self.next_refnum += 1;
self.open_files.insert(refnum, vfs_name.clone());
if wants_write {
self.write_refnums.insert(refnum);
}
self.file_positions.insert(refnum, 0);
eprintln!("[TRAP] FSpOpenDF -> refnum={} vfs=\"{}\"", refnum, vfs_name);
bus.write_word(ref_num_ptr, refnum);
bus.write_word(sp + 10, 0); // noErr
cpu.write_reg(Register::A7, sp + 10);
} else {
eprintln!("[TRAP] FSpOpenDF(\"{}\") -> fnfErr", filename);
bus.write_word(sp + 10, (-43i16) as u16); // fnfErr
cpu.write_reg(Register::A7, sp + 10);
}
Ok(())
}
4 => {
// FSpCreate ($AA52, selector 4)
// Creates a new file (both forks) by FSSpec.
// FUNCTION FSpCreate(spec: FSSpec; creator: OSType;
// fileType: OSType; scriptTag: ScriptCode): OSErr;
// Files 1992, 8480..8531 (function 8484; trap macro /
// selector 8510..8514; result codes 8517..8531 with
// dirNFErr at 8529 and dupFNErr at 8528).
//
// Returns dupFNErr (-48) if a case-insensitive
// match already exists in VFS, noErr otherwise.
// IM:Files 8528 (dupFNErr result code).
let sp = cpu.read_reg(Register::A7);
let spec_ptr = bus.read_long(sp + 10);
let file_type = bus.read_long(sp + 2);
let creator = bus.read_long(sp + 6);
// Phantom parID → BasiliskII returns dupFNErr (-48).
// IM:Files 8529 documents dirNFErr (-120) for a missing parent
// directory; however BasiliskII (extfs over HFS) empirically
// returns -48 (dupFNErr) for phantom parIDs. BasiliskII is
// ground truth per the golden gate.
// IM:Files 1992, lines 8510..8531
let filename = read_fsspec_name(bus, spec_ptr);
let vref = bus.read_word(spec_ptr) as i16;
let dir_id = bus.read_long(spec_ptr + 2);
if trace_fsspec_enabled() {
eprintln!(
"[FSSPEC] FSpCreateResFile file='{}' vref={} dir_id={}",
filename, vref, dir_id
);
}
let par_id = dir_id;
if par_id > 1 && self.directory_path_for_id(par_id).is_none() {
if trace_fsspec_enabled() {
eprintln!(
"[FSSPEC] FSpCreateResFile rejecting par_id={} for file='{}'",
par_id, filename
);
}
bus.write_word(sp + 14, (-48i16) as u16); // dupFNErr
cpu.write_reg(Register::A7, sp + 14);
return Some(Ok(()));
}
let vfs_key = self
.vfs_key_for_fsspec(vref, dir_id, &filename)
.unwrap_or_else(|| {
super::TrapDispatcher::normalize_vfs_path(&filename)
});
if trace_fsspec_enabled() {
eprintln!(
"[FSSPEC] FSpCreateResFile file='{}' vref={} dir_id={} vfs_key='{}'",
filename, vref, dir_id, vfs_key
);
}
// dupFNErr (-48): file already exists in VFS.
// IM:Files 8528; matches BasiliskII empirical behaviour.
let already_exists = self.vfs.contains_key(&vfs_key)
|| self.vfs.keys().any(|k| k.eq_ignore_ascii_case(&vfs_key));
if already_exists {
bus.write_word(sp + 14, (-48i16) as u16); // dupFNErr
} else {
self.vfs.insert(vfs_key.clone(), Vec::new());
self.vfs_rsrc.entry(vfs_key.clone()).or_default();
self.set_vfs_entry_finfo(&vfs_key, file_type, creator, 0);
self.touch_vfs_entry(&vfs_key);
if let Some(ref dir) = self.output_dir {
let host_path = dir.join(&vfs_key);
let _ = std::fs::File::create(&host_path);
}
bus.write_word(sp + 14, 0); // noErr
}
cpu.write_reg(Register::A7, sp + 14);
Ok(())
}
6 => {
// FSpDelete ($AA52, selector 6)
// Removes a file or directory specified by an FSSpec.
// FUNCTION FSpDelete(spec: FSSpec): OSErr;
// Files 1992, 2-329; FSpDelete subsection at IM:Files
// 8576..8610 (function signature 8580; trap macro/
// selector 8590..8594; result codes 8597..8610 with
// fnfErr at 8603, fBsyErr at 8607, dirNFErr at 8608).
//
// Regression coverage: aa52_fsp_delete_busy (the
// BasiliskII-baked golden exercises the delete-while-
// open path — BasiliskII on Unix deletes the open file
// with noErr, second FSpDelete returns fnfErr -43).
let sp = cpu.read_reg(Register::A7);
let spec_ptr = bus.read_long(sp);
let filename = read_fsspec_name(bus, spec_ptr);
let vref = bus.read_word(spec_ptr) as i16;
let dir_id = bus.read_long(spec_ptr + 2);
let found_in_vfs = if let Some(vfs_name) =
self.find_vfs_file_for_hfs_lookup(vref, dir_id, &filename)
{
self.vfs.remove(&vfs_name);
self.vfs_rsrc.remove(&vfs_name);
self.remove_vfs_entry_metadata(&vfs_name);
true
} else if let Some(vfs_name) = self.find_vfs_file(&filename) {
self.vfs.remove(&vfs_name);
self.vfs_rsrc.remove(&vfs_name);
self.remove_vfs_entry_metadata(&vfs_name);
true
} else {
false
};
let found_on_host = if let Some(ref dir) = self.output_dir {
let host_path = self
.vfs_key_for_fsspec(vref, dir_id, &filename)
.map(|key| dir.join(key))
.unwrap_or_else(|| dir.join(&filename));
std::fs::remove_file(&host_path).is_ok()
} else {
false
};
// fnfErr (-43) when file not found anywhere.
// IM:Files 8603: result code fnfErr.
let err: i16 = if found_in_vfs || found_on_host {
0
} else {
-43
};
bus.write_word(sp + 4, err as u16);
cpu.write_reg(Register::A7, sp + 4);
Ok(())
}
7 => {
// FSpGetFInfo ($AA52, selector 7)
// Returns Finder information for a file specified by an FSSpec.
// FUNCTION FSpGetFInfo(spec: FSSpec; VAR fndrInfo: FInfo): OSErr;
// Files 1992, 2-327; FSpGetFInfo subsection at IM:Files 8616..8650
// (function signature 8620; trap macro/selector 8630..8631; result
// codes 8643..8650 with fnfErr at 8645).
//
// Regression coverage: fsp_get_finfo. Strengthened
// from `legacy_smoke` (compare=trace, no asserts) to
// `behavior_state` (compare=pixmap, 1bpp offscreen indicator
// bands) per the catalogue gate. Two-band pixmap proves the
// missing-file path returns fnfErr (-43) AND the companion
// FSMakeFSSpec dispatch populated the spec — both bands non-
// degenerate vs a no-op stub.
//
// BasiliskII mutates the caller's `fndrInfo` buffer on the
// fnfErr branch instead of leaving the pre-poisoned bytes
// untouched. Mirror that observed mutation here so callers do
// not read stale poison after a missing-file probe.
let sp = cpu.read_reg(Register::A7);
let finfo_ptr = bus.read_long(sp);
let spec_ptr = bus.read_long(sp + 4);
let filename = read_fsspec_name(bus, spec_ptr);
eprintln!("[TRAP] FSpGetFInfo file='{}'", filename);
let vref = bus.read_word(spec_ptr) as i16;
let dir_id = bus.read_long(spec_ptr + 2);
let vfs_name = self
.find_vfs_file_for_hfs_lookup(vref, dir_id, &filename)
.or_else(|| {
self.find_vfs_rsrc_file_for_hfs_lookup(vref, dir_id, &filename)
});
let directory_name =
self.find_vfs_directory_for_hfs_lookup(vref, dir_id, &filename);
if directory_name.is_some() {
// Directory specs are valid for FSpGetFInfo.
// Finder type/creator for directories follows
// the same canonical values used by PBGetCatInfo.
Self::write_finfo(
bus,
finfo_ptr,
u32::from_be_bytes(*b"fold"),
u32::from_be_bytes(*b"MACS"),
0,
);
bus.write_word(sp + 8, 0); // noErr
} else if let Some(vfs_name) = vfs_name {
if let Some(metadata) = self.vfs_file_metadata(&vfs_name) {
Self::write_finfo(
bus,
finfo_ptr,
metadata.file_type,
metadata.creator,
metadata.finder_flags,
);
}
bus.write_word(sp + 8, 0); // noErr
} else {
eprintln!("[TRAP] FSpGetFInfo(\"{}\") -> fnfErr", filename);
Self::write_finfo(bus, finfo_ptr, 0, 0, 0);
bus.write_word(sp + 8, (-43i16) as u16); // fnfErr
}
cpu.write_reg(Register::A7, sp + 8);
Ok(())
}
8 => {
// FSpSetFInfo (selector 8)
// FUNCTION FSpSetFInfo(spec: FSSpec; fndrInfo: FInfo): OSErr;
// Stack: [result(2)] [fndrInfo_ptr(4)] [spec_ptr(4)] — pop 8, leave 2
// More Macintosh Toolbox, 1-89
let sp = cpu.read_reg(Register::A7);
let finfo_ptr = bus.read_long(sp);
let spec_ptr = bus.read_long(sp + 4);
let filename = read_fsspec_name(bus, spec_ptr);
let vref = bus.read_word(spec_ptr) as i16;
let dir_id = bus.read_long(spec_ptr + 2);
if let Some(vfs_name) =
self.find_vfs_file_for_hfs_lookup(vref, dir_id, &filename)
{
let file_type = bus.read_long(finfo_ptr);
let creator = bus.read_long(finfo_ptr + 4);
let finder_flags = bus.read_word(finfo_ptr + 8);
self.set_vfs_entry_finfo(&vfs_name, file_type, creator, finder_flags);
}
bus.write_word(sp + 8, 0); // noErr
cpu.write_reg(Register::A7, sp + 8);
Ok(())
}
9 => {
// FSpSetFLock ($AA52, selector 9)
// Locks a file — new access paths become read-only.
// FUNCTION FSpSetFLock(spec: FSSpec): OSErr;
// Inside Macintosh: Files 1992, 8681..8713
let sp = cpu.read_reg(Register::A7);
let spec_ptr = bus.read_long(sp);
let filename = read_fsspec_name(bus, spec_ptr);
let vref = bus.read_word(spec_ptr) as i16;
let dir_id = bus.read_long(spec_ptr + 2);
let err: i16 = if let Some(vfs_name) =
self.find_vfs_file_for_hfs_lookup(vref, dir_id, &filename)
{
self.locked_files.insert(vfs_name);
0
} else if self.find_vfs_file(&filename).is_some() {
let normalized = super::TrapDispatcher::normalize_vfs_path(&filename);
self.locked_files.insert(normalized);
0
} else {
-43 // fnfErr
};
eprintln!("[TRAP] FSpSetFLock(\"{}\") -> {}", filename, err);
bus.write_word(sp + 4, err as u16);
cpu.write_reg(Register::A7, sp + 4);
Ok(())
}
10 => {
// FSpRstFLock ($AA52, selector 10)
// Unlocks a file.
// FUNCTION FSpRstFLock(spec: FSSpec): OSErr;
// Inside Macintosh: Files 1992, 8716..8748
let sp = cpu.read_reg(Register::A7);
let spec_ptr = bus.read_long(sp);
let filename = read_fsspec_name(bus, spec_ptr);
let vref = bus.read_word(spec_ptr) as i16;
let dir_id = bus.read_long(spec_ptr + 2);
if let Some(vfs_name) =
self.find_vfs_file_for_hfs_lookup(vref, dir_id, &filename)
{
self.locked_files.remove(&vfs_name);
} else if let Some(vfs_name) = self.find_vfs_file(&filename) {
self.locked_files.remove(&vfs_name);
}
eprintln!("[TRAP] FSpRstFLock(\"{}\")", filename);
bus.write_word(sp + 4, 0u16); // noErr
cpu.write_reg(Register::A7, sp + 4);
Ok(())
}
13 => {
// FSpOpenResFile (selector 13 = $000D)
// FUNCTION FSpOpenResFile(spec: FSSpec; permission: SignedByte): INTEGER;
// Inside Macintosh Volume VI, 9-43; More Macintosh Toolbox, 1-58
// Stack (rightmost on top): [permission(2)] [spec_ptr(4)] [result(2)]
let sp = cpu.read_reg(Register::A7);
let _permission = bus.read_word(sp);
let spec_ptr = bus.read_long(sp + 2);
let filename = read_fsspec_name(bus, spec_ptr);
if super::dispatch::trace_resfile_enabled() {
eprintln!("[TRAP] FSpOpenResFile file='{}'", filename);
}
let vref = bus.read_word(spec_ptr) as i16;
let dir_id = bus.read_long(spec_ptr + 2);
let rsrc_key =
self.find_vfs_rsrc_file_for_hfs_lookup(vref, dir_id, &filename);
let data_key = self.find_vfs_file_for_hfs_lookup(vref, dir_id, &filename);
if trace_fsspec_enabled() {
eprintln!(
"[FSSPEC] FSpOpenResFile file='{}' rsrc_match={:?} data_match={:?}",
filename,
rsrc_key,
data_key
);
}
if let Some(vfs_key) = rsrc_key {
// Dedupe: re-opening the same fork must reuse
// the existing refnum, not re-allocate every
// resource, and must not change CurResFile.
// (See OpenRFPerm in toolbox.rs.)
if let Some(existing) = self.refnum_for_resource_file_name(&vfs_key) {
if super::dispatch::trace_resfile_enabled() {
eprintln!(
"[TRAP] FSpOpenResFile: \"{}\" already open as refnum {}, dedup",
filename, existing
);
}
bus.write_word(sp + 6, existing);
cpu.write_reg(Register::D0, existing as u32);
bus.write_word(0x0A60, 0); // ResErr = noErr
cpu.write_reg(Register::A7, sp + 6);
return Some(Ok(()));
}
let rsrc_data =
self.vfs_rsrc.get(&vfs_key).cloned().unwrap_or_default();
let refnum = self.next_refnum;
self.next_refnum += 1;
if !rsrc_data.is_empty() {
if let Some(fork) =
crate::managers::resource::ResourceFork::parse(&rsrc_data)
{
self.merge_resources_from_fork(&fork, bus, refnum);
} else {
self.register_empty_resource_file(refnum);
}
} else {
self.register_empty_resource_file(refnum);
}
self.set_resource_file_name(refnum, vfs_key.clone());
bus.write_word(sp + 6, refnum);
cpu.write_reg(Register::D0, refnum as u32);
bus.write_word(0x0A60, 0); // ResErr = noErr
self.set_current_resource_refnum(bus, refnum);
} else {
// FSpOpenResFile failure semantics:
// - If the file exists but has no resource fork, return
// resFNotFound (-193): "resource file not found".
// - If the file itself is missing, return fnfErr (-43).
//
// IM:Volume VI (1991), Resource Manager 13-19..13-20:
// FSpOpenResFile uses HOpenResFile result codes and
// returns -1 on failure; those include fnfErr for
// missing files. Keep the existing data-only
// resFNotFound behavior for compatibility with the
// OpenResFile/HOpenResFile path.
bus.write_word(sp + 6, (-1i16) as u16);
cpu.write_reg(Register::D0, (-1i32) as u32);
let res_err: i16 = if data_key.is_some() { -193 } else { -43 };
bus.write_word(0x0A60, res_err as u16);
}
cpu.write_reg(Register::A7, sp + 6);
Ok(())
}
14 => {
// FSpCreateResFile (selector 14 = $000E)
// PROCEDURE FSpCreateResFile(spec: FSSpec; creator: OSType;
// fileType: OSType; scriptTag: ScriptCode);
// Stack: [scriptTag(2)] [fileType(4)] [creator(4)] [spec_ptr(4)] — pop 14
// More Macintosh Toolbox, 1-56
let sp = cpu.read_reg(Register::A7);
let file_type = bus.read_long(sp + 2);
let creator = bus.read_long(sp + 6);
let spec_ptr = bus.read_long(sp + 10);
let filename = read_fsspec_name(bus, spec_ptr);
if !filename.is_empty() {
let normalized = super::TrapDispatcher::normalize_vfs_path(&filename);
self.vfs.entry(normalized.clone()).or_default();
self.vfs_rsrc.entry(normalized.clone()).or_default();
self.set_vfs_entry_finfo(&normalized, file_type, creator, 0);
self.touch_vfs_entry(&normalized);
if let Some(ref dir) = self.output_dir {
let host_path = dir.join(&normalized);
if let Some(parent) = host_path.parent() {
let _ = std::fs::create_dir_all(parent);
}
let _ = std::fs::write(host_path, []);
}
}
bus.write_word(0x0A60, 0); // ResErr = noErr
cpu.write_reg(Register::A7, sp + 14);
Ok(())
}
_ => {
eprintln!(
"[TRAP] HighLevelFSDispatch: Unimplemented Selector {}",
selector
);
Err(Error::Halted)
}
}
}
// FSDispatch ($A060) / HFSDispatch ($A260)
// HFSDispatch / FSDispatch ($A060): Selectors 1=PBOpenWD, 2=PBCloseWD, 7=PBGetWDInfo, 8=PBGetFCBInfo, 9=PBGetCatInfo, 26=PBHOpenDF; by-name lookups retry via default/WD directory
(false, 0x60) => {
let selector = cpu.read_reg(Register::D0) & 0xFFFF;
let pb = cpu.read_reg(Register::A0);
if selector == 1 {
// PBOpenWD ($A260, selector 1)
// Creates a working directory for the specified directory.
// FUNCTION PBOpenWD(paramBlock: WDPBPtr; async: Boolean): OSErr;
// Files 1992, 2-201 to 2-202
let name_ptr = bus.read_long(pb + 18);
let name = Self::read_pb_filename(bus, name_ptr);
let vref = bus.read_word(pb + 22) as i16;
let proc_id = bus.read_long(pb + 28);
let requested_dir_id = bus.read_long(pb + 48);
let base_dir_id = self.resolve_directory_id(vref, requested_dir_id);
let effective_dir_id = if name.is_empty() {
base_dir_id
} else if let Some(path) =
self.find_vfs_directory_in_directory(base_dir_id, &name)
{
self.vfs_directories
.get(&path)
.map(|directory| directory.dir_id)
.unwrap_or(base_dir_id)
} else {
eprintln!(
"[TRAP] FSDispatch PBOpenWD name='{}' vref={} dirID={} -> fnfErr",
name, vref, requested_dir_id
);
bus.write_word(pb + 16, (-43i16) as u16);
cpu.write_reg(Register::D0, (-43i32) as u32);
return Some(Ok(()));
};
if let Some(wd_ref_num) =
self.open_working_directory(vref, effective_dir_id, proc_id)
{
let volume_ref_num = self.resolve_volume_ref_num(vref);
eprintln!(
"[TRAP] FSDispatch PBOpenWD name='{}' vref={} dirID={} -> wdRefNum={} resolvedDirID={}",
name, vref, requested_dir_id, wd_ref_num, effective_dir_id
);
bus.write_word(pb + 22, wd_ref_num as u16);
bus.write_long(pb + 28, proc_id);
bus.write_word(pb + 32, volume_ref_num as u16);
bus.write_long(pb + 48, effective_dir_id);
bus.write_word(pb + 16, 0);
cpu.write_reg(Register::D0, 0);
} else {
bus.write_word(pb + 16, (-43i16) as u16);
cpu.write_reg(Register::D0, (-43i32) as u32);
}
return Some(Ok(()));
}
if selector == 2 {
// PBCloseWD ($A260, selector 2)
// Releases a working directory reference number.
// FUNCTION PBCloseWD(paramBlock: WDPBPtr; async: Boolean): OSErr;
// Files 1992, 2-202 to 2-203
//
// Regression coverage:
// pb_close_wd — BasiliskII golden
// via dispatch-via-poisoned-ioResult indicator on
// phantom ioVRefNum=-999. Real Mac returns noErr (0)
// per IM:Files 10366 ("If you specify a volume
// reference number in the ioVRefNum field, PBCloseWD
// does nothing") — the close-side mirror of
// PBGetWDInfo's polymorphic-input divergence (where
// real Mac returned nsvErr -35 instead of rfNumErr
// -51). Systemless returns rfNumErr (-51) on the
// HashMap-miss fallback path below. The trace gate
// (histogram bucket $260) pins trap-word dispatch
// without forcing pixel-perfect match across the
// value-divergence.
let wd_ref_num = bus.read_word(pb + 22) as i16;
eprintln!("[TRAP] FSDispatch PBCloseWD wdRefNum={}", wd_ref_num);
let err = if wd_ref_num == super::TrapDispatcher::boot_volume_ref_num()
|| self.close_working_directory(wd_ref_num)
{
0
} else {
-51i16
};
bus.write_word(pb + 16, err as u16);
cpu.write_reg(Register::D0, err as i32 as u32);
return Some(Ok(()));
}
if selector == 7 {
// PBGetWDInfo ($A260, selector 7)
// Converts a working directory reference number to volume/directory info.
// FUNCTION PBGetWDInfo(paramBlock: WDPBPtr; async: Boolean): OSErr;
// Files 1992, 2-203 to 2-204
//
// Regression coverage:
// pb_get_wd_info — BasiliskII golden via
// dispatch-via-poisoned-ioResult indicator on phantom
// ioVRefNum=-999 + ioWDIndex=0. Real Mac returns nsvErr (-35) —
// interprets -999 as a vRefNum (outside the wdRefNum range per
// IM:Files 6633) — Systemless returns rfNumErr (-51). The trace
// gate (histogram bucket $260) pins trap-word dispatch without
// forcing pixel-perfect match across the value-divergence.
let name_ptr = bus.read_long(pb + 18);
let input_vref = bus.read_word(pb + 22) as i16;
let wd_index = bus.read_word(pb + 26) as i16;
let info = if wd_index > 0 {
self.working_directory_by_index(wd_index, input_vref)
} else if input_vref == 0 {
Some(super::dispatch::WorkingDirectory {
ref_num: self
.open_working_directory(
super::TrapDispatcher::boot_volume_ref_num(),
self.default_dir_id,
0,
)
.unwrap_or(super::TrapDispatcher::boot_volume_ref_num()),
volume_ref_num: super::TrapDispatcher::boot_volume_ref_num(),
dir_id: self.default_dir_id,
proc_id: 0,
})
} else {
self.working_directory_info(input_vref)
};
if let Some(working_directory) = info {
let returned_vref = if wd_index > 0 {
working_directory.volume_ref_num
} else {
working_directory.ref_num
};
eprintln!(
"[TRAP] FSDispatch PBGetWDInfo input_vRefNum={} ioWDIndex={} -> returned_vRefNum={} ioWDVRefNum={} ioWDDirID={} ioWDProcID=${:08X}",
input_vref,
wd_index,
returned_vref,
working_directory.volume_ref_num,
working_directory.dir_id,
working_directory.proc_id
);
if name_ptr != 0 {
Self::write_pstring(
bus,
name_ptr,
super::TrapDispatcher::boot_volume_name(),
);
}
bus.write_word(pb + 22, returned_vref as u16);
bus.write_long(pb + 28, working_directory.proc_id);
bus.write_word(pb + 32, working_directory.volume_ref_num as u16);
bus.write_long(pb + 48, working_directory.dir_id);
bus.write_word(pb + 16, 0);
cpu.write_reg(Register::D0, 0);
} else {
eprintln!(
"[TRAP] FSDispatch PBGetWDInfo input_vRefNum={} ioWDIndex={} -> rfNumErr",
input_vref, wd_index
);
bus.write_word(pb + 16, (-51i16) as u16);
cpu.write_reg(Register::D0, (-51i32) as u32);
}
return Some(Ok(()));
}
let name_ptr = bus.read_long(pb + 18);
let vref = bus.read_word(pb + 22) as i16;
let dir_id = bus.read_long(pb + 48);
let filename = Self::read_pb_filename(bus, name_ptr);
let fdir_index = bus.read_word(pb + 28) as i16;
eprintln!(
"[TRAP] FSDispatch selector={} name_ptr=${:08X} filename=\"{}\" vref={} dirID={} fdirIdx={}",
selector, name_ptr, filename, vref, dir_id, fdir_index
);
let (resolved_vref, effective_dir_id) =
self.resolve_volume_and_directory(vref, dir_id);
// PBGetFCBInfo (selector 8): returns info about an open file control block.
// Input: ioRefNum (offset 24) = file reference number; ioFCBIndx (offset 28)
// for indexed access if ioRefNum=0.
// Output: ioNamePtr filled with filename, ioVRefNum, ioFCBParID, etc.
// Files 1992, 2-241 to 2-243
//
// Regression coverage:
// pb_get_fcb_info — BasiliskII golden via
// dispatch-via-poisoned-ioResult indicator on phantom
// ioRefNum=-999 + ioFCBIndx=0. Real Mac returns rfNumErr (-51) —
// treats -999 as a categorically-invalid refnum per IM:Files
// 11733..11736 — Systemless returns fnOpnErr (-38) per the
// open_files HashMap miss at line ~4089. The trace gate
// (histogram bucket $260) pins trap-word dispatch without
// forcing pixel-perfect match across the value-divergence.
// Same polymorphic-input divergence class as PBGetWDInfo
// selector $0007 (rfNumErr-vs-nsvErr split).
if selector == 8 {
let ref_num = bus.read_word(pb + 24);
let fcb_index = bus.read_word(pb + 28) as i16;
if !filename.is_empty() || dir_id != 0 {
if let Some(entry) =
self.lookup_catalog_entry_for_hfs_lookup(vref, dir_id, &filename, 0)
{
if entry.is_directory {
if let Some(directory) = self.vfs_directories.get(&entry.path) {
Self::fill_directory_catalog_info(bus, pb, directory);
}
} else if let Some(metadata) = self.vfs_file_metadata(&entry.path) {
self.fill_file_catalog_info(bus, pb, &entry.path, metadata);
}
if name_ptr != 0 {
Self::write_pstring(bus, name_ptr, entry.name.as_str());
}
bus.write_word(pb + 22, resolved_vref as u16);
bus.write_long(pb + 48, effective_dir_id);
bus.write_word(pb + 16, 0);
cpu.write_reg(Register::D0, 0);
} else {
bus.write_word(pb + 16, (-43i16) as u16);
cpu.write_reg(Register::D0, (-43i32) as u32);
}
return Some(Ok(()));
}
eprintln!(
"[TRAP] FSDispatch PBGetFCBInfo refNum={} fcbIndx={}",
ref_num, fcb_index
);
// Look up the file by reference number.
// refNum 0 = the app's resource file.
let resolved_name = if ref_num == 0 {
// App's own resource file
self.launched_app_path.clone()
} else {
self.open_files.get(&ref_num).cloned()
};
if let Some(vfs_name) = resolved_name {
let base_name = super::TrapDispatcher::vfs_basename(&vfs_name).to_string();
let parent_dir_id = self
.vfs_file_metadata(&vfs_name)
.map(|m| m.parent_dir_id)
.unwrap_or(2);
if name_ptr != 0 {
Self::write_pstring(bus, name_ptr, &base_name);
}
bus.write_word(pb + 22, super::TrapDispatcher::boot_volume_ref_num_u16()); // ioVRefNum
bus.write_word(pb + 30, 0); // filler1
bus.write_long(pb + 32, 0); // ioFCBFlNm (file number)
bus.write_word(pb + 36, 0x0200); // ioFCBFlags (resource fork)
bus.write_word(pb + 38, 0); // ioFCBStBlk
bus.write_long(pb + 40, 0); // ioFCBEOF
bus.write_long(pb + 44, 0); // ioFCBPLen
bus.write_long(pb + 48, 0); // ioFCBCrPs
bus.write_word(pb + 52, super::TrapDispatcher::boot_volume_ref_num_u16()); // ioFCBVRefNum
bus.write_long(pb + 54, 0); // ioFCBClpSiz
bus.write_long(pb + 58, parent_dir_id); // ioFCBParID
eprintln!(
"[TRAP] FSDispatch PBGetFCBInfo -> name=\"{}\" parentDirID={}",
base_name, parent_dir_id
);
bus.write_word(pb + 16, 0); // noErr
cpu.write_reg(Register::D0, 0);
} else {
eprintln!(
"[TRAP] FSDispatch PBGetFCBInfo: refNum={} not found",
ref_num
);
bus.write_word(pb + 16, (-38i16) as u16); // fnOpnErr
cpu.write_reg(Register::D0, (-38i32) as u32);
}
return Some(Ok(()));
}
// PBGetCatInfo (selector 9): returns catalog information.
// Supports indexed enumeration and directory IDs.
// Files 1992, 2-190 to 2-192
//
// Regression coverage:
// pb_get_cat_info — BasiliskII-baked
// missing-file failure path. Pre-poisons
// ioResult = 0x3FFF, dispatches with a phantom
// filename, asserts ioResult overwritten — pins
// selector $0009 dispatch via the trace-mode
// histogram bucket $260 = 1 alongside the visual
// indicator. Per IM:Files 9944..9952 the
// documented errors are noErr / nsvErr / ioErr /
// bdNamErr / fnfErr / paramErr / dirNFErr; the
// impl below writes -43 fnfErr on lookup miss.
if selector == 9 {
let lookup = self
.lookup_catalog_entry_for_hfs_lookup(vref, dir_id, &filename, fdir_index);
if let Some(entry) = lookup {
if entry.is_directory {
if let Some(directory) = self.vfs_directories.get(&entry.path) {
Self::fill_directory_catalog_info(bus, pb, directory);
eprintln!(
"[TRAP] FSDispatch PBGetCatInfo dir \"{}\" -> dirID={} parentDirID={}",
entry.name, directory.dir_id, directory.parent_dir_id
);
}
} else if let Some(metadata) = self.vfs_file_metadata(&entry.path) {
self.fill_file_catalog_info(bus, pb, &entry.path, metadata);
eprintln!(
"[TRAP] FSDispatch PBGetCatInfo file \"{}\" -> fileID={} parentDirID={}",
entry.name, metadata.file_id, metadata.parent_dir_id
);
}
if name_ptr != 0 {
Self::write_pstring(bus, name_ptr, &entry.name);
}
bus.write_word(pb + 22, resolved_vref as u16);
bus.write_word(pb + 16, 0);
cpu.write_reg(Register::D0, 0);
} else {
bus.write_word(pb + 16, (-43i16) as u16); // fnfErr
cpu.write_reg(Register::D0, (-43i32) as u32);
}
return Some(Ok(()));
}
// Selector 26 = PBHOpenDF: open data fork by name
// PBHOpenDF opens the data fork of a file specified by name, vRefNum, and dirID.
// Files 1992, 2-271
//
// Regression coverage:
// pbh_open_df — BasiliskII-baked
// missing-file failure path. Pairs the strong
// err == -43 assertion with the strong
// ioResult == -43 assertion (both render TRUE
// under BasiliskII System 7.5.3 + extfs); pins
// selector $001A dispatch via the trace-mode
// histogram bucket $260 = 1 alongside the visual
// indicator. Per IM:Files 9590..9598 the
// documented errors are noErr / nsvErr / ioErr /
// bdNamErr / tmfoErr / fnfErr (-43) / opWrErr /
// permErr / dirNFErr / afpAccessDenied; the impl
// below writes -43 fnfErr on lookup miss.
if selector == 26 && !filename.is_empty() {
// Clear ioRefNum upfront. Files 1992 leaves the output
// undefined on failure, but MPW's FSOpen glue writes
// `*refNum = pb.ioRefNum` regardless of the result
// code — clearing makes the failure path deterministic.
bus.write_word(pb + 24, 0);
if let Some(vfs_name) =
self.find_vfs_file_for_hfs_lookup(vref, dir_id, &filename)
{
let refnum = self.next_refnum;
self.next_refnum += 1;
self.open_files.insert(refnum, vfs_name.clone());
self.file_positions.insert(refnum, 0);
bus.write_word(pb + 24, refnum); // ioRefNum
bus.write_word(pb + 16, 0);
cpu.write_reg(Register::D0, 0);
eprintln!(
"[TRAP] FSDispatch PBHOpenDF(\"{}\") -> refnum={} vfs=\"{}\"",
filename, refnum, vfs_name
);
return Some(Ok(()));
} else {
eprintln!("[TRAP] FSDispatch PBHOpenDF(\"{}\") -> fnfErr", filename);
bus.write_word(pb + 16, (-43i16) as u16);
cpu.write_reg(Register::D0, (-43i32) as u32);
return Some(Ok(()));
}
}
// Return noErr via PB ioResult and D0
bus.write_word(pb + 16, 0);
cpu.write_reg(Register::D0, 0);
Ok(())
}
// InitFS ($A06C)
// Public A-trap constant from the Mac OS SDK trap table.
// Systemless does not model any file-system init side effects here;
// the caller-visible contract we keep is a no-op that returns
// noErr and preserves the caller's stack.
//
// MPW Universal Headers `Traps.h` publishes `_InitFS = 0xA06C`.
(false, 0x6C) => {
cpu.write_reg(Register::D0, 0);
Ok(())
}
_ => return None,
})
}
/// Shared implementation of SizeResource ($A9A5) and MaxSizeRsrc ($A821).
///
/// Pascal signature is identical for both:
/// FUNCTION (theResource: Handle): LONGINT;
/// Stack on entry:
/// SP theResource (Handle, 4 bytes)
/// SP + 4 result (LongInt, 4 bytes — caller-allocated)
/// Pops 4 bytes of args, leaves the 4-byte result slot.
///
/// IM:I-121 (SizeResource) and IM:IV-16 (MaxSizeRsrc) document a
/// disk-vs-map distinction (SizeResource may load the resource;
/// MaxSizeRsrc reads from the map only) which collapses in our
/// memory-resident HLE — every handle we know about points at a
/// real guest-bus allocation. Both traps therefore return the
/// same byte count from `bus.get_alloc_size`. The contract test
/// `maxsizersrc_matches_sizeresource_for_loaded_resource` is the
/// gate that pins the equivalence.
fn handle_resource_size_query<C: CpuOps>(
&self,
bus: &mut MacMemoryBus,
cpu: &mut C,
) -> Result<()> {
let sp = cpu.read_reg(Register::A7);
let handle = bus.read_long(sp);
let size: i32 = if let Some((ptr, _, _)) = self.loaded_handles.get(&handle).copied() {
// IM:I I-121 keys validity on whether the handle is a Resource
// Manager handle, not on whether the handle's master pointer is
// currently non-NIL (SetResLoad(FALSE) can return empty handles).
bus.get_alloc_size(ptr).map(|s| s as i32).unwrap_or(-1)
} else {
-1
};
if size < 0 {
bus.write_word(0x0A60, (-192i16) as u16); // ResErr = resNotFound
} else {
bus.write_word(0x0A60, 0); // ResErr = noErr
}
bus.write_long(sp + 4, size as u32);
cpu.write_reg(Register::A7, sp + 4);
Ok(())
}
/// Selector 1 of _ResourceDispatch ($A822). Copies `count` bytes
/// from (*handle + offset) to `buffer`. Returns the ResErr value
/// to write back to $0A60.
/// More Macintosh Toolbox 1993, 1-69.
fn read_partial_resource(
&self,
bus: &mut MacMemoryBus,
handle: u32,
offset: i32,
buffer: u32,
count: i32,
) -> i16 {
if handle == 0 {
return Self::RES_NOT_FOUND;
}
let ptr = bus.read_long(handle);
if ptr == 0 {
return Self::RES_NOT_FOUND;
}
let size = bus.get_alloc_size(ptr).unwrap_or(0) as i64;
// MMTB 1-69: "If you try to read past the end of a resource
// or the value of the offset parameter is out of bounds,
// ResError returns the result code inputOutOfBounds (-190)."
if offset < 0 || count < 0 {
return -190;
}
let end = offset as i64 + count as i64;
if end > size {
return -190;
}
if count == 0 {
return 0;
}
let bytes = bus.read_bytes(ptr + offset as u32, count as usize);
bus.write_bytes(buffer, &bytes);
// The classic Resource Manager reports `resourceInMemory`
// after a partial read from a resource that is already loaded.
-188
}
/// Selector 2 of _ResourceDispatch ($A822). Copies `count` bytes
/// from `buffer` into the resource starting at `offset`. If the
/// write would extend past the current resource size, the
/// allocation is grown to fit and `writingPastEnd` (-189) is
/// returned per MMTB 1-70.
fn write_partial_resource(
&mut self,
bus: &mut MacMemoryBus,
handle: u32,
offset: i32,
buffer: u32,
count: i32,
) -> i16 {
if handle == 0 {
return Self::RES_NOT_FOUND;
}
let ptr = bus.read_long(handle);
if ptr == 0 {
return Self::RES_NOT_FOUND;
}
if offset < 0 || count < 0 {
return -190;
}
if count == 0 {
// Zero-length writes are no-ops even if the offset is past
// the current resource tail.
return 0;
}
let cur_size = bus.get_alloc_size(ptr).unwrap_or(0) as i64;
let end = offset as i64 + count as i64;
let mut past_end = false;
let live_ptr = if end > cur_size {
past_end = true;
self.resize_resource_allocation(bus, handle, ptr, end as u32)
} else {
ptr
};
if live_ptr == 0 {
// Allocation failed when extending — surface a memory
// error in the File Manager band per MMTB 1-70.
return -108; // memFullErr
}
if count > 0 {
let bytes = bus.read_bytes(buffer, count as usize);
bus.write_bytes(live_ptr + offset as u32, &bytes);
}
if past_end {
-189
} else {
0
}
}
/// Selector 3 of _ResourceDispatch ($A822). Resizes the resource
/// allocation to `new_size`, preserving as many bytes as fit.
/// MMTB 1-71.
fn set_resource_size(&mut self, bus: &mut MacMemoryBus, handle: u32, new_size: i32) -> i16 {
if handle == 0 {
return Self::RES_NOT_FOUND;
}
if new_size < 0 {
// Out-of-band size; treat as input error. MMTB doesn't
// document negative sizes, but the partial-resource
// family already uses inputOutOfBounds for invalid
// numeric arguments per IM:VI 1-69.
return -190;
}
let ptr = bus.read_long(handle);
if ptr == 0 {
// Empty handle (e.g. SetResLoad(FALSE) + GetResource).
// Resize the backing allocation that the Resource Manager
// still tracks in `loaded_handles`. If we only allocate a
// new block here, the resource map continues to point at the
// stale pointer and a later GetResource can synthesize a
// duplicate handle for the same (type, id) pair.
if new_size == 0 {
return 0;
}
let Some((old_ptr, _, _)) = self.loaded_handles.get(&handle).copied() else {
return -192;
};
let new_ptr = self.resize_resource_allocation(bus, handle, old_ptr, new_size as u32);
if new_ptr == 0 {
return -108; // memFullErr
}
return 0;
}
let new_ptr = self.resize_resource_allocation(bus, handle, ptr, new_size as u32);
if new_ptr == 0 && new_size != 0 {
return -108; // memFullErr
}
0
}
/// Resize the master-pointer allocation backing a resource handle
/// to at least `new_size` bytes, preserving the prefix data.
/// Returns the new master-pointer address (which may differ from
/// `old_ptr` if the data had to be relocated), or 0 on
/// allocation failure when `new_size > 0`.
///
/// Updates the live ptr through `*handle`, the `ptr_to_handle`
/// map, the `loaded_handles` ptr field, and any
/// `LoadedResources::files[refnum].loaded` / `.named` entries
/// pointing at the old pointer so subsequent GetResource lookups
/// see the new allocation.
fn resize_resource_allocation(
&mut self,
bus: &mut MacMemoryBus,
handle: u32,
old_ptr: u32,
new_size: u32,
) -> u32 {
let old_size = bus.get_alloc_size(old_ptr).unwrap_or(0);
let aligned_old = (old_size + 3) & !3;
let aligned_new = (new_size + 3) & !3;
if aligned_new <= aligned_old {
// Fits in the existing aligned bucket; just retag the
// logical size. Mirrors the SetHandleSize fast path.
bus.set_alloc_size(old_ptr, new_size);
return old_ptr;
}
let new_ptr = bus.alloc(new_size);
if new_ptr == 0 {
return 0;
}
let copy_len = old_size.min(new_size) as usize;
if copy_len > 0 {
let bytes = bus.read_bytes(old_ptr, copy_len);
bus.write_bytes(new_ptr, &bytes);
}
bus.free(old_ptr);
bus.write_long(handle, new_ptr);
self.ptr_to_handle.remove(&old_ptr);
self.ptr_to_handle.insert(new_ptr, handle);
if let Some(entry) = self.loaded_handles.get_mut(&handle) {
entry.0 = new_ptr;
}
if let Some(resources) = self.resources.as_mut() {
for file in resources.files.values_mut() {
for v in file.loaded.values_mut() {
if *v == old_ptr {
*v = new_ptr;
}
}
for (_id, v) in file.named.values_mut() {
if *v == old_ptr {
*v = new_ptr;
}
}
}
}
new_ptr
}
/// Read a Pascal string filename from a parameter block's ioNamePtr.
pub(crate) fn read_pb_filename(bus: &MacMemoryBus, name_ptr: u32) -> String {
if name_ptr == 0 {
return String::new();
}
String::from_utf8_lossy(&bus.read_pstring(name_ptr)).to_string()
}
fn write_pstring(bus: &mut MacMemoryBus, ptr: u32, value: &str) {
if ptr == 0 {
return;
}
let bytes = value.as_bytes();
let len = bytes.len().min(63);
bus.write_byte(ptr, len as u8);
for (idx, byte) in bytes.iter().take(len).enumerate() {
bus.write_byte(ptr + 1 + idx as u32, *byte);
}
}
fn write_finfo(
bus: &mut MacMemoryBus,
finfo_ptr: u32,
file_type: u32,
creator: u32,
finder_flags: u16,
) {
bus.write_long(finfo_ptr, file_type);
bus.write_long(finfo_ptr + 4, creator);
bus.write_word(finfo_ptr + 8, finder_flags);
bus.write_long(finfo_ptr + 10, 0); // fdLocation
bus.write_word(finfo_ptr + 14, 0); // fdFldr
}
fn fill_file_catalog_info(
&mut self,
bus: &mut MacMemoryBus,
pb: u32,
key: &str,
metadata: super::dispatch::VfsMetadata,
) {
let data_len = self.vfs.get(key).map_or(0, |data| data.len() as u32);
let rsrc_len = self.vfs_rsrc.get(key).map_or(0, |data| data.len() as u32);
// ioFlAttrib bit 0 = file locked. Files 1992, 2-192. Maintained
// by HSetFLock/HRstFLock ($A241/$A242).
let locked_bit: u8 = if self.locked_files.contains(key) {
0x01
} else {
0x00
};
bus.write_byte(pb + 30, locked_bit); // ioFlAttrib
bus.write_byte(pb + 31, 0); // ioACUser / ioFlVersNum
Self::write_finfo(
bus,
pb + 32,
metadata.file_type,
metadata.creator,
metadata.finder_flags,
);
bus.write_long(pb + 48, metadata.file_id);
bus.write_long(pb + 54, data_len);
bus.write_long(pb + 58, data_len);
bus.write_long(pb + 64, rsrc_len);
bus.write_long(pb + 68, rsrc_len);
bus.write_long(pb + 72, metadata.created_date);
bus.write_long(pb + 76, metadata.modified_date);
// ioFlParID: parent directory ID.
// Files 1992, 2-192
bus.write_long(pb + 100, metadata.parent_dir_id);
}
fn fill_directory_catalog_info(
bus: &mut MacMemoryBus,
pb: u32,
directory: &super::dispatch::VfsDirectory,
) {
bus.write_byte(pb + 30, 0x10); // kFolderBit
bus.write_byte(pb + 31, 0);
Self::write_finfo(
bus,
pb + 32,
u32::from_be_bytes(*b"fold"),
u32::from_be_bytes(*b"MACS"),
0,
);
bus.write_long(pb + 48, directory.dir_id);
bus.write_long(pb + 72, 0);
bus.write_long(pb + 76, 0);
// ioDrParID: parent directory ID.
// Files 1992, 2-192
bus.write_long(pb + 100, directory.parent_dir_id);
}
fn lookup_catalog_entry(
&mut self,
dir_id: u32,
filename: &str,
fdir_index: i16,
) -> Option<super::dispatch::VfsCatalogEntry> {
if fdir_index < 0 {
let directory = self.directory_entry_for_id(dir_id)?;
let path = self.directory_path_for_id(dir_id)?.to_string();
return Some(super::dispatch::VfsCatalogEntry {
path,
name: directory.name.clone(),
is_directory: true,
});
}
if fdir_index > 0 {
let entries = self.list_vfs_catalog_entries(dir_id);
return entries.get(fdir_index as usize - 1).cloned();
}
if !filename.is_empty() {
if let Some(path) = self.find_vfs_file_in_directory(dir_id, filename) {
self.vfs_file_metadata(&path)?;
return Some(super::dispatch::VfsCatalogEntry {
path: path.clone(),
name: super::TrapDispatcher::vfs_basename(&path).to_string(),
is_directory: false,
});
}
if let Some(path) = self.find_vfs_directory_in_directory(dir_id, filename) {
let directory = self.vfs_directories.get(&path)?;
return Some(super::dispatch::VfsCatalogEntry {
path: path.clone(),
name: directory.name.clone(),
is_directory: true,
});
}
}
None
}
fn lookup_catalog_entry_for_hfs_lookup(
&mut self,
vref: i16,
dir_id: u32,
filename: &str,
fdir_index: i16,
) -> Option<super::dispatch::VfsCatalogEntry> {
for candidate_dir_id in self.hfs_lookup_directory_ids(vref, dir_id) {
if let Some(entry) = self.lookup_catalog_entry(candidate_dir_id, filename, fdir_index) {
return Some(entry);
}
}
None
}
fn find_vfs_file_for_hfs_lookup(
&mut self,
vref: i16,
dir_id: u32,
filename: &str,
) -> Option<String> {
for candidate_dir_id in self.hfs_lookup_directory_ids(vref, dir_id) {
if let Some(path) = self.find_vfs_file_in_directory(candidate_dir_id, filename) {
return Some(path);
}
}
None
}
fn find_vfs_rsrc_file_for_hfs_lookup(
&mut self,
vref: i16,
dir_id: u32,
filename: &str,
) -> Option<String> {
for candidate_dir_id in self.hfs_lookup_directory_ids(vref, dir_id) {
if let Some(path) = self.find_vfs_rsrc_file_in_directory(candidate_dir_id, filename) {
return Some(path);
}
}
None
}
fn find_vfs_directory_for_hfs_lookup(
&mut self,
vref: i16,
dir_id: u32,
filename: &str,
) -> Option<String> {
for candidate_dir_id in self.hfs_lookup_directory_ids(vref, dir_id) {
if let Some(path) = self.find_vfs_directory_in_directory(candidate_dir_id, filename) {
return Some(path);
}
}
None
}
pub(crate) fn vfs_key_for_fsspec(&self, vref: i16, dir_id: u32, filename: &str) -> Option<String> {
let resolved_dir_id = self.resolve_directory_id(vref, dir_id);
let dir_path = self.directory_path_for_id(resolved_dir_id)?;
let normalized = super::TrapDispatcher::normalize_vfs_path(filename);
Some(if dir_path.is_empty() {
normalized
} else {
format!("{dir_path}/{normalized}")
})
}
/// Find a file in VFS by name, trying exact match then basename match.
pub(crate) fn find_vfs_file(&self, name: &str) -> Option<String> {
let normalized = super::TrapDispatcher::normalize_vfs_path(name);
// Sort key iteration so the first match is stable across runs.
let mut sorted_keys: Vec<&String> = self.vfs.keys().collect();
sorted_keys.sort_unstable();
if let Some(found) = sorted_keys.iter().copied().find(|key| {
super::TrapDispatcher::normalize_vfs_path(key).eq_ignore_ascii_case(&normalized)
}) {
return Some(found.clone());
}
let basename = normalized.rsplit('/').next().unwrap_or(normalized.as_str());
for key in &sorted_keys {
let key_base = key.rsplit('/').next().unwrap_or(key);
if key_base.eq_ignore_ascii_case(basename) {
return Some((*key).clone());
}
}
None
}
}
#[cfg(test)]
mod tests {
use super::super::dispatch::{LoadedResources, ResourceFileMap};
use super::super::test_helpers::{setup, TEST_SP};
use crate::cpu::{CpuOps, Register};
use crate::memory::globals::addr;
use crate::memory::MemoryBus;
use std::collections::HashMap;
use super::super::test_helpers::MockCpu;
// ================================================================
// Helper: call dispatch_resource and unwrap
// ================================================================
fn call(
dispatcher: &mut super::super::TrapDispatcher,
is_tool: bool,
trap_num: u16,
cpu: &mut MockCpu,
bus: &mut crate::memory::MacMemoryBus,
) -> crate::Result<()> {
dispatcher
.dispatch_resource(is_tool, trap_num, cpu, bus)
.expect("trap arm should be handled")
}
fn call_trap_word(
dispatcher: &mut super::super::TrapDispatcher,
trap_word: u16,
cpu: &mut MockCpu,
bus: &mut crate::memory::MacMemoryBus,
) -> crate::Result<()> {
dispatcher.dispatch(trap_word, cpu, bus)
}
#[test]
fn initpack_pops_pack_id_and_records_last_pack() {
let (mut disp, mut cpu, mut bus) = setup();
let sp = TEST_SP;
bus.write_word(sp, 0x1234);
bus.write_long(sp + 2, 0xDEAD_BEEF);
let sp_pre = cpu.read_reg(Register::A7);
let result = call_trap_word(&mut disp, 0xA9E5, &mut cpu, &mut bus);
assert!(result.is_ok(), "InitPack should return cleanly");
assert_eq!(cpu.read_reg(Register::A7), sp_pre + 2);
assert_eq!(disp.last_init_pack_id, Some(0x1234_i16));
assert_eq!(bus.read_long(sp + 2), 0xDEAD_BEEF);
}
#[test]
fn phantom_script_aliases_clear_d0_and_balance_stack() {
let (mut disp, mut cpu, mut bus) = setup();
let sp = TEST_SP;
// AA87 GetScript alias: selector word + script word + result slot.
bus.write_word(sp, 0x0001);
bus.write_word(sp + 2, 0x1234);
bus.write_word(sp + 4, 0xBEEF);
cpu.write_reg(Register::A7, sp);
cpu.write_reg(Register::D0, 0xDEAD_BEEF);
call_trap_word(&mut disp, 0xAA87, &mut cpu, &mut bus).unwrap();
assert_eq!(cpu.read_reg(Register::D0), 0, "AA87 clears D0");
assert_eq!(bus.read_word(sp + 4), 0, "AA87 writes zero result");
assert_eq!(cpu.read_reg(Register::A7), sp + 4, "AA87 pops 4 bytes");
// AA88 SetScript alias: value, selector, script; no result slot.
bus.write_word(sp, 0x5678);
bus.write_word(sp + 2, 0x0002);
bus.write_word(sp + 4, 0x1234);
bus.write_word(sp + 6, 0xCAFE);
cpu.write_reg(Register::A7, sp);
cpu.write_reg(Register::D0, 0xDEAD_BEEF);
call_trap_word(&mut disp, 0xAA88, &mut cpu, &mut bus).unwrap();
assert_eq!(cpu.read_reg(Register::D0), 0, "AA88 clears D0");
assert_eq!(bus.read_word(sp + 6), 0xCAFE, "AA88 leaves trailing memory untouched");
assert_eq!(cpu.read_reg(Register::A7), sp + 6, "AA88 pops 6 bytes");
// AA89 GetEnvirons alias: selector word + result slot.
bus.write_word(sp, 0x0005);
bus.write_word(sp + 2, 0xBEEF);
cpu.write_reg(Register::A7, sp);
cpu.write_reg(Register::D0, 0xDEAD_BEEF);
call_trap_word(&mut disp, 0xAA89, &mut cpu, &mut bus).unwrap();
assert_eq!(cpu.read_reg(Register::D0), 0, "AA89 clears D0");
assert_eq!(bus.read_word(sp + 2), 0, "AA89 writes zero result");
assert_eq!(cpu.read_reg(Register::A7), sp + 2, "AA89 pops 2 bytes");
}
/// Write a Pascal string (length-prefixed) into memory at `addr`.
fn write_pstring(bus: &mut crate::memory::MacMemoryBus, addr: u32, s: &[u8]) {
bus.write_byte(addr, s.len() as u8);
for (i, &b) in s.iter().enumerate() {
bus.write_byte(addr + 1 + i as u32, b);
}
}
/// Write an FSSpec at `spec_ptr`: vRefNum(2) + dirID(4) + name(pstring, 64 bytes).
fn write_fsspec(
bus: &mut crate::memory::MacMemoryBus,
spec_ptr: u32,
vref: u16,
dir_id: u32,
name: &[u8],
) {
bus.write_word(spec_ptr, vref);
bus.write_long(spec_ptr + 2, dir_id);
let len = name.len().min(63) as u8;
bus.write_byte(spec_ptr + 6, len);
for (i, &byte) in name.iter().take(len as usize).enumerate() {
bus.write_byte(spec_ptr + 7 + i as u32, byte);
}
}
fn make_single_resource_fork_bytes(
res_type: [u8; 4],
res_id: i16,
data: &[u8],
) -> Vec<u8> {
let data_offset = 16u32;
let data_length = (4 + data.len()) as u32;
let map_offset = data_offset + data_length;
let type_list_offset = 30u16;
let ref_list_offset = 10u16;
let name_list_offset = 40u16;
let map_length = 52u32;
let mut bytes = vec![0u8; (map_offset + map_length) as usize];
let mut header = [0u8; 16];
header[0..4].copy_from_slice(&data_offset.to_be_bytes());
header[4..8].copy_from_slice(&map_offset.to_be_bytes());
header[8..12].copy_from_slice(&data_length.to_be_bytes());
header[12..16].copy_from_slice(&map_length.to_be_bytes());
bytes[0..16].copy_from_slice(&header);
let data_start = data_offset as usize;
bytes[data_start..data_start + 4].copy_from_slice(&(data.len() as u32).to_be_bytes());
bytes[data_start + 4..data_start + 4 + data.len()].copy_from_slice(data);
let map_start = map_offset as usize;
bytes[map_start..map_start + 16].copy_from_slice(&header);
bytes[map_start + 16..map_start + 20].copy_from_slice(&0u32.to_be_bytes());
bytes[map_start + 20..map_start + 22].copy_from_slice(&0u16.to_be_bytes());
bytes[map_start + 22..map_start + 24].copy_from_slice(&0u16.to_be_bytes());
bytes[map_start + 24..map_start + 26].copy_from_slice(&type_list_offset.to_be_bytes());
bytes[map_start + 26..map_start + 28].copy_from_slice(&name_list_offset.to_be_bytes());
bytes[map_start + 28..map_start + 30].copy_from_slice(&0u16.to_be_bytes());
let type_list_start = map_start + type_list_offset as usize;
bytes[type_list_start..type_list_start + 2].copy_from_slice(&0u16.to_be_bytes());
bytes[type_list_start + 2..type_list_start + 6].copy_from_slice(&res_type);
bytes[type_list_start + 6..type_list_start + 8].copy_from_slice(&0u16.to_be_bytes());
bytes[type_list_start + 8..type_list_start + 10].copy_from_slice(&ref_list_offset.to_be_bytes());
let ref_list_start = map_start + type_list_offset as usize + ref_list_offset as usize;
bytes[ref_list_start..ref_list_start + 2].copy_from_slice(&(res_id as u16).to_be_bytes());
bytes[ref_list_start + 2..ref_list_start + 4].copy_from_slice(&0xFFFFu16.to_be_bytes());
bytes[ref_list_start + 4] = 0;
bytes[ref_list_start + 5..ref_list_start + 8].copy_from_slice(&0u32.to_be_bytes()[1..4]);
bytes
}
#[test]
fn systemevent_mouse_down_returns_false_and_preserves_stack_and_d0() {
let (mut disp, mut cpu, mut bus) = setup();
let sp = TEST_SP;
// EventRecord (16 bytes by value): mouseDown event.
bus.write_word(sp, 1); // what = mouseDown
bus.write_long(sp + 2, 0x1122_3344); // message
bus.write_long(sp + 6, 0x5566_7788); // when
bus.write_long(sp + 10, 0x99AA_BBCC); // where
bus.write_word(sp + 14, 0x0102); // modifiers
bus.write_word(sp + 16, 0xBEEF); // result sentinel
let result = disp.dispatch_resource(true, 0x1B2, &mut cpu, &mut bus);
assert!(result.is_some());
assert!(result.unwrap().is_ok());
assert_eq!(cpu.read_reg(Register::A7), sp + 16);
assert_eq!(bus.read_word(sp + 16), 0);
assert_eq!(cpu.read_reg(Register::D0), 0);
}
/// Set up loaded resources with one entry.
fn setup_resources(
dispatcher: &mut super::super::TrapDispatcher,
bus: &mut crate::memory::MacMemoryBus,
res_type: &[u8; 4],
res_id: i16,
data: &[u8],
) -> u32 {
let data_ptr = bus.alloc(data.len() as u32);
bus.write_bytes(data_ptr, data);
let mut loaded = HashMap::new();
loaded.insert((*res_type, res_id), data_ptr);
let file = ResourceFileMap {
loaded,
named: HashMap::new(),
attrs: HashMap::new(),
map_attrs: 0,
};
dispatcher.resources = Some(LoadedResources {
files: HashMap::from([(0, file)]),
names: HashMap::new(),
search_order: vec![0],
current_file: 0,
});
data_ptr
}
#[test]
fn movehhi_respects_setrespurge_for_changed_resource_handles() {
// Inside Macintosh Volume I (1985), p. I-126 and Memory 1992,
// pp. 2-18 / 2-91: SetResPurge installs the purge hook so a
// changed resource can be written out when the Memory Manager
// moves a resource handle.
let (mut disp, mut cpu, mut bus) = setup();
let data_ptr = setup_resources(&mut disp, &mut bus, b"STR ", 32000, &[0x41, 0x42, 0x43]);
let handle = disp.get_or_create_resource_handle(&mut bus, *b"STR ", 32000, data_ptr);
let changed = super::super::TrapDispatcher::RES_CHANGED_ATTR;
let sp = TEST_SP;
// FALSE (high byte 0) keeps the changed resource marked as changed.
cpu.write_reg(Register::A7, sp);
bus.write_word(sp, 0x00FF);
let result = disp.dispatch_toolbox(true, 0x193, &mut cpu, &mut bus);
assert!(result.is_some());
assert!(result.unwrap().is_ok());
assert!(!disp.res_purge);
cpu.write_reg(Register::A7, sp);
bus.write_long(sp, handle);
call(&mut disp, true, 0x1AA, &mut cpu, &mut bus).unwrap(); // ChangedResource
assert_ne!(
disp.resource_attributes_for_handle(handle).unwrap_or(0) & changed,
0
);
cpu.write_reg(Register::A0, handle);
let result = disp.dispatch_memory(false, 0x64, &mut cpu, &mut bus);
assert!(result.is_some(), "MoveHHi should be handled");
assert!(result.unwrap().is_ok(), "MoveHHi should succeed");
assert_eq!(cpu.read_reg(Register::D0), 0, "MoveHHi should return noErr");
assert_ne!(
disp.resource_attributes_for_handle(handle).unwrap_or(0) & changed,
0,
"SetResPurge(FALSE) should keep the resource changed after MoveHHi"
);
// TRUE (high byte 1) clears the changed flag through the purge hook.
cpu.write_reg(Register::A7, sp);
bus.write_word(sp, 0x0100);
let result = disp.dispatch_toolbox(true, 0x193, &mut cpu, &mut bus);
assert!(result.is_some());
assert!(result.unwrap().is_ok());
assert!(disp.res_purge);
cpu.write_reg(Register::A7, sp);
cpu.write_reg(Register::A0, handle);
let result = disp.dispatch_memory(false, 0x64, &mut cpu, &mut bus);
assert!(result.is_some(), "MoveHHi should be handled");
assert!(result.unwrap().is_ok(), "MoveHHi should succeed");
assert_eq!(cpu.read_reg(Register::D0), 0, "MoveHHi should return noErr");
assert_eq!(
disp.resource_attributes_for_handle(handle).unwrap_or(0) & changed,
0,
"SetResPurge(TRUE) should clear the changed flag through MoveHHi"
);
}
// ================================================================
// 0. CreateResFile (0x1B1)
// ================================================================
#[test]
fn createresfile_missing_file_creates_data_and_resource_forks() {
// IM:More Macintosh Toolbox 1993, p. 1-57: CreateResFile creates a
// file with a zero-length data fork and an empty resource fork map
// when no matching file exists.
let (mut disp, mut cpu, mut bus) = setup();
let sp = TEST_SP;
let name_ptr = 0x230000u32;
write_pstring(&mut bus, name_ptr, b"Prefs.RSRC");
bus.write_long(sp, name_ptr);
call(&mut disp, true, 0x1B1, &mut cpu, &mut bus).unwrap();
assert_eq!(
cpu.read_reg(Register::A7),
sp + 4,
"CreateResFile pops fileName ptr"
);
assert_eq!(bus.read_word(0x0A60), 0, "ResErr should be noErr");
assert!(
disp.vfs.contains_key("Prefs.RSRC"),
"missing file should create data fork"
);
assert!(
disp.vfs_rsrc.contains_key("Prefs.RSRC"),
"missing file should create resource fork"
);
assert_eq!(disp.vfs.get("Prefs.RSRC").unwrap(), &Vec::<u8>::new());
assert!(
crate::managers::resource::ResourceFork::parse(
disp.vfs_rsrc.get("Prefs.RSRC").unwrap()
)
.is_some(),
"CreateResFile should seed a structurally valid empty resource map"
);
}
#[test]
fn createresfile_existing_data_fork_adds_resource_fork_without_truncating_data() {
// IM:More Macintosh Toolbox 1993, p. 1-57: if data fork exists with
// zero-length/missing resource fork, CreateResFile adds the resource
// fork map and does not destroy file data.
let (mut disp, mut cpu, mut bus) = setup();
disp.vfs
.insert("DATAONLY".to_string(), vec![0xDE, 0xAD, 0xBE, 0xEF]);
let sp = TEST_SP;
let name_ptr = 0x230100u32;
write_pstring(&mut bus, name_ptr, b"DATAONLY");
bus.write_long(sp, name_ptr);
call(&mut disp, true, 0x1B1, &mut cpu, &mut bus).unwrap();
assert_eq!(cpu.read_reg(Register::A7), sp + 4);
assert_eq!(bus.read_word(0x0A60), 0, "ResErr should be noErr");
assert_eq!(
disp.vfs.get("DATAONLY").unwrap(),
&vec![0xDE, 0xAD, 0xBE, 0xEF],
"CreateResFile should not truncate existing data fork bytes"
);
assert!(
crate::managers::resource::ResourceFork::parse(disp.vfs_rsrc.get("DATAONLY").unwrap())
.is_some(),
"CreateResFile should add a resource fork map for existing data file"
);
}
#[test]
fn createresfile_existing_zero_length_resource_fork_returns_noerr() {
// IM:More Macintosh Toolbox 1993, p. 1-57: if the data fork exists
// and the resource fork is zero-length, CreateResFile creates the
// empty resource map instead of returning dupFNErr.
let (mut disp, mut cpu, mut bus) = setup();
disp.vfs
.insert("ZERORSRC".to_string(), vec![0xDE, 0xAD, 0xBE, 0xEF]);
disp.vfs_rsrc.insert("ZERORSRC".to_string(), Vec::new());
let sp = TEST_SP;
let name_ptr = 0x230180u32;
write_pstring(&mut bus, name_ptr, b"ZERORSRC");
bus.write_long(sp, name_ptr);
call(&mut disp, true, 0x1B1, &mut cpu, &mut bus).unwrap();
assert_eq!(cpu.read_reg(Register::A7), sp + 4);
assert_eq!(bus.read_word(0x0A60), 0, "ResErr should be noErr");
assert_eq!(
disp.vfs.get("ZERORSRC").unwrap(),
&vec![0xDE, 0xAD, 0xBE, 0xEF],
"CreateResFile should preserve existing data fork bytes"
);
assert!(
crate::managers::resource::ResourceFork::parse(disp.vfs_rsrc.get("ZERORSRC").unwrap())
.is_some(),
"CreateResFile should turn a zero-length resource fork into an empty mapped fork"
);
}
#[test]
fn createresfile_existing_resource_fork_returns_dupfnerr_and_leaves_file_unchanged() {
// IM:More Macintosh Toolbox 1993, p. 1-57 (Special Considerations):
// if a matching resource file is found during PBOpenRF search,
// CreateResFile does nothing and ResError reports dupFNErr (-48).
let (mut disp, mut cpu, mut bus) = setup();
disp.vfs.insert("DUPLICATE".to_string(), vec![0x11, 0x22]);
disp.vfs_rsrc
.insert("DUPLICATE".to_string(), vec![0x33, 0x44]);
let sp = TEST_SP;
let name_ptr = 0x230200u32;
write_pstring(&mut bus, name_ptr, b"DUPLICATE");
bus.write_long(sp, name_ptr);
call(&mut disp, true, 0x1B1, &mut cpu, &mut bus).unwrap();
assert_eq!(cpu.read_reg(Register::A7), sp + 4);
assert_eq!(
bus.read_word(0x0A60) as i16,
-48,
"ResErr should be dupFNErr"
);
assert_eq!(disp.vfs.get("DUPLICATE").unwrap(), &vec![0x11, 0x22]);
assert_eq!(disp.vfs_rsrc.get("DUPLICATE").unwrap(), &vec![0x33, 0x44]);
}
// ================================================================
// 1. GetResource (0x1A0) — found
// ================================================================
#[test]
fn get_resource_found() {
let (mut disp, mut cpu, mut bus) = setup();
let data_ptr = setup_resources(&mut disp, &mut bus, b"STR#", 128, &[0; 10]);
// Push params: SP+0 = id(2), SP+2 = type(4)
let sp = TEST_SP;
bus.write_word(sp, 128u16); // id
bus.write_long(sp + 2, u32::from_be_bytes(*b"STR#")); // type
call(&mut disp, true, 0x1A0, &mut cpu, &mut bus).unwrap();
let new_sp = cpu.read_reg(Register::A7);
assert_eq!(new_sp, TEST_SP + 6, "SP should advance by 6");
let handle = bus.read_long(new_sp);
assert_ne!(handle, 0, "handle should be non-zero");
// Dereference handle -> ptr should equal data_ptr
let deref = bus.read_long(handle);
assert_eq!(deref, data_ptr);
assert_eq!(bus.read_word(0x0A60), 0, "ResErr should be noErr");
}
#[test]
fn get_resource_respects_setresload_false_until_loadresource() {
let (mut disp, mut cpu, mut bus) = setup();
let data_ptr = setup_resources(&mut disp, &mut bus, b"LOAD", 501, &[0xCA, 0xFE]);
// IM:More Macintosh Toolbox 1993, 1-79 to 1-80: after
// SetResLoad(FALSE), GetResource returns an empty handle for data
// that is not already in memory; LoadResource later fills it.
disp.res_load = false;
let sp = TEST_SP;
bus.write_word(sp, 501u16);
bus.write_long(sp + 2, u32::from_be_bytes(*b"LOAD"));
call(&mut disp, true, 0x1A0, &mut cpu, &mut bus).unwrap();
let handle = bus.read_long(TEST_SP + 6);
assert_ne!(handle, 0, "GetResource should still return a handle");
assert_eq!(
bus.read_long(handle),
0,
"master pointer should be nil while automatic loading is disabled"
);
assert_eq!(bus.read_word(0x0A60), 0);
cpu.write_reg(Register::A7, TEST_SP);
bus.write_long(TEST_SP, handle);
call(&mut disp, true, 0x1A2, &mut cpu, &mut bus).unwrap();
assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 4);
assert_eq!(bus.read_long(handle), data_ptr);
assert_eq!(bus.read_word(0x0A60), 0);
}
#[test]
fn get_resource_autoloads_existing_empty_handle_after_setresload_true() {
let (mut disp, mut cpu, mut bus) = setup();
let data_ptr = setup_resources(&mut disp, &mut bus, b"LOAD", 502, &[0xBE, 0xEF]);
disp.res_load = false;
bus.write_word(TEST_SP, 502u16);
bus.write_long(TEST_SP + 2, u32::from_be_bytes(*b"LOAD"));
call(&mut disp, true, 0x1A0, &mut cpu, &mut bus).unwrap();
let empty_handle = bus.read_long(TEST_SP + 6);
assert_ne!(empty_handle, 0);
assert_eq!(bus.read_long(empty_handle), 0);
// IM:More Macintosh Toolbox 1993, 1-79: SetResLoad(TRUE) returns
// resource-returning calls to automatic loading behavior.
disp.res_load = true;
cpu.write_reg(Register::A7, TEST_SP);
bus.write_word(TEST_SP, 502u16);
bus.write_long(TEST_SP + 2, u32::from_be_bytes(*b"LOAD"));
call(&mut disp, true, 0x1A0, &mut cpu, &mut bus).unwrap();
let loaded_handle = bus.read_long(TEST_SP + 6);
assert_eq!(loaded_handle, empty_handle);
assert_eq!(bus.read_long(loaded_handle), data_ptr);
assert_eq!(bus.read_word(0x0A60), 0);
cpu.write_reg(Register::A0, data_ptr);
let recovered = disp.dispatch_memory(false, 0x28, &mut cpu, &mut bus);
assert!(recovered.is_some(), "RecoverHandle should be handled");
assert!(recovered.unwrap().is_ok(), "RecoverHandle should succeed");
assert_eq!(cpu.read_reg(Register::A0), loaded_handle);
assert_eq!(
disp.ptr_to_handle.get(&data_ptr).copied(),
Some(loaded_handle)
);
}
// ================================================================
// 1b. GetResource (0x1A0) — not found
// ================================================================
#[test]
fn get_resource_not_found() {
let (mut disp, mut cpu, mut bus) = setup();
let sp = TEST_SP;
bus.write_word(sp, 999u16);
bus.write_long(sp + 2, u32::from_be_bytes(*b"XXXX"));
call(&mut disp, true, 0x1A0, &mut cpu, &mut bus).unwrap();
let new_sp = cpu.read_reg(Register::A7);
assert_eq!(new_sp, TEST_SP + 6);
let handle = bus.read_long(new_sp);
assert_eq!(handle, 0, "handle should be NULL");
// ResErr at $0A60 = -192
assert_eq!(bus.read_word(0x0A60), (-192i16) as u16);
}
// More Macintosh Toolbox (1993), p. 1-78: RGetResource follows the
// GetResource search contract (plus ROM retry on real hardware).
#[test]
fn rgetresource_returns_handle_for_open_chain_match() {
let (mut disp, mut cpu, mut bus) = setup();
let data_ptr =
setup_resources(&mut disp, &mut bus, b"TEST", 128, &[0x41, 0x42, 0x43, 0x44]);
bus.write_word(TEST_SP, 128u16);
bus.write_long(TEST_SP + 2, u32::from_be_bytes(*b"TEST"));
call(&mut disp, true, 0x00C, &mut cpu, &mut bus).unwrap();
assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 6);
let handle = bus.read_long(TEST_SP + 6);
assert_ne!(handle, 0);
assert_eq!(bus.read_long(handle), data_ptr);
assert_eq!(bus.read_word(0x0A60), 0);
}
// More Macintosh Toolbox (1993), p. 1-78: failed RGetResource requests
// return NIL and surface not-found through ResError.
#[test]
fn rgetresource_miss_returns_nil_with_resnotfound() {
let (mut disp, mut cpu, mut bus) = setup();
bus.write_word(TEST_SP, 777u16);
bus.write_long(TEST_SP + 2, u32::from_be_bytes(*b"MISS"));
call(&mut disp, true, 0x00C, &mut cpu, &mut bus).unwrap();
assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 6);
assert_eq!(bus.read_long(TEST_SP + 6), 0);
assert_eq!(bus.read_word(0x0A60), (-192i16) as u16);
}
// More Macintosh Toolbox (1993), p. 1-78: RGetResource delegates to
// GetResource chain semantics before any ROM retry.
#[test]
fn rgetresource_walks_chain_not_just_current_file() {
let (mut disp, mut cpu, mut bus) = setup();
let chain_ptr = bus.alloc(4);
bus.write_bytes(chain_ptr, &[0xAA, 0xBB, 0xCC, 0xDD]);
disp.resources = Some(LoadedResources {
files: HashMap::from([
(
100,
ResourceFileMap {
loaded: HashMap::from([((*b"PICT", 300), chain_ptr)]),
named: HashMap::new(),
attrs: HashMap::new(),
map_attrs: 0,
},
),
(
200,
ResourceFileMap {
loaded: HashMap::new(),
named: HashMap::new(),
attrs: HashMap::new(),
map_attrs: 0,
},
),
]),
names: HashMap::new(),
search_order: vec![100, 200],
current_file: 200,
});
bus.write_word(TEST_SP, 300u16);
bus.write_long(TEST_SP + 2, u32::from_be_bytes(*b"PICT"));
call(&mut disp, true, 0x00C, &mut cpu, &mut bus).unwrap();
let handle = bus.read_long(TEST_SP + 6);
assert_ne!(handle, 0);
assert_eq!(bus.read_long(handle), chain_ptr);
assert_eq!(disp.resource_handle_files.get(&handle).copied(), Some(100));
assert_eq!(bus.read_word(0x0A60), 0);
}
// ================================================================
// 2. Get1Resource (0x01F) — found
// ================================================================
#[test]
fn get1_resource_found() {
let (mut disp, mut cpu, mut bus) = setup();
let data_ptr = setup_resources(&mut disp, &mut bus, b"DLOG", 200, &[0xAB; 8]);
let sp = TEST_SP;
bus.write_word(sp, 200u16);
bus.write_long(sp + 2, u32::from_be_bytes(*b"DLOG"));
call(&mut disp, true, 0x01F, &mut cpu, &mut bus).unwrap();
let new_sp = cpu.read_reg(Register::A7);
assert_eq!(new_sp, TEST_SP + 6);
let handle = bus.read_long(new_sp);
assert_ne!(handle, 0);
let deref = bus.read_long(handle);
assert_eq!(deref, data_ptr);
}
#[test]
fn get1_resource_uses_current_resource_file() {
let (mut disp, mut cpu, mut bus) = setup();
let app_ptr = bus.alloc(4);
bus.write_bytes(app_ptr, &[0xAA; 4]);
let images_ptr = bus.alloc(4);
bus.write_bytes(images_ptr, &[0xBB; 4]);
disp.resources = Some(LoadedResources {
files: HashMap::from([
(
0,
ResourceFileMap {
loaded: HashMap::from([((*b"PICT", 1114), app_ptr)]),
named: HashMap::new(),
attrs: HashMap::new(),
map_attrs: 0,
},
),
(
101,
ResourceFileMap {
loaded: HashMap::from([((*b"PICT", 1114), images_ptr)]),
named: HashMap::new(),
attrs: HashMap::new(),
map_attrs: 0,
},
),
]),
names: HashMap::new(),
search_order: vec![0, 101],
current_file: 101,
});
let sp = TEST_SP;
bus.write_word(sp, 1114u16);
bus.write_long(sp + 2, u32::from_be_bytes(*b"PICT"));
call(&mut disp, true, 0x01F, &mut cpu, &mut bus).unwrap();
let handle = bus.read_long(TEST_SP + 6);
assert_ne!(handle, 0);
assert_eq!(bus.read_long(handle), images_ptr);
assert_eq!(disp.resource_handle_files.get(&handle).copied(), Some(101));
}
// ================================================================
// 2b. Get1Resource (0x01F) — not found
// ================================================================
#[test]
fn get1_resource_not_found() {
let (mut disp, mut cpu, mut bus) = setup();
let sp = TEST_SP;
bus.write_word(sp, 999u16);
bus.write_long(sp + 2, u32::from_be_bytes(*b"NONE"));
call(&mut disp, true, 0x01F, &mut cpu, &mut bus).unwrap();
let new_sp = cpu.read_reg(Register::A7);
assert_eq!(new_sp, TEST_SP + 6);
let handle = bus.read_long(new_sp);
assert_eq!(handle, 0);
}
// ================================================================
// 3. DetachResource (0x192)
// ================================================================
#[test]
fn detach_resource() {
let (mut disp, mut cpu, mut bus) = setup();
let shared_ptr = bus.alloc(8);
for (offset, byte) in [0x10u8, 0x20, 0x30, 0x40, 0x50, 0x60, 0x70, 0x80]
.into_iter()
.enumerate()
{
bus.write_byte(shared_ptr + offset as u32, byte);
}
let handle = bus.alloc(4);
bus.write_long(handle, shared_ptr);
disp.loaded_handles
.insert(handle, (shared_ptr, *b"TEST", 7));
let sp = TEST_SP;
bus.write_long(sp, handle);
call(&mut disp, true, 0x192, &mut cpu, &mut bus).unwrap();
let new_sp = cpu.read_reg(Register::A7);
assert_eq!(new_sp, TEST_SP + 4);
let detached_ptr = bus.read_long(handle);
assert_ne!(detached_ptr, shared_ptr);
assert_eq!(
bus.read_bytes(detached_ptr, 8),
bus.read_bytes(shared_ptr, 8)
);
assert!(!disp.loaded_handles.contains_key(&handle));
assert_eq!(bus.read_word(0x0A60), 0);
bus.write_byte(detached_ptr + 3, 0xEE);
assert_eq!(bus.read_byte(shared_ptr + 3), 0x40);
}
// Inside Macintosh Volume I (1985), p. I-122: DetachResource must not
// detach resChanged resources, but ResError still returns noErr.
#[test]
fn detach_resource_reschanged_handle_returns_noerr_without_detaching() {
let (mut disp, mut cpu, mut bus) = setup();
let data_ptr = setup_resources(&mut disp, &mut bus, b"DLOG", 201, &[0xAA; 8]);
let handle = disp.get_or_create_resource_handle(&mut bus, *b"DLOG", 201, data_ptr);
let original_ptr = bus.read_long(handle);
bus.write_long(TEST_SP, handle);
call(&mut disp, true, 0x1AA, &mut cpu, &mut bus).unwrap(); // ChangedResource
assert_eq!(bus.read_word(0x0A60), 0);
assert_ne!(
disp.resource_attributes_for_handle(handle).unwrap_or(0)
& super::super::TrapDispatcher::RES_CHANGED_ATTR,
0
);
cpu.write_reg(Register::A7, TEST_SP);
bus.write_long(TEST_SP, handle);
call(&mut disp, true, 0x192, &mut cpu, &mut bus).unwrap();
assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 4);
assert_eq!(bus.read_word(0x0A60), 0);
assert_eq!(bus.read_long(handle), original_ptr);
assert!(disp.loaded_handles.contains_key(&handle));
assert!(disp.resource_handle_files.contains_key(&handle));
}
// Inside Macintosh Volume I (1985), p. I-122: DetachResource does nothing
// and returns resNotFound when the handle is not a resource handle.
#[test]
fn detach_resource_non_resource_handle_returns_resnotfound() {
let (mut disp, mut cpu, mut bus) = setup();
let fake_ptr = bus.alloc(6);
bus.write_bytes(fake_ptr, &[1, 2, 3, 4, 5, 6]);
let fake_handle = bus.alloc(4);
bus.write_long(fake_handle, fake_ptr);
bus.write_long(TEST_SP, fake_handle);
call(&mut disp, true, 0x192, &mut cpu, &mut bus).unwrap();
assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 4);
assert_eq!(bus.read_long(fake_handle), fake_ptr);
assert_eq!(bus.read_word(0x0A60) as i16, -192);
}
// ================================================================
// 4. LoadResource (0x1A2)
// ================================================================
#[test]
fn load_resource() {
let (mut disp, mut cpu, mut bus) = setup();
let data_ptr = setup_resources(&mut disp, &mut bus, b"DLOG", 200, &[0xAB; 8]);
let handle = disp.get_or_create_resource_handle(&mut bus, *b"DLOG", 200, data_ptr);
let sp = TEST_SP;
bus.write_long(sp, handle);
call(&mut disp, true, 0x1A2, &mut cpu, &mut bus).unwrap();
assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 4);
assert_eq!(bus.read_word(0x0A60), 0);
}
#[test]
fn load_resource_loaded_handle_is_noop_and_recoverable() {
let (mut disp, mut cpu, mut bus) = setup();
let data_ptr = setup_resources(&mut disp, &mut bus, b"DLOG", 203, &[0xCA, 0xFE]);
let handle = disp.get_or_create_resource_handle(&mut bus, *b"DLOG", 203, data_ptr);
cpu.write_reg(Register::A7, TEST_SP);
bus.write_long(TEST_SP, handle);
call(&mut disp, true, 0x1A2, &mut cpu, &mut bus).unwrap();
assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 4);
assert_eq!(bus.read_word(0x0A60), 0);
assert_eq!(bus.read_long(handle), data_ptr);
assert_eq!(
disp.resource_handle_files.get(&handle).copied(),
Some(disp.current_resource_refnum()),
"LoadResource should leave the resource-file binding intact"
);
cpu.write_reg(Register::A0, data_ptr);
let recovered = disp.dispatch_memory(false, 0x28, &mut cpu, &mut bus);
assert!(recovered.is_some(), "RecoverHandle should be handled");
assert!(recovered.unwrap().is_ok(), "RecoverHandle should succeed");
assert_eq!(cpu.read_reg(Register::A0), handle);
}
#[test]
fn load_resource_rewrites_stale_master_pointer_after_reload() {
let (mut disp, mut cpu, mut bus) = setup();
let data_ptr = setup_resources(&mut disp, &mut bus, b"DLOG", 204, &[0xAB, 0xCD]);
let handle = disp.get_or_create_resource_handle(&mut bus, *b"DLOG", 204, data_ptr);
bus.write_long(handle, 0x1234_5678);
assert_ne!(bus.read_long(handle), data_ptr);
cpu.write_reg(Register::A7, TEST_SP);
bus.write_long(TEST_SP, handle);
call(&mut disp, true, 0x1A2, &mut cpu, &mut bus).unwrap();
assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 4);
assert_eq!(bus.read_long(handle), data_ptr);
assert_eq!(
disp.ptr_to_handle.get(&data_ptr).copied(),
Some(handle),
"LoadResource should restore RecoverHandle-style ownership"
);
assert_eq!(bus.read_word(0x0A60), 0);
}
#[test]
fn load_resource_repopulates_handle_after_emptyhandle_purge() {
// More Macintosh Toolbox (1993), p. 1-80: LoadResource reads
// resource data into memory for a resource handle. Memory (1992),
// p. 2-52 explicitly directs purged resources to LoadResource.
let (mut disp, mut cpu, mut bus) = setup();
let data_ptr = setup_resources(&mut disp, &mut bus, b"DLOG", 202, &[0xCA, 0xFE]);
let handle = disp.get_or_create_resource_handle(&mut bus, *b"DLOG", 202, data_ptr);
let refnum = disp
.resource_handle_files
.get(&handle)
.copied()
.expect("resource handle should have a file binding");
cpu.write_reg(Register::A0, handle);
let emptied = disp.dispatch_memory(false, 0x2B, &mut cpu, &mut bus);
assert!(emptied.is_some(), "EmptyHandle should be handled");
assert!(emptied.unwrap().is_ok(), "EmptyHandle should succeed");
assert_eq!(
bus.read_long(handle),
0,
"resource handle should be empty after purge"
);
cpu.write_reg(Register::A7, TEST_SP);
bus.write_long(TEST_SP, handle);
call(&mut disp, true, 0x1A2, &mut cpu, &mut bus).unwrap();
assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 4);
assert_eq!(
bus.read_long(handle),
data_ptr,
"LoadResource should restore the purged resource's master pointer"
);
assert_eq!(
disp.ptr_to_handle.get(&data_ptr).copied(),
Some(handle),
"LoadResource should restore RecoverHandle-style ptr ownership"
);
assert!(
!disp.detached_handles.contains_key(&handle),
"LoadResource should clear detached handle state after reloading"
);
assert!(
!disp.detached_handle_files.contains_key(&handle),
"LoadResource should clear detached file ownership after reloading"
);
cpu.write_reg(Register::A0, data_ptr);
let recovered = disp.dispatch_memory(false, 0x28, &mut cpu, &mut bus);
assert!(recovered.is_some(), "RecoverHandle should be handled");
assert!(recovered.unwrap().is_ok(), "RecoverHandle should succeed");
assert_eq!(
cpu.read_reg(Register::A0),
handle,
"RecoverHandle should rediscover the reloaded resource handle"
);
assert_eq!(
disp.resource_handle_files.get(&handle).copied(),
Some(refnum),
"LoadResource should restore the resource-file binding after reloading a purged handle"
);
assert_eq!(bus.read_word(0x0A60), 0, "LoadResource should report noErr");
}
#[test]
fn load_resource_populates_empty_handle_after_setresload_false() {
// IM:More Macintosh Toolbox 1993, p. 1-80: SetResLoad(FALSE) can
// return an empty handle for a present resource; LoadResource later
// fills the master pointer back in.
let (mut disp, mut cpu, mut bus) = setup();
let data_ptr = setup_resources(&mut disp, &mut bus, b"LOAD", 205, &[0x12, 0x34]);
disp.res_load = false;
let handle = disp.get_or_create_resource_handle(&mut bus, *b"LOAD", 205, data_ptr);
assert_eq!(bus.read_long(handle), 0);
cpu.write_reg(Register::A7, TEST_SP);
bus.write_long(TEST_SP, handle);
call(&mut disp, true, 0x1A2, &mut cpu, &mut bus).unwrap();
assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 4);
assert_eq!(bus.read_long(handle), data_ptr);
assert_eq!(bus.read_word(0x0A60), 0);
assert!(
disp.loaded_handles.contains_key(&handle),
"LoadResource should keep the reloaded resource registered as loaded"
);
assert!(
disp.resource_handle_files.contains_key(&handle),
"LoadResource should keep the resource-file ownership for the reloaded handle"
);
cpu.write_reg(Register::A0, data_ptr);
let recovered = disp.dispatch_memory(false, 0x28, &mut cpu, &mut bus);
assert!(recovered.is_some(), "RecoverHandle should be handled");
assert!(recovered.unwrap().is_ok(), "RecoverHandle should succeed");
assert_eq!(cpu.read_reg(Register::A0), handle);
}
#[test]
fn load_resource_restores_stale_detached_file_binding_when_reloading() {
let (mut disp, mut cpu, mut bus) = setup();
let data_ptr = setup_resources(&mut disp, &mut bus, b"DLOG", 206, &[0xDE, 0xAD]);
let handle = disp.get_or_create_resource_handle(&mut bus, *b"DLOG", 206, data_ptr);
let refnum = disp
.resource_handle_files
.get(&handle)
.copied()
.expect("resource handle should have a file binding");
disp.resource_handle_files.remove(&handle);
disp.detached_handle_files.insert(handle, refnum);
cpu.write_reg(Register::A7, TEST_SP);
bus.write_long(TEST_SP, handle);
call(&mut disp, true, 0x1A2, &mut cpu, &mut bus).unwrap();
assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 4);
assert_eq!(bus.read_long(handle), data_ptr);
assert_eq!(bus.read_word(0x0A60), 0);
assert_eq!(
disp.resource_handle_files.get(&handle).copied(),
Some(refnum)
);
assert!(!disp.detached_handle_files.contains_key(&handle));
}
#[test]
fn load_resource_non_resource_handle_leaves_handle_intact_and_reserr_noerr() {
let (mut disp, mut cpu, mut bus) = setup();
let fake_handle = bus.alloc(4);
bus.write_long(fake_handle, 0x00AB_CDEF);
cpu.write_reg(Register::A7, TEST_SP);
bus.write_long(TEST_SP, fake_handle);
call(&mut disp, true, 0x1A2, &mut cpu, &mut bus).unwrap();
assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 4);
assert_eq!(bus.read_long(fake_handle), 0x00AB_CDEF);
assert_eq!(bus.read_word(0x0A60), 0);
}
// ================================================================
// 5. ReleaseResource (0x1A3)
// ================================================================
#[test]
fn release_resource() {
let (mut disp, mut cpu, mut bus) = setup();
let data_ptr = setup_resources(&mut disp, &mut bus, b"DLOG", 200, &[0xAB; 8]);
let handle = disp.get_or_create_resource_handle(&mut bus, *b"DLOG", 200, data_ptr);
let sp = TEST_SP;
bus.write_long(sp, handle);
call(&mut disp, true, 0x1A3, &mut cpu, &mut bus).unwrap();
assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 4);
assert_eq!(bus.read_long(handle), 0);
assert!(!disp.loaded_handles.contains_key(&handle));
assert!(!disp.resource_handle_files.contains_key(&handle));
assert_eq!(bus.read_word(0x0A60), 0);
}
#[test]
fn release_resource_reschanged_handle_is_not_released_and_reserr_is_noerr() {
// IM:I I-121: ReleaseResource does not release a resource whose
// resChanged attribute is set, but ResError still reports noErr.
let (mut disp, mut cpu, mut bus) = setup();
let data_ptr = setup_resources(&mut disp, &mut bus, b"DLOG", 201, &[0xAA; 12]);
let handle = disp.get_or_create_resource_handle(&mut bus, *b"DLOG", 201, data_ptr);
let original_ptr = bus.read_long(handle);
// Mark resource changed via ChangedResource ($A9AA).
bus.write_long(TEST_SP, handle);
call(&mut disp, true, 0x1AA, &mut cpu, &mut bus).unwrap();
assert_eq!(bus.read_word(0x0A60), 0);
cpu.write_reg(Register::A7, TEST_SP);
bus.write_long(TEST_SP, handle);
call(&mut disp, true, 0x1A3, &mut cpu, &mut bus).unwrap();
assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 4);
assert_eq!(bus.read_long(handle), original_ptr);
assert!(disp.loaded_handles.contains_key(&handle));
assert!(disp.resource_handle_files.contains_key(&handle));
assert_eq!(bus.read_word(0x0A60), 0);
}
#[test]
fn release_resource_non_resource_handle_sets_resnotfound() {
// IM:I I-121: non-resource handles are ignored and report resNotFound.
let (mut disp, mut cpu, mut bus) = setup();
let fake_ptr = bus.alloc(4);
bus.write_bytes(fake_ptr, &[0xDE, 0xAD, 0xBE, 0xEF]);
let fake_handle = bus.alloc(4);
bus.write_long(fake_handle, fake_ptr);
bus.write_long(TEST_SP, fake_handle);
call(&mut disp, true, 0x1A3, &mut cpu, &mut bus).unwrap();
assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 4);
assert_eq!(bus.read_long(fake_handle), fake_ptr);
assert!(!disp.loaded_handles.contains_key(&fake_handle));
assert_eq!(bus.read_word(0x0A60) as i16, -192);
}
// ================================================================
// 5b. SizeResource (0x1A5)
// ================================================================
#[test]
fn size_resource_loaded_resource_handle_returns_byte_size() {
// IM:I I-121: SizeResource returns the resource size in bytes.
let (mut disp, mut cpu, mut bus) = setup();
let bytes = [0x10u8, 0x20, 0x30, 0x40, 0x50, 0x60, 0x70];
let data_ptr = setup_resources(&mut disp, &mut bus, b"STR ", 77, &bytes);
let handle = disp.get_or_create_resource_handle(&mut bus, *b"STR ", 77, data_ptr);
bus.write_word(0x0A60, (-192i16) as u16); // prove success clears stale error
bus.write_long(TEST_SP, handle);
call(&mut disp, true, 0x1A5, &mut cpu, &mut bus).unwrap();
let new_sp = cpu.read_reg(Register::A7);
assert_eq!(new_sp, TEST_SP + 4);
assert_eq!(bus.read_long(new_sp) as i32, bytes.len() as i32);
assert_eq!(bus.read_word(0x0A60), 0);
}
#[test]
fn size_resource_empty_handle_from_setresload_false_style_still_reports_size() {
// IM:I I-118..I-121: SetResLoad(FALSE) can yield an empty resource
// handle, but SizeResource still reports the resource-file byte size.
let (mut disp, mut cpu, mut bus) = setup();
let bytes = [0x41u8, 0x42, 0x43, 0x44, 0x45];
let data_ptr = setup_resources(&mut disp, &mut bus, b"ALRT", 90, &bytes);
let handle = disp.get_or_create_resource_handle(&mut bus, *b"ALRT", 90, data_ptr);
// Emulate an empty handle while retaining Resource Manager ownership.
bus.write_long(handle, 0);
bus.write_long(TEST_SP, handle);
call(&mut disp, true, 0x1A5, &mut cpu, &mut bus).unwrap();
let new_sp = cpu.read_reg(Register::A7);
assert_eq!(new_sp, TEST_SP + 4);
assert_eq!(bus.read_long(new_sp) as i32, bytes.len() as i32);
assert_eq!(bus.read_word(0x0A60), 0);
}
#[test]
fn size_resource_non_resource_handles_return_minus_one_and_resnotfound() {
// IM:I I-121: SizeResource returns -1 and resNotFound for handles that
// are not Resource Manager handles (including detached handles).
let (mut disp, mut cpu, mut bus) = setup();
// Unknown handle case.
let fake_ptr = bus.alloc(3);
bus.write_bytes(fake_ptr, &[0x01, 0x02, 0x03]);
let fake_handle = bus.alloc(4);
bus.write_long(fake_handle, fake_ptr);
bus.write_long(TEST_SP, fake_handle);
call(&mut disp, true, 0x1A5, &mut cpu, &mut bus).unwrap();
let mut new_sp = cpu.read_reg(Register::A7);
assert_eq!(new_sp, TEST_SP + 4);
assert_eq!(bus.read_long(new_sp) as i32, -1);
assert_eq!(bus.read_word(0x0A60) as i16, -192);
// Detached resource handle case.
let data_ptr = setup_resources(&mut disp, &mut bus, b"DLOG", 301, &[0xAA; 6]);
let res_handle = disp.get_or_create_resource_handle(&mut bus, *b"DLOG", 301, data_ptr);
cpu.write_reg(Register::A7, TEST_SP);
bus.write_long(TEST_SP, res_handle);
call(&mut disp, true, 0x192, &mut cpu, &mut bus).unwrap(); // DetachResource
cpu.write_reg(Register::A7, TEST_SP);
bus.write_long(TEST_SP, res_handle);
call(&mut disp, true, 0x1A5, &mut cpu, &mut bus).unwrap();
new_sp = cpu.read_reg(Register::A7);
assert_eq!(new_sp, TEST_SP + 4);
assert_eq!(bus.read_long(new_sp) as i32, -1);
assert_eq!(bus.read_word(0x0A60) as i16, -192);
}
// ================================================================
// 5c. MaxSizeRsrc (0x021)
// ================================================================
#[test]
fn maxsizersrc_returns_resource_size_for_loaded_resource_handle() {
// IM:IV IV-16: MaxSizeRsrc returns resource size from the map.
let (mut disp, mut cpu, mut bus) = setup();
let bytes = [0xAAu8, 0xBB, 0xCC, 0xDD, 0xEE, 0xFF];
let data_ptr = setup_resources(&mut disp, &mut bus, b"MSIZ", 42, &bytes);
let handle = disp.get_or_create_resource_handle(&mut bus, *b"MSIZ", 42, data_ptr);
bus.write_word(0x0A60, (-192i16) as u16); // stale error should clear
bus.write_long(TEST_SP, handle);
call(&mut disp, true, 0x021, &mut cpu, &mut bus).unwrap();
let new_sp = cpu.read_reg(Register::A7);
assert_eq!(new_sp, TEST_SP + 4);
assert_eq!(bus.read_long(new_sp) as i32, bytes.len() as i32);
assert_eq!(bus.read_word(0x0A60), 0);
}
#[test]
fn maxsizersrc_returns_minus_one_and_sets_resnotfound_for_non_resource_handle() {
// IM:I I-121 + IM:IV IV-16: non-resource handles report
// "not found" and return -1 size.
let (mut disp, mut cpu, mut bus) = setup();
let fake_ptr = bus.alloc(5);
bus.write_bytes(fake_ptr, &[1, 2, 3, 4, 5]);
let fake_handle = bus.alloc(4);
bus.write_long(fake_handle, fake_ptr);
bus.write_long(TEST_SP, fake_handle);
call(&mut disp, true, 0x021, &mut cpu, &mut bus).unwrap();
let new_sp = cpu.read_reg(Register::A7);
assert_eq!(new_sp, TEST_SP + 4);
assert_eq!(bus.read_long(new_sp) as i32, -1);
assert_eq!(bus.read_word(0x0A60) as i16, -192);
}
#[test]
fn maxsizersrc_consumes_handle_argument_and_writes_function_result_slot() {
// FUNCTION MaxSizeRsrc(theResource: Handle): LONGINT
// (IM:IV IV-16): pop 4-byte arg, write 4-byte result at new SP.
let (mut disp, mut cpu, mut bus) = setup();
let data_ptr = setup_resources(&mut disp, &mut bus, b"MSZ2", 7, &[0x10, 0x20, 0x30]);
let handle = disp.get_or_create_resource_handle(&mut bus, *b"MSZ2", 7, data_ptr);
bus.write_long(TEST_SP, handle);
bus.write_long(TEST_SP + 4, 0xDEADBEEF); // result slot sentinel
call(&mut disp, true, 0x021, &mut cpu, &mut bus).unwrap();
assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 4);
assert_eq!(
bus.read_long(TEST_SP + 4),
3,
"MaxSizeRsrc writes result to function-result slot at post-pop SP"
);
}
// ================================================================
// 5d. ResourceDispatch (0x022) — selectors 1/2/3
// ================================================================
#[test]
fn readpartialresource_selector_1_copies_requested_bytes_and_pops_16_bytes() {
// MMTB 1993 1-69 + IM:VI C-27: selector 1 (or $7001 glue form)
// performs ReadPartialResource and consumes 16 bytes of args.
let (mut disp, mut cpu, mut bus) = setup();
let data_ptr = setup_resources(
&mut disp,
&mut bus,
b"PART",
90,
&[0x10, 0x20, 0x30, 0x40, 0x50],
);
let handle = disp.get_or_create_resource_handle(&mut bus, *b"PART", 90, data_ptr);
let buffer = bus.alloc(6);
bus.write_bytes(buffer, &[0xEE; 6]);
bus.write_long(TEST_SP, 3); // count
bus.write_long(TEST_SP + 4, buffer);
bus.write_long(TEST_SP + 8, 1); // offset
bus.write_long(TEST_SP + 12, handle);
cpu.write_reg(Register::D0, 0x7001); // low-byte selector form
call(&mut disp, true, 0x022, &mut cpu, &mut bus).unwrap();
assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 16);
assert_eq!(bus.read_bytes(buffer, 3), vec![0x20, 0x30, 0x40]);
assert_eq!(bus.read_word(0x0A60) as i16, -188);
}
#[test]
fn writepartialresource_selector_2_extends_resource_and_sets_writingpastend() {
// MMTB 1993 1-70: writes past end extend the resource and report
// writingPastEnd (-189).
let (mut disp, mut cpu, mut bus) = setup();
let data_ptr = setup_resources(&mut disp, &mut bus, b"WRPT", 91, &[0xAA, 0xBB, 0xCC, 0xDD]);
let handle = disp.get_or_create_resource_handle(&mut bus, *b"WRPT", 91, data_ptr);
let src = bus.alloc(4);
bus.write_bytes(src, &[1, 2, 3, 4]);
bus.write_long(TEST_SP, 4); // count
bus.write_long(TEST_SP + 4, src);
bus.write_long(TEST_SP + 8, 3); // offset
bus.write_long(TEST_SP + 12, handle);
cpu.write_reg(Register::D0, 0x0002);
call(&mut disp, true, 0x022, &mut cpu, &mut bus).unwrap();
assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 16);
assert_eq!(bus.read_word(0x0A60) as i16, -189);
let live_ptr = bus.read_long(handle);
assert_eq!(bus.get_alloc_size(live_ptr).unwrap_or(0), 7);
assert_eq!(
bus.read_bytes(live_ptr, 7),
vec![0xAA, 0xBB, 0xCC, 1, 2, 3, 4]
);
}
#[test]
fn writepartialresource_selector_2_zero_count_does_not_resize_resource() {
// MMTB 1993 1-70: a zero-length write is a no-op and does not
// grow the resource or report writingPastEnd.
let (mut disp, mut cpu, mut bus) = setup();
let data_ptr = setup_resources(&mut disp, &mut bus, b"ZCNT", 93, &[0x11, 0x22, 0x33, 0x44]);
let handle = disp.get_or_create_resource_handle(&mut bus, *b"ZCNT", 93, data_ptr);
let src = bus.alloc(4);
bus.write_bytes(src, &[9, 8, 7, 6]);
bus.write_long(TEST_SP, 0); // count
bus.write_long(TEST_SP + 4, src);
bus.write_long(TEST_SP + 8, 7); // offset past end
bus.write_long(TEST_SP + 12, handle);
cpu.write_reg(Register::D0, 0x0002);
call(&mut disp, true, 0x022, &mut cpu, &mut bus).unwrap();
assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 16);
assert_eq!(bus.read_word(0x0A60), 0);
let live_ptr = bus.read_long(handle);
assert_eq!(bus.get_alloc_size(live_ptr).unwrap_or(0), 4);
assert_eq!(bus.read_bytes(live_ptr, 4), vec![0x11, 0x22, 0x33, 0x44]);
}
#[test]
fn setresourcesize_selector_3_updates_resource_size_visible_to_sizeresource() {
// MMTB 1993 1-71: selector 3 updates resource size.
let (mut disp, mut cpu, mut bus) = setup();
let data_ptr = setup_resources(&mut disp, &mut bus, b"RSIZ", 92, &[0x10, 0x20, 0x30]);
let handle = disp.get_or_create_resource_handle(&mut bus, *b"RSIZ", 92, data_ptr);
bus.write_long(TEST_SP, 9); // newSize
bus.write_long(TEST_SP + 4, handle);
cpu.write_reg(Register::D0, 0x0003);
call(&mut disp, true, 0x022, &mut cpu, &mut bus).unwrap();
assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 8);
assert_eq!(bus.read_word(0x0A60), 0);
cpu.write_reg(Register::A7, TEST_SP);
bus.write_long(TEST_SP, handle);
call(&mut disp, true, 0x1A5, &mut cpu, &mut bus).unwrap(); // SizeResource
assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 4);
assert_eq!(bus.read_long(TEST_SP + 4), 9);
assert_eq!(bus.read_word(0x0A60), 0);
}
#[test]
fn setresourcesize_selector_3_resizes_emptied_resource_handle_in_place() {
// IM:VI 13-23: SetResourceSize changes the size of a resource
// without writing data. An emptied resource handle should still
// stay tied to the same logical resource after resizing.
let (mut disp, mut cpu, mut bus) = setup();
let data_ptr = setup_resources(&mut disp, &mut bus, b"RSZ0", 93, &[0xAA, 0xBB, 0xCC]);
let handle = disp.get_or_create_resource_handle(&mut bus, *b"RSZ0", 93, data_ptr);
bus.write_long(handle, 0);
bus.write_long(TEST_SP, 7);
bus.write_long(TEST_SP + 4, handle);
cpu.write_reg(Register::D0, 0x0003);
call(&mut disp, true, 0x022, &mut cpu, &mut bus).unwrap();
assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 8);
assert_eq!(bus.read_word(0x0A60), 0);
assert_ne!(
bus.read_long(handle),
0,
"resized empty handle should be repopulated"
);
cpu.write_reg(Register::A7, TEST_SP);
bus.write_word(TEST_SP, 93);
bus.write_long(TEST_SP + 2, u32::from_be_bytes(*b"RSZ0"));
bus.write_long(TEST_SP + 6, 0xDEADBEEF);
call(&mut disp, true, 0x1A0, &mut cpu, &mut bus).unwrap();
assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 6);
assert_eq!(
bus.read_long(TEST_SP + 6),
handle,
"relookup should resolve to the same handle after resize"
);
assert_eq!(bus.read_word(0x0A60), 0);
cpu.write_reg(Register::A7, TEST_SP);
bus.write_long(TEST_SP, handle);
call(&mut disp, true, 0x1A5, &mut cpu, &mut bus).unwrap();
assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 4);
assert_eq!(bus.read_long(TEST_SP + 4), 7);
assert_eq!(bus.read_word(0x0A60), 0);
}
// ================================================================
// 6. ResError (0x1AF)
// ================================================================
#[test]
fn res_error() {
let (mut disp, mut cpu, mut bus) = setup();
// Set ResErr to a known value
bus.write_word(0x0A60, (-108i16) as u16);
call(&mut disp, true, 0x1AF, &mut cpu, &mut bus).unwrap();
let sp = cpu.read_reg(Register::A7);
assert_eq!(sp, TEST_SP, "SP should be unchanged");
let result = bus.read_word(sp) as i16;
assert_eq!(result, -108);
}
fn addreference_setup() -> (
super::super::TrapDispatcher,
MockCpu,
crate::memory::MacMemoryBus,
u32,
u32,
) {
let (mut disp, cpu, mut bus) = setup();
let source_ptr = bus.alloc(8);
bus.write_bytes(source_ptr, &[0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88]);
let mut file0_loaded = HashMap::new();
file0_loaded.insert((*b"CURS", 1), source_ptr);
let mut file0 = ResourceFileMap {
loaded: file0_loaded,
named: HashMap::new(),
attrs: HashMap::from([((*b"CURS", 1), 0x0004u8)]),
map_attrs: 0,
};
file0
.named
.insert((*b"CURS", "SystemCursor".to_string()), (1, source_ptr));
let file1 = ResourceFileMap::default();
disp.resources = Some(LoadedResources {
files: HashMap::from([(0, file0), (1, file1)]),
names: HashMap::new(),
search_order: vec![0, 1],
current_file: 1,
});
let source_handle =
disp.get_or_create_resource_handle_in_file(&mut bus, *b"CURS", 1, source_ptr, 0);
(disp, cpu, bus, source_handle, source_ptr)
}
#[test]
fn addreference_adds_system_reference_and_marks_current_file_changed() {
let (mut disp, mut cpu, mut bus, source_handle, source_ptr) = addreference_setup();
let sp = TEST_SP;
let name_ptr = 0x200000u32;
write_pstring(&mut bus, name_ptr, b"AddedReference");
let ref_id = 200i16;
let current_before = disp.current_resource_refnum();
bus.write_long(sp, name_ptr);
bus.write_word(sp + 4, ref_id as u16);
bus.write_long(sp + 6, source_handle);
call_trap_word(&mut disp, 0xA9AC, &mut cpu, &mut bus).unwrap();
assert_eq!(cpu.read_reg(Register::A7), sp + 10);
assert_eq!(bus.read_word(0x0A60), 0);
assert_eq!(disp.current_resource_refnum(), current_before);
assert_eq!(
disp.resource_handle_files.get(&source_handle).copied(),
Some(0)
);
let file = disp.resources.as_ref().unwrap().files.get(&1).unwrap();
assert_eq!(disp.loaded_handles.get(&source_handle).copied(), Some((source_ptr, *b"CURS", 1)));
assert_eq!(file.loaded.get(&(*b"CURS", ref_id)), Some(&source_ptr));
assert_eq!(file.named.get(&(*b"CURS", "AddedReference".to_string())), Some(&(ref_id, source_ptr)));
assert_eq!(
file.attrs.get(&(*b"CURS", ref_id)).copied().unwrap_or(0)
& (super::super::TrapDispatcher::RES_CHANGED_ATTR as u8
| super::super::TrapDispatcher::RES_SYS_REF_ATTR as u8),
(super::super::TrapDispatcher::RES_CHANGED_ATTR as u8
| super::super::TrapDispatcher::RES_SYS_REF_ATTR as u8)
);
assert_ne!(
file.map_attrs & super::super::TrapDispatcher::RES_MAP_CHANGED_ATTR,
0
);
}
// Inside Macintosh Volume I (1985), p. I-124: RmvResource removes the
// current-file reference, leaves the handle's data allocated, and keeps
// the current resource file unchanged.
#[test]
fn rmvereference_removes_current_file_reference_without_disposing_handle_data() {
let (mut disp, mut cpu, mut bus) = setup();
let data_ptr = setup_resources(
&mut disp,
&mut bus,
b"STR ",
30000,
&[0x10, 0x20, 0x30, 0x40],
);
let handle = disp.get_or_create_resource_handle(&mut bus, *b"STR ", 30000, data_ptr);
let current_before = disp.current_resource_refnum();
bus.write_long(TEST_SP, handle);
call_trap_word(&mut disp, 0xA9AE, &mut cpu, &mut bus).unwrap();
assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 4);
assert_eq!(bus.read_word(0x0A60), 0);
assert_eq!(disp.current_resource_refnum(), current_before);
assert_eq!(bus.read_long(handle), data_ptr);
assert!(!disp.loaded_handles.contains_key(&handle));
assert!(!disp.resource_handle_files.contains_key(&handle));
assert!(!disp
.resources
.as_ref()
.unwrap()
.files
.get(&0)
.unwrap()
.loaded
.contains_key(&(*b"STR ", 30000)));
}
#[test]
fn addreference_shadows_system_handle_metadata_in_current_file() {
let (mut disp, mut cpu, mut bus, source_handle, _source_ptr) = addreference_setup();
let sp = TEST_SP;
let add_name_ptr = 0x200020u32;
let info_name_ptr = 0x200040u32;
let info_type_ptr = 0x200060u32;
let info_id_ptr = 0x200080u32;
write_pstring(&mut bus, add_name_ptr, b"AddedReference");
let ref_id = 206i16;
bus.write_long(sp, add_name_ptr);
bus.write_word(sp + 4, ref_id as u16);
bus.write_long(sp + 6, source_handle);
call_trap_word(&mut disp, 0xA9AC, &mut cpu, &mut bus).unwrap();
assert_eq!(bus.read_word(0x0A60), 0);
bus.write_long(sp, info_name_ptr);
bus.write_long(sp + 4, info_type_ptr);
bus.write_long(sp + 8, info_id_ptr);
bus.write_long(sp + 12, source_handle);
cpu.write_reg(Register::A7, sp);
call_trap_word(&mut disp, 0xA9A8, &mut cpu, &mut bus).unwrap();
assert_eq!(cpu.read_reg(Register::A7), sp + 16);
assert_eq!(bus.read_word(0x0A60), 0);
assert_eq!(disp.resource_handle_files.get(&source_handle).copied(), Some(0));
assert_eq!(bus.read_word(info_id_ptr), ref_id as u16);
assert_eq!(bus.read_long(info_type_ptr), u32::from_be_bytes(*b"CURS"));
assert_eq!(bus.read_pstring(info_name_ptr), b"AddedReference".to_vec());
cpu.write_reg(Register::A7, sp);
bus.write_long(sp, source_handle);
bus.write_word(sp + 4, 0xBEEF);
call_trap_word(&mut disp, 0xA9A6, &mut cpu, &mut bus).unwrap();
let attrs = bus.read_word(sp + 4);
assert_eq!(bus.read_word(0x0A60), 0);
assert_ne!(
attrs
& (super::super::TrapDispatcher::RES_CHANGED_ATTR as u16
| super::super::TrapDispatcher::RES_SYS_REF_ATTR as u16),
0
);
assert_ne!(attrs & super::super::TrapDispatcher::RES_SYS_REF_ATTR as u16, 0);
assert_ne!(attrs & super::super::TrapDispatcher::RES_CHANGED_ATTR as u16, 0);
}
#[test]
fn addreference_preserves_a5_a6_and_pops_success_frame() {
let (mut disp, mut cpu, mut bus, source_handle, _source_ptr) = addreference_setup();
let sp = TEST_SP;
let name_ptr = 0x200180u32;
write_pstring(&mut bus, name_ptr, b"Regs");
let ref_id = 204i16;
cpu.write_reg(Register::A5, 0x1A5A_0000);
cpu.write_reg(Register::A6, 0x1A6A_0000);
bus.write_long(sp, name_ptr);
bus.write_word(sp + 4, ref_id as u16);
bus.write_long(sp + 6, source_handle);
call_trap_word(&mut disp, 0xA9AC, &mut cpu, &mut bus).unwrap();
assert_eq!(cpu.read_reg(Register::A7), sp + 10);
assert_eq!(cpu.read_reg(Register::A5), 0x1A5A_0000);
assert_eq!(cpu.read_reg(Register::A6), 0x1A6A_0000);
assert_eq!(bus.read_word(0x0A60), 0);
}
#[test]
fn addreference_preserves_all_nonvolatile_registers_on_success() {
let (mut disp, mut cpu, mut bus, source_handle, _source_ptr) = addreference_setup();
let sp = TEST_SP;
let name_ptr = 0x2001C0u32;
write_pstring(&mut bus, name_ptr, b"AllRegs");
let ref_id = 205i16;
let sentinels = [
(Register::D2, 0xD202_0202),
(Register::D3, 0xD303_0303),
(Register::D4, 0xD404_0404),
(Register::D5, 0xD505_0505),
(Register::D6, 0xD606_0606),
(Register::D7, 0xD707_0707),
(Register::A2, 0xA202_0202),
(Register::A3, 0xA303_0303),
(Register::A4, 0xA404_0404),
(Register::A5, 0xA505_0505),
(Register::A6, 0xA606_0606),
];
for &(reg, value) in &sentinels {
cpu.write_reg(reg, value);
}
bus.write_long(sp, name_ptr);
bus.write_word(sp + 4, ref_id as u16);
bus.write_long(sp + 6, source_handle);
call_trap_word(&mut disp, 0xA9AC, &mut cpu, &mut bus).unwrap();
assert_eq!(cpu.read_reg(Register::A7), sp + 10);
for &(reg, value) in &sentinels {
assert_eq!(cpu.read_reg(reg), value, "{reg:?} should be preserved");
}
assert_eq!(bus.read_word(0x0A60), 0);
}
#[test]
fn addreference_duplicate_reference_sets_addreffailed() {
let (mut disp, mut cpu, mut bus, source_handle, _source_ptr) = addreference_setup();
let sp = TEST_SP;
let name_ptr = 0x200200u32;
write_pstring(&mut bus, name_ptr, b"DuplicateRef");
let ref_id = 201i16;
bus.write_long(sp, name_ptr);
bus.write_word(sp + 4, ref_id as u16);
bus.write_long(sp + 6, source_handle);
call_trap_word(&mut disp, 0xA9AC, &mut cpu, &mut bus).unwrap();
assert_eq!(bus.read_word(0x0A60), 0);
bus.write_long(sp, name_ptr);
bus.write_word(sp + 4, ref_id as u16);
bus.write_long(sp + 6, source_handle);
call_trap_word(&mut disp, 0xA9AC, &mut cpu, &mut bus).unwrap();
assert_eq!(bus.read_word(0x0A60) as i16, -195);
assert_eq!(
disp.resources.as_ref().unwrap().files.get(&1).unwrap().loaded.len(),
1
);
}
#[test]
fn addreference_non_system_handle_sets_addreffailed() {
let (mut disp, mut cpu, mut bus, _source_handle, _source_ptr) = addreference_setup();
let sp = TEST_SP;
let name_ptr = 0x200300u32;
write_pstring(&mut bus, name_ptr, b"RawHandle");
let raw_ptr = bus.alloc(4);
bus.write_long(raw_ptr, 0xCAFEBABE);
let raw_handle = bus.alloc(4);
bus.write_long(raw_handle, raw_ptr);
bus.write_long(sp, name_ptr);
bus.write_word(sp + 4, 202u16);
bus.write_long(sp + 6, raw_handle);
call_trap_word(&mut disp, 0xA9AC, &mut cpu, &mut bus).unwrap();
assert_eq!(cpu.read_reg(Register::A7), sp + 10);
assert_eq!(bus.read_word(0x0A60) as i16, -195);
assert!(!disp
.resources
.as_ref()
.unwrap()
.files
.get(&1)
.unwrap()
.loaded
.contains_key(&(*b"CURS", 202)));
}
#[test]
fn addreference_current_system_file_sets_addreffailed() {
let (mut disp, mut cpu, mut bus, source_handle, _source_ptr) = addreference_setup();
let sp = TEST_SP;
let name_ptr = 0x200400u32;
write_pstring(&mut bus, name_ptr, b"SysFileRef");
bus.write_word(sp, 0);
let result = disp.dispatch_toolbox(true, 0x198, &mut cpu, &mut bus);
assert!(result.is_some());
assert!(result.unwrap().is_ok());
assert_eq!(bus.read_word(0x0A60), 0);
assert_eq!(disp.current_resource_refnum(), 0);
cpu.write_reg(Register::A7, sp);
bus.write_long(sp, name_ptr);
bus.write_word(sp + 4, 203u16);
bus.write_long(sp + 6, source_handle);
call_trap_word(&mut disp, 0xA9AC, &mut cpu, &mut bus).unwrap();
assert_eq!(cpu.read_reg(Register::A7), sp + 10);
assert_eq!(bus.read_word(0x0A60) as i16, -195);
assert_eq!(disp.current_resource_refnum(), 0);
}
// ================================================================
// 7. Get1NamedResource (0x020) — found
// ================================================================
#[test]
fn get1_named_resource_found() {
let (mut disp, mut cpu, mut bus) = setup();
let data_ptr = bus.alloc(16);
bus.write_bytes(data_ptr, &[0x42; 16]);
let mut loaded = HashMap::new();
loaded.insert((*b"STR ", 500i16), data_ptr);
let mut named = HashMap::new();
named.insert((*b"STR ", "MyString".to_string()), (500i16, data_ptr));
disp.resources = Some(LoadedResources {
files: HashMap::from([(
0,
ResourceFileMap {
loaded,
named,
attrs: HashMap::new(),
map_attrs: 0,
},
)]),
names: HashMap::new(),
search_order: vec![0],
current_file: 0,
});
// Write Pascal string name at 0x200000
let name_addr = 0x200000u32;
write_pstring(&mut bus, name_addr, b"MyString");
let sp = TEST_SP;
bus.write_long(sp, name_addr); // name_ptr
bus.write_long(sp + 4, u32::from_be_bytes(*b"STR ")); // type
call(&mut disp, true, 0x020, &mut cpu, &mut bus).unwrap();
let new_sp = cpu.read_reg(Register::A7);
assert_eq!(new_sp, TEST_SP + 8);
let handle = bus.read_long(new_sp);
assert_ne!(
handle, 0,
"handle should be non-zero for found named resource"
);
}
// ================================================================
// 7b. GetResInfo (0x1A8) — named resource metadata
// ================================================================
#[test]
fn get_res_info_named_resource_returns_id_type_and_name() {
let (mut disp, mut cpu, mut bus) = setup();
let data_ptr = setup_resources(&mut disp, &mut bus, b"STR ", 500, &[0x42; 16]);
disp.resources
.as_mut()
.unwrap()
.files
.get_mut(&0)
.unwrap()
.named
.insert((*b"STR ", "MyString".to_string()), (500, data_ptr));
let handle = disp.get_or_create_resource_handle(&mut bus, *b"STR ", 500, data_ptr);
let name_ptr = 0x200000u32;
let type_ptr = 0x200100u32;
let id_ptr = 0x200104u32;
let sp = TEST_SP;
bus.write_long(sp, name_ptr);
bus.write_long(sp + 4, type_ptr);
bus.write_long(sp + 8, id_ptr);
bus.write_long(sp + 12, handle);
call(&mut disp, true, 0x1A8, &mut cpu, &mut bus).unwrap();
assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 16);
assert_eq!(bus.read_word(id_ptr) as i16, 500);
assert_eq!(bus.read_long(type_ptr), u32::from_be_bytes(*b"STR "));
assert_eq!(bus.read_byte(name_ptr), 8);
assert_eq!(bus.read_bytes(name_ptr + 1, 8), b"MyString");
assert_eq!(bus.read_word(0x0A60), 0);
}
// ================================================================
// 7c. GetResInfo (0x1A8) — unnamed resource clears stale Str255
// ================================================================
#[test]
fn get_res_info_unnamed_resource_clears_name_buffer() {
let (mut disp, mut cpu, mut bus) = setup();
let data_ptr = setup_resources(&mut disp, &mut bus, b"TEST", 7, &[0x10, 0x20, 0x30]);
let handle = disp.get_or_create_resource_handle(&mut bus, *b"TEST", 7, data_ptr);
let name_ptr = 0x200200u32;
let type_ptr = 0x200300u32;
let id_ptr = 0x200304u32;
bus.write_bytes(name_ptr, b"urer\0");
let sp = TEST_SP;
bus.write_long(sp, name_ptr);
bus.write_long(sp + 4, type_ptr);
bus.write_long(sp + 8, id_ptr);
bus.write_long(sp + 12, handle);
call(&mut disp, true, 0x1A8, &mut cpu, &mut bus).unwrap();
assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 16);
assert_eq!(bus.read_word(id_ptr) as i16, 7);
assert_eq!(bus.read_long(type_ptr), u32::from_be_bytes(*b"TEST"));
assert_eq!(bus.read_byte(name_ptr), 0);
assert_eq!(bus.read_word(0x0A60), 0);
}
// More Macintosh Toolbox (1993), pp. 1-81 to 1-82: if the handle is not
// a resource handle, GetResInfo does nothing and ResError is resNotFound.
#[test]
fn get_res_info_non_resource_handle_sets_resnotfound() {
let (mut disp, mut cpu, mut bus) = setup();
let fake_ptr = bus.alloc(4);
bus.write_long(fake_ptr, 0xDEADBEEF);
let fake_handle = bus.alloc(4);
bus.write_long(fake_handle, fake_ptr);
let name_ptr = 0x200400u32;
let type_ptr = 0x200500u32;
let id_ptr = 0x200504u32;
bus.write_pstring(name_ptr, b"keepme");
bus.write_long(type_ptr, 0x11223344);
bus.write_word(id_ptr, 0x5566);
bus.write_long(TEST_SP, name_ptr);
bus.write_long(TEST_SP + 4, type_ptr);
bus.write_long(TEST_SP + 8, id_ptr);
bus.write_long(TEST_SP + 12, fake_handle);
call(&mut disp, true, 0x1A8, &mut cpu, &mut bus).unwrap();
assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 16);
assert_eq!(bus.read_word(0x0A60), (-192i16) as u16);
assert_eq!(bus.read_byte(name_ptr), 6);
assert_eq!(bus.read_long(type_ptr), 0x11223344);
assert_eq!(bus.read_word(id_ptr), 0x5566);
}
// ================================================================
// 7d. SetResInfo (0x1A9)
// ================================================================
// Inside Macintosh Volume I (1985), p. I-122: SetResInfo takes
// (Handle, Integer, Str255 ptr) and pops 10 bytes.
#[test]
fn setresinfo_consumes_handle_id_and_name_arguments() {
let (mut disp, mut cpu, mut bus) = setup();
let data_ptr = setup_resources(&mut disp, &mut bus, b"SINF", 7, &[0x10, 0x20]);
let handle = disp.get_or_create_resource_handle(&mut bus, *b"SINF", 7, data_ptr);
let name_ptr = 0x200600u32;
write_pstring(&mut bus, name_ptr, b"Renamed");
bus.write_long(TEST_SP, name_ptr);
bus.write_word(TEST_SP + 4, 11u16);
bus.write_long(TEST_SP + 6, handle);
bus.write_word(TEST_SP + 10, 0xBEEF);
call(&mut disp, true, 0x1A9, &mut cpu, &mut bus).unwrap();
assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 10);
assert_eq!(bus.read_word(TEST_SP + 10), 0xBEEF);
assert_eq!(bus.read_word(0x0A60), 0);
}
// Inside Macintosh Volume I (1985), p. I-122: SetResInfo updates
// resource map metadata (ID and name) for the specified resource.
#[test]
fn setresinfo_updates_resource_id_and_name_for_unprotected_resource() {
let (mut disp, mut cpu, mut bus) = setup();
let data_ptr = setup_resources(&mut disp, &mut bus, b"NAME", 7, &[0x31, 0x32, 0x33]);
let handle = disp.get_or_create_resource_handle(&mut bus, *b"NAME", 7, data_ptr);
disp.resources
.as_mut()
.unwrap()
.files
.get_mut(&0)
.unwrap()
.named
.insert((*b"NAME", "OldName".to_string()), (7, data_ptr));
let name_ptr = 0x200700u32;
write_pstring(&mut bus, name_ptr, b"NewName");
bus.write_long(TEST_SP, name_ptr);
bus.write_word(TEST_SP + 4, 11u16);
bus.write_long(TEST_SP + 6, handle);
call(&mut disp, true, 0x1A9, &mut cpu, &mut bus).unwrap();
let file = disp.resources.as_ref().unwrap().files.get(&0).unwrap();
assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 10);
assert_eq!(bus.read_word(0x0A60), 0);
assert_eq!(
disp.loaded_handles.get(&handle).map(|entry| entry.2),
Some(11i16)
);
assert!(file.loaded.contains_key(&(*b"NAME", 11)));
assert!(!file.loaded.contains_key(&(*b"NAME", 7)));
assert_eq!(
file.named.get(&(*b"NAME", "NewName".to_string())),
Some(&(11, data_ptr))
);
assert!(!file.named.contains_key(&(*b"NAME", "OldName".to_string())));
}
// Inside Macintosh Volume I (1985), p. I-122: resource metadata APIs
// report missing-handle failures via ResError.
#[test]
fn setresinfo_invalid_handle_sets_resnotfound() {
let (mut disp, mut cpu, mut bus) = setup();
let fake_ptr = bus.alloc(4);
bus.write_long(fake_ptr, 0xCAFEBABE);
let fake_handle = bus.alloc(4);
bus.write_long(fake_handle, fake_ptr);
let name_ptr = 0x200800u32;
write_pstring(&mut bus, name_ptr, b"NeverUsed");
bus.write_long(TEST_SP, name_ptr);
bus.write_word(TEST_SP + 4, 19u16);
bus.write_long(TEST_SP + 6, fake_handle);
call(&mut disp, true, 0x1A9, &mut cpu, &mut bus).unwrap();
assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 10);
assert_eq!(bus.read_word(0x0A60) as i16, -192);
}
// Inside Macintosh Volume IV (1986), p. IV-16: resProtected blocks
// metadata mutation for SetResInfo and returns resAttrErr.
#[test]
fn setresinfo_protected_resource_is_noop_with_resattrerr() {
let (mut disp, mut cpu, mut bus) = setup();
let data_ptr = setup_resources(&mut disp, &mut bus, b"PROT", 3, &[0x01, 0x02]);
let handle = disp.get_or_create_resource_handle(&mut bus, *b"PROT", 3, data_ptr);
disp.resources
.as_mut()
.unwrap()
.files
.get_mut(&0)
.unwrap()
.named
.insert((*b"PROT", "KeepName".to_string()), (3, data_ptr));
disp.resources
.as_mut()
.unwrap()
.files
.get_mut(&0)
.unwrap()
.attrs
.insert((*b"PROT", 3), 0x0008u8);
let name_ptr = 0x200900u32;
write_pstring(&mut bus, name_ptr, b"NewName");
bus.write_long(TEST_SP, name_ptr);
bus.write_word(TEST_SP + 4, 22u16);
bus.write_long(TEST_SP + 6, handle);
call(&mut disp, true, 0x1A9, &mut cpu, &mut bus).unwrap();
let file = disp.resources.as_ref().unwrap().files.get(&0).unwrap();
assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 10);
assert_eq!(
bus.read_word(0x0A60) as i16,
super::super::TrapDispatcher::RES_ATTR_ERR
);
assert_eq!(
disp.loaded_handles.get(&handle).map(|entry| entry.2),
Some(3i16)
);
assert!(file.loaded.contains_key(&(*b"PROT", 3)));
assert!(!file.loaded.contains_key(&(*b"PROT", 22)));
assert!(file.named.contains_key(&(*b"PROT", "KeepName".to_string())));
assert!(!file.named.contains_key(&(*b"PROT", "NewName".to_string())));
}
// ================================================================
// 7e. GetResFileAttrs (0x1F6) / SetResFileAttrs (0x1F7)
// ================================================================
// Inside Macintosh Volume I (1985), p. I-126: GetResFileAttrs returns
// the file map attribute word for a valid resource-file refNum.
#[test]
fn getresfileattrs_returns_only_documented_map_attr_bits_for_open_resource_file() {
let (mut disp, mut cpu, mut bus) = setup();
setup_resources(&mut disp, &mut bus, b"RFAT", 1, &[0xAA]);
disp.resources
.as_mut()
.unwrap()
.files
.get_mut(&0)
.unwrap()
.map_attrs = 0x0234;
bus.write_word(TEST_SP, 0u16);
bus.write_word(TEST_SP + 2, 0xBEEF);
call(&mut disp, true, 0x1F6, &mut cpu, &mut bus).unwrap();
assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 2);
assert_eq!(bus.read_word(TEST_SP + 2), 0x0020);
assert_eq!(bus.read_word(0x0A60), 0);
}
// Inside Macintosh Volume I (1985), p. I-126: if refNum is unknown,
// GetResFileAttrs does nothing and returns resFNotFound in ResError.
#[test]
fn getresfileattrs_missing_refnum_sets_resfnotfound_and_preserves_result_slot() {
let (mut disp, mut cpu, mut bus) = setup();
setup_resources(&mut disp, &mut bus, b"RFAT", 1, &[0xAA]);
bus.write_word(TEST_SP, 99u16);
bus.write_word(TEST_SP + 2, 0xBEEF);
call(&mut disp, true, 0x1F6, &mut cpu, &mut bus).unwrap();
assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 2);
assert_eq!(bus.read_word(TEST_SP + 2), 0xBEEF);
assert_eq!(bus.read_word(0x0A60) as i16, -193);
}
// Inside Macintosh Volume I (1985), p. I-126: GetResFileAttrs is a
// Pascal FUNCTION that consumes one INTEGER refNum and writes one
// INTEGER result slot.
#[test]
fn getresfileattrs_consumes_refnum_and_result_slot() {
let (mut disp, mut cpu, mut bus) = setup();
setup_resources(&mut disp, &mut bus, b"RFAT", 1, &[0xAA]);
disp.resources
.as_mut()
.unwrap()
.files
.get_mut(&0)
.unwrap()
.map_attrs = 0x0234;
bus.write_word(TEST_SP, 0u16);
bus.write_word(TEST_SP + 2, 0xBEEF);
let sp_pre = cpu.read_reg(Register::A7);
call(&mut disp, true, 0x1F6, &mut cpu, &mut bus).unwrap();
assert_eq!(cpu.read_reg(Register::A7), sp_pre + 2);
assert_eq!(bus.read_word(TEST_SP + 2), 0x0020);
}
// Inside Macintosh Volume I (1985), p. I-126: SetResFileAttrs takes
// refNum + attrs and mutates the target file map attributes.
#[test]
fn setresfileattrs_consumes_refnum_and_attrs_arguments() {
let (mut disp, mut cpu, mut bus) = setup();
setup_resources(&mut disp, &mut bus, b"SRAF", 2, &[0xBB]);
bus.write_word(TEST_SP, 0x01A5u16);
bus.write_word(TEST_SP + 2, 0u16);
bus.write_word(TEST_SP + 4, 0xBEEF);
call(&mut disp, true, 0x1F7, &mut cpu, &mut bus).unwrap();
assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 4);
assert_eq!(bus.read_word(TEST_SP + 4), 0xBEEF);
assert_eq!(bus.read_word(0x0A60), 0);
assert_eq!(
disp.resources
.as_ref()
.unwrap()
.files
.get(&0)
.unwrap()
.map_attrs,
0x00A0
);
}
// Inside Macintosh Volume I (1985), p. I-126 lists only the
// mapReadOnly/mapCompact/mapChanged bits as public resource-file
// attributes; undefined bits are ignored by SetResFileAttrs and not
// surfaced by GetResFileAttrs.
#[test]
fn setresfileattrs_getresfileattrs_roundtrip_only_documented_bits() {
let (mut disp, mut cpu, mut bus) = setup();
setup_resources(&mut disp, &mut bus, b"MASK", 3, &[0xCC]);
bus.write_word(TEST_SP, 0xFFFFu16);
bus.write_word(TEST_SP + 2, 0u16);
call(&mut disp, true, 0x1F7, &mut cpu, &mut bus).unwrap();
cpu.write_reg(Register::A7, TEST_SP);
bus.write_word(TEST_SP, 0u16);
bus.write_word(TEST_SP + 2, 0xBEEFu16);
call(&mut disp, true, 0x1F6, &mut cpu, &mut bus).unwrap();
assert_eq!(
disp.resources
.as_ref()
.unwrap()
.files
.get(&0)
.unwrap()
.map_attrs,
0x00E0
);
assert_eq!(bus.read_word(TEST_SP + 2), 0x00E0);
assert_eq!(bus.read_word(0x0A60), 0);
}
// Inside Macintosh Volume I (1985), p. I-126: for unknown refNum,
// SetResFileAttrs does nothing and still returns noErr.
#[test]
fn setresfileattrs_missing_refnum_is_noop_with_noerr() {
let (mut disp, mut cpu, mut bus) = setup();
setup_resources(&mut disp, &mut bus, b"SRAF", 2, &[0xBB]);
disp.resources
.as_mut()
.unwrap()
.files
.get_mut(&0)
.unwrap()
.map_attrs = 0x0033;
bus.write_word(TEST_SP, 0x0F0Fu16);
bus.write_word(TEST_SP + 2, 77u16);
call(&mut disp, true, 0x1F7, &mut cpu, &mut bus).unwrap();
assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 4);
assert_eq!(bus.read_word(0x0A60), 0);
assert_eq!(
disp.resources
.as_ref()
.unwrap()
.files
.get(&0)
.unwrap()
.map_attrs,
0x0033
);
}
// Inside Macintosh Volume I (1985), p. I-125: WriteResource clears
// resChanged on successful writes for changed resources.
#[test]
fn writeresource_changed_resource_clears_reschanged_attribute() {
let (mut disp, mut cpu, mut bus) = setup();
let data_ptr = setup_resources(&mut disp, &mut bus, b"WRIT", 77, &[0x10, 0x20, 0x30]);
let handle = disp.get_or_create_resource_handle(&mut bus, *b"WRIT", 77, data_ptr);
bus.write_long(TEST_SP, handle);
call(&mut disp, true, 0x1AA, &mut cpu, &mut bus).unwrap(); // ChangedResource
assert_ne!(
disp.resource_attributes_for_handle(handle).unwrap_or(0)
& super::super::TrapDispatcher::RES_CHANGED_ATTR,
0
);
cpu.write_reg(Register::A7, TEST_SP);
bus.write_long(TEST_SP, handle);
call(&mut disp, true, 0x1B0, &mut cpu, &mut bus).unwrap();
assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 4);
assert_eq!(bus.read_word(0x0A60), 0);
assert_eq!(
disp.resource_attributes_for_handle(handle).unwrap_or(0)
& super::super::TrapDispatcher::RES_CHANGED_ATTR,
0
);
}
// Inside Macintosh Volume I (1985), p. I-125: WriteResource is a no-op
// with noErr for protected resources and for resources not marked changed.
#[test]
fn writeresource_protected_or_unchanged_resource_is_noop_with_noerr() {
let (mut disp, mut cpu, mut bus) = setup();
let changed = super::super::TrapDispatcher::RES_CHANGED_ATTR;
let prot_ptr = setup_resources(&mut disp, &mut bus, b"PROT", 1, &[0xAA; 4]);
let prot_handle = disp.get_or_create_resource_handle(&mut bus, *b"PROT", 1, prot_ptr);
bus.write_long(TEST_SP, prot_handle);
call(&mut disp, true, 0x1AA, &mut cpu, &mut bus).unwrap(); // ChangedResource
{
let attrs = disp
.resources
.as_mut()
.unwrap()
.files
.get_mut(&0)
.unwrap()
.attrs
.entry((*b"PROT", 1))
.or_insert(0);
*attrs |= 0x0008u8; // resProtected
}
cpu.write_reg(Register::A7, TEST_SP);
bus.write_long(TEST_SP, prot_handle);
call(&mut disp, true, 0x1B0, &mut cpu, &mut bus).unwrap();
assert_eq!(bus.read_word(0x0A60), 0);
assert_ne!(
disp.resource_attributes_for_handle(prot_handle)
.unwrap_or(0)
& changed,
0
);
let plain_ptr = setup_resources(&mut disp, &mut bus, b"PLAI", 2, &[0xBB; 4]);
let plain_handle = disp.get_or_create_resource_handle(&mut bus, *b"PLAI", 2, plain_ptr);
cpu.write_reg(Register::A7, TEST_SP);
bus.write_long(TEST_SP, plain_handle);
call(&mut disp, true, 0x1B0, &mut cpu, &mut bus).unwrap();
assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 4);
assert_eq!(bus.read_word(0x0A60), 0);
assert_eq!(
disp.resource_attributes_for_handle(plain_handle)
.unwrap_or(0)
& changed,
0
);
}
// Inside Macintosh Volume I (1985), p. I-125: non-resource handles cause
// WriteResource to do nothing and return resNotFound.
#[test]
fn writeresource_non_resource_handle_returns_resnotfound() {
let (mut disp, mut cpu, mut bus) = setup();
let fake_ptr = bus.alloc(8);
bus.write_bytes(fake_ptr, &[0xDE, 0xAD, 0xBE, 0xEF, 1, 2, 3, 4]);
let fake_handle = bus.alloc(4);
bus.write_long(fake_handle, fake_ptr);
bus.write_long(TEST_SP, fake_handle);
call(&mut disp, true, 0x1B0, &mut cpu, &mut bus).unwrap();
assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 4);
assert_eq!(bus.read_word(0x0A60) as i16, -192);
assert_eq!(bus.read_long(fake_handle), fake_ptr);
}
// ================================================================
// 8. CurResFile (0x194)
// ================================================================
#[test]
fn cur_res_file() {
let (mut disp, mut cpu, mut bus) = setup();
call(&mut disp, true, 0x194, &mut cpu, &mut bus).unwrap();
let sp = cpu.read_reg(Register::A7);
assert_eq!(sp, TEST_SP, "SP should be unchanged");
let refnum = bus.read_word(sp);
assert_eq!(refnum, 0);
}
// ================================================================
// 9. HomeResFile (0x1A4)
// ================================================================
#[test]
fn home_res_file_returns_loaded_resource_refnum() {
let (mut disp, mut cpu, mut bus) = setup();
let handle = 0x1234u32;
disp.loaded_handles.insert(handle, (0x200000, *b"STR ", 1));
disp.resource_handle_files.insert(handle, 128);
let sp = TEST_SP;
bus.write_long(sp, handle);
call(&mut disp, true, 0x1A4, &mut cpu, &mut bus).unwrap();
let new_sp = cpu.read_reg(Register::A7);
assert_eq!(new_sp, TEST_SP + 4);
let refnum = bus.read_word(new_sp);
assert_eq!(refnum, 128);
assert_eq!(bus.read_word(0x0A60), 0);
}
#[test]
fn home_res_file_returns_minus_one_for_detached_handle() {
let (mut disp, mut cpu, mut bus) = setup();
let handle = 0x5678u32;
disp.detached_handles.insert(handle, (*b"STR ", 2));
disp.detached_handle_files.insert(handle, 202);
let sp = TEST_SP;
bus.write_long(sp, handle);
call(&mut disp, true, 0x1A4, &mut cpu, &mut bus).unwrap();
let new_sp = cpu.read_reg(Register::A7);
assert_eq!(new_sp, TEST_SP + 4);
let refnum = bus.read_word(new_sp) as i16;
assert_eq!(refnum, -1);
assert_eq!(bus.read_word(0x0A60) as i16, -192);
}
#[test]
fn home_res_file_returns_minus_one_for_unknown_handle() {
let (mut disp, mut cpu, mut bus) = setup();
let sp = TEST_SP;
bus.write_long(sp, 0x9ABCu32);
call(&mut disp, true, 0x1A4, &mut cpu, &mut bus).unwrap();
let new_sp = cpu.read_reg(Register::A7);
assert_eq!(new_sp, TEST_SP + 4);
let refnum = bus.read_word(new_sp) as i16;
assert_eq!(refnum, -1);
assert_eq!(bus.read_word(0x0A60) as i16, -192);
}
// ================================================================
// 10. LoadSeg (0x1F0)
// ================================================================
#[test]
fn loadseg_patches_calling_jump_table_entry_to_jmp_loaded_code() {
// Inside Macintosh Volume II (1985), p. II-61: LoadSeg rewrites an
// unloaded jump-table entry to the loaded JMP form.
//
// Use the documented MPW entry layout:
// [offset, MOVE.W #segNum,-(SP), segNum, _LoadSeg]
// so the trap can consume segNum from A7 and patch the entry.
let (mut disp, mut cpu, mut bus) = setup();
// Set up segment 1 at address 0x210000
let seg_addr = 0x210000u32;
// Write a non-0xFFFF header word so header_size = 4
// Segment header: taboff=0, nentries=0 (no JT entries to patch)
bus.write_word(seg_addr, 0x0000); // taboff
bus.write_word(seg_addr + 2, 0x0000); // nentries
let mut seg_map = HashMap::new();
seg_map.insert(1i16, seg_addr);
disp.register_segments(seg_map);
// Set up a standard MPW jump table entry at 0x200000:
// +0: 0000 routine offset within segment
// +2: 3F3C MOVE.W #imm, -(SP)
// +4: 0001 segment number
// +6: A9F0 _LoadSeg trap
// After trap executes, the entry is patched to:
// +0: seg#
// +2: 4EF9 JMP.L
// +4: code_addr
// Inside Macintosh Volume II, II-61
let island_addr = 0x200000u32;
bus.write_word(island_addr, 0x0000); // routine offset
bus.write_word(island_addr + 2, 0x3F3C); // MOVE.W
bus.write_word(island_addr + 4, 0x0001); // seg#
bus.write_word(island_addr + 6, 0xA9F0); // _LoadSeg
// PC points past the A9F0 instruction (as if CPU just executed it)
cpu.write_reg(Register::PC, island_addr + 8);
// Standard format: segment number is pushed on stack by MOVE.W
let sp = TEST_SP;
bus.write_word(sp, 1u16); // segment number
call(&mut disp, true, 0x1F0, &mut cpu, &mut bus).unwrap();
// Standard MPW format pops the segment number pushed by MOVE.W.
let sp_after = cpu.read_reg(Register::A7);
assert_eq!(sp_after, TEST_SP + 2);
// Handler patches the island and sets PC to island+2 (the JMP.L instruction).
// The JMP.L target is seg_addr + header_size(4) + routine_offset(0) = seg_addr + 4.
let pc = cpu.read_reg(Register::PC);
assert_eq!(pc, island_addr + 2, "PC should point to patched JMP.L");
// Verify the island was patched correctly
assert_eq!(
bus.read_word(island_addr),
1,
"island[0] should be seg number"
);
assert_eq!(
bus.read_word(island_addr + 2),
0x4EF9,
"island[2] should be JMP.L"
);
assert_eq!(
bus.read_long(island_addr + 4),
seg_addr + 4,
"JMP target should be code entry"
);
}
#[test]
fn loadseg_mpw_call_consumes_segment_number_word_argument() {
// Inside Macintosh Volume II (1985), p. II-60:
// MPW jump-table callers push segNum with MOVE.W #segNum, -(SP)
// before executing _LoadSeg.
let (mut disp, mut cpu, mut bus) = setup();
let seg_addr = 0x220000u32;
bus.write_word(seg_addr, 0x0000);
bus.write_word(seg_addr + 2, 0x0000);
disp.register_segments(HashMap::from([(1i16, seg_addr)]));
let island_addr = 0x210000u32;
bus.write_word(island_addr, 0x0000);
bus.write_word(island_addr + 2, 0x3F3C);
bus.write_word(island_addr + 4, 0x0001);
bus.write_word(island_addr + 6, 0xA9F0);
cpu.write_reg(Register::PC, island_addr + 8);
bus.write_word(TEST_SP, 1u16);
bus.write_word(TEST_SP + 2, 0xBEEF);
call(&mut disp, true, 0x1F0, &mut cpu, &mut bus).unwrap();
assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 2);
assert_eq!(bus.read_word(TEST_SP + 2), 0xBEEF);
}
#[test]
fn loadseg_patches_all_entries_for_loaded_segment() {
// Inside Macintosh Volume II (1985), pp. II-60 to II-61:
// LoadSeg patches every unloaded jump-table entry for the segment
// so future cross-segment calls can run directly.
let (mut disp, mut cpu, mut bus) = setup();
let seg_addr = 0x230000u32;
bus.write_word(seg_addr, 0x0000);
bus.write_word(seg_addr + 2, 0x0002);
disp.register_segments(HashMap::from([(1i16, seg_addr)]));
let island_addr = 0x240000u32;
bus.write_word(island_addr, 0x0000); // routine offset 0
bus.write_word(island_addr + 2, 0x3F3C);
bus.write_word(island_addr + 4, 0x0001);
bus.write_word(island_addr + 6, 0xA9F0);
bus.write_word(island_addr + 8, 0x0004); // routine offset 4
bus.write_word(island_addr + 10, 0x3F3C);
bus.write_word(island_addr + 12, 0x0001);
bus.write_word(island_addr + 14, 0xA9F0);
cpu.write_reg(Register::PC, island_addr + 8);
cpu.write_reg(Register::A5, island_addr);
bus.write_word(TEST_SP, 1u16);
call(&mut disp, true, 0x1F0, &mut cpu, &mut bus).unwrap();
assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 2);
assert_eq!(bus.read_word(island_addr), 1);
assert_eq!(bus.read_word(island_addr + 2), 0x4EF9);
assert_eq!(bus.read_long(island_addr + 4), seg_addr + 4);
assert_eq!(bus.read_word(island_addr + 8), 1);
assert_eq!(bus.read_word(island_addr + 10), 0x4EF9);
assert_eq!(bus.read_long(island_addr + 12), seg_addr + 8);
}
// ================================================================
// 11. GetResAttrs (0x1A6)
// ================================================================
// Inside Macintosh Volume I (1985), p. I-121: GetResAttrs returns
// the map attribute word for a live resource handle.
#[test]
fn get_res_attrs_returns_attribute_bits_for_live_resource_handle() {
let (mut disp, mut cpu, mut bus) = setup();
let data_ptr = setup_resources(&mut disp, &mut bus, b"ATTR", 42, &[0x11, 0x22, 0x33]);
let handle = disp.get_or_create_resource_handle(&mut bus, *b"ATTR", 42, data_ptr);
disp.resources
.as_mut()
.unwrap()
.files
.get_mut(&0)
.unwrap()
.attrs
.insert((*b"ATTR", 42), 0x0026u8);
let sp = TEST_SP;
bus.write_long(sp, handle);
bus.write_word(sp + 4, 0xBEEF);
call(&mut disp, true, 0x1A6, &mut cpu, &mut bus).unwrap();
assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 4);
assert_eq!(bus.read_word(TEST_SP + 4), 0x0026);
assert_eq!(bus.read_word(0x0A60), 0);
}
// BasiliskII returns resChanged in the result slot and sets ResError to
// resNotFound when the handle is not a resource handle.
#[test]
fn get_res_attrs_returns_res_changed_for_unknown_handle() {
let (mut disp, mut cpu, mut bus) = setup();
let sp = TEST_SP;
bus.write_long(sp, 0x00DEAD00);
bus.write_word(sp + 4, 0xBEEF);
call(&mut disp, true, 0x1A6, &mut cpu, &mut bus).unwrap();
assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 4);
assert_eq!(bus.read_word(TEST_SP + 4), 0x0002);
assert_eq!(bus.read_word(0x0A60) as i16, -192);
}
// Inside Macintosh Volume I (1985), p. I-121 and p. I-123:
// GetResAttrs observes the resChanged bit set by AddResource on a live
// resource handle.
#[test]
fn get_res_attrs_after_add_resource_returns_res_changed() {
let (mut disp, mut cpu, mut bus) = setup();
let data_ptr = bus.alloc(3);
bus.write_bytes(data_ptr, &[0x11, 0x22, 0x33]);
let handle = bus.alloc(4);
bus.write_long(handle, data_ptr);
disp.resources = Some(LoadedResources {
files: HashMap::from([(0, ResourceFileMap::default())]),
names: HashMap::new(),
search_order: vec![0],
current_file: 0,
});
bus.write_long(TEST_SP, 0);
bus.write_word(TEST_SP + 4, 43u16);
bus.write_long(TEST_SP + 6, u32::from_be_bytes(*b"ATTR"));
bus.write_long(TEST_SP + 10, handle);
call(&mut disp, true, 0x1AB, &mut cpu, &mut bus).unwrap(); // AddResource
assert_eq!(bus.read_word(0x0A60), 0);
cpu.write_reg(Register::A7, TEST_SP);
bus.write_long(TEST_SP, handle);
bus.write_word(TEST_SP + 4, 0xBEEF);
call(&mut disp, true, 0x1A6, &mut cpu, &mut bus).unwrap();
assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 4);
assert_eq!(bus.read_word(TEST_SP + 4), 0x0002);
assert_eq!(bus.read_word(0x0A60), 0);
}
// Inside Macintosh Volume IV (1986), p. IV-16 / More Macintosh Toolbox
// 1993, p. 1-120: RsrcMapEntry returns the resource-reference offset from
// the start of the resource map for live resource handles.
#[test]
#[ignore]
fn rsrcmapentry_returns_reference_offset_for_live_resource_handle() {
let (mut disp, mut cpu, mut bus) = setup();
let rsrc_bytes = make_single_resource_fork_bytes(*b"TEST", 43, &[0x11, 0x22]);
disp.vfs_rsrc
.insert("RsrcMapEntryTest".to_string(), rsrc_bytes);
disp.open_resource_file_from_vfs_key(&mut bus, "RsrcMapEntryTest", false);
let handle = disp
.loaded_handles
.iter()
.find_map(|(handle, (_, res_type, res_id))| {
(*res_type == *b"TEST" && *res_id == 43).then_some(*handle)
})
.expect("resource handle should be loaded");
bus.write_long(TEST_SP, handle);
bus.write_long(TEST_SP + 4, 0xBEEF_BEEF);
call(&mut disp, true, 0x1C5, &mut cpu, &mut bus).unwrap();
assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 4);
assert_eq!(bus.read_long(TEST_SP + 4), 40);
assert_eq!(bus.read_word(0x0A60), 0);
}
// Nil handles are tolerated by leaving the result slot untouched while
// setting ResErr to resNotFound.
#[test]
#[ignore]
fn rsrcmapentry_nil_handle_sets_resnotfound_and_leaves_result_slot() {
let (mut disp, mut cpu, mut bus) = setup();
bus.write_long(TEST_SP, 0);
bus.write_long(TEST_SP + 4, 0xBEEF_BEEF);
call(&mut disp, true, 0x1C5, &mut cpu, &mut bus).unwrap();
assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 4);
assert_eq!(bus.read_long(TEST_SP + 4), 0xBEEF_BEEF);
assert_eq!(bus.read_word(0x0A60) as i16, -192);
}
// Inside Macintosh Volume IV (1986), p. IV-16 / More Macintosh Toolbox
// 1993, p. 1-120: non-resource handles report resNotFound and leave the
// caller's result slot unchanged.
#[test]
#[ignore]
fn rsrcmapentry_unknown_handle_sets_resnotfound_and_leaves_result_slot() {
let (mut disp, mut cpu, mut bus) = setup();
bus.write_long(TEST_SP, 0x00DE_AD00);
bus.write_long(TEST_SP + 4, 0xBEEF_BEEF);
call(&mut disp, true, 0x1C5, &mut cpu, &mut bus).unwrap();
assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 4);
assert_eq!(bus.read_long(TEST_SP + 4), 0xBEEF_BEEF);
assert_eq!(bus.read_word(0x0A60) as i16, -192);
}
// ================================================================
// 12. ChangedResource (0x1AA)
// ================================================================
// Inside Macintosh Volume I (1985), p. I-123: ChangedResource marks
// a resource as changed so UpdateResFile/WriteResource will flush it.
#[test]
fn changedresource_sets_reschanged_attribute_for_unprotected_resource() {
let (mut disp, mut cpu, mut bus) = setup();
let data_ptr = setup_resources(&mut disp, &mut bus, b"CHNG", 9, &[0xAA, 0xBB]);
let handle = disp.get_or_create_resource_handle(&mut bus, *b"CHNG", 9, data_ptr);
bus.write_long(TEST_SP, handle);
bus.write_word(0x0A60, 0xBEEF);
call(&mut disp, true, 0x1AA, &mut cpu, &mut bus).unwrap();
assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 4);
assert_eq!(bus.read_word(0x0A60), 0);
assert_ne!(
disp.resource_attributes_for_handle(handle).unwrap_or(0)
& u16::from(super::super::TrapDispatcher::RES_CHANGED_ATTR),
0
);
}
// Inside Macintosh Volume IV (1986), p. IV-16: ChangedResource does not
// modify protected resources (resProtected bit set) but returns resAttrErr.
#[test]
fn changedresource_protected_resource_is_noop_with_resattrerr() {
let (mut disp, mut cpu, mut bus) = setup();
let data_ptr = setup_resources(&mut disp, &mut bus, b"PROT", 3, &[0x10, 0x20]);
let handle = disp.get_or_create_resource_handle(&mut bus, *b"PROT", 3, data_ptr);
disp.resources
.as_mut()
.unwrap()
.files
.get_mut(&0)
.unwrap()
.attrs
.insert((*b"PROT", 3), 0x0008u8);
bus.write_long(TEST_SP, handle);
bus.write_word(0x0A60, 0xBEEF);
call(&mut disp, true, 0x1AA, &mut cpu, &mut bus).unwrap();
let attrs_after = disp.resource_attributes_for_handle(handle).unwrap_or(0);
assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 4);
assert_eq!(
bus.read_word(0x0A60) as i16,
super::super::TrapDispatcher::RES_ATTR_ERR
);
assert_eq!(
attrs_after & u16::from(super::super::TrapDispatcher::RES_CHANGED_ATTR),
0
);
assert_ne!(attrs_after & 0x0008, 0);
}
// ================================================================
// 13a. HighLevelFSDispatch (0x252) selector 1 — FSMakeFSSpec
// ================================================================
#[test]
fn hlfs_dispatch_fsmakefsspec() {
let (mut disp, mut cpu, mut bus) = setup();
let spec_ptr = 0x300000u32;
let name_ptr = 0x300100u32;
write_pstring(&mut bus, name_ptr, b"MyFile.txt");
let sp = TEST_SP;
bus.write_long(sp, spec_ptr); // spec_ptr
bus.write_long(sp + 4, name_ptr); // name_ptr
bus.write_long(sp + 8, 2); // dirID
bus.write_word(sp + 12, 1); // vRefNum
cpu.write_reg(Register::D0, 1); // selector = FSMakeFSSpec
call(&mut disp, true, 0x252, &mut cpu, &mut bus).unwrap();
let new_sp = cpu.read_reg(Register::A7);
assert_eq!(new_sp, TEST_SP + 14);
// Check FSSpec was filled: vRefNum at spec+0, dirID at spec+2, name at spec+6
assert_eq!(
bus.read_word(spec_ptr),
super::super::dispatch::BOOT_VOLUME_REF_NUM as u16
);
assert_eq!(bus.read_long(spec_ptr + 2), 2);
assert_eq!(bus.read_byte(spec_ptr + 6), 10); // length of "MyFile.txt"
// File doesn't exist in VFS — result should be fnfErr (-43)
// Files 1992, 2-166
assert_eq!(bus.read_word(new_sp) as i16, -43);
}
// ================================================================
// 13a2. FSMakeFSSpec returns noErr for existing file
// ================================================================
#[test]
fn hlfs_dispatch_fsmakefsspec_exists() {
let (mut disp, mut cpu, mut bus) = setup();
disp.vfs.insert("MyFile.txt".to_string(), vec![]);
let spec_ptr = 0x300000u32;
let name_ptr = 0x300100u32;
write_pstring(&mut bus, name_ptr, b"MyFile.txt");
let sp = TEST_SP;
bus.write_long(sp, spec_ptr);
bus.write_long(sp + 4, name_ptr);
bus.write_long(sp + 8, 2);
bus.write_word(sp + 12, 1);
cpu.write_reg(Register::D0, 1);
call(&mut disp, true, 0x252, &mut cpu, &mut bus).unwrap();
let new_sp = cpu.read_reg(Register::A7);
assert_eq!(new_sp, TEST_SP + 14);
assert_eq!(bus.read_word(new_sp), 0); // noErr
}
// ================================================================
// 13a3. FSMakeFSSpec returns noErr for existing directory
// ================================================================
#[test]
fn hlfs_dispatch_fsmakefsspec_dir() {
let (mut disp, mut cpu, mut bus) = setup();
// Add a file with directory component
disp.vfs.insert("MyDir/SomeFile.txt".to_string(), vec![]);
let spec_ptr = 0x300000u32;
let name_ptr = 0x300100u32;
write_pstring(&mut bus, name_ptr, b":MyDir");
let sp = TEST_SP;
bus.write_long(sp, spec_ptr);
bus.write_long(sp + 4, name_ptr);
bus.write_long(sp + 8, 2);
bus.write_word(sp + 12, 1);
cpu.write_reg(Register::D0, 1);
call(&mut disp, true, 0x252, &mut cpu, &mut bus).unwrap();
let new_sp = cpu.read_reg(Register::A7);
assert_eq!(new_sp, TEST_SP + 14);
assert_eq!(bus.read_word(new_sp), 0); // noErr — directory exists
}
// ================================================================
// 13b. HighLevelFSDispatch (0x252) selector 2 — FSpOpenDF
// ================================================================
#[test]
fn hlfs_dispatch_fspopendf() {
let (mut disp, mut cpu, mut bus) = setup();
// Add file to VFS so FSpOpenDF can find it
disp.vfs.insert("OpenMe.txt".to_string(), vec![1, 2, 3]);
let spec_ptr = 0x300000u32;
write_fsspec(&mut bus, spec_ptr, 1, 2, b"OpenMe.txt");
let ref_num_ptr = 0x300200u32;
let sp = TEST_SP;
bus.write_long(sp, ref_num_ptr); // refNumPtr
bus.write_word(sp + 4, 1); // permission (fsRdPerm)
bus.write_long(sp + 6, spec_ptr); // spec_ptr
cpu.write_reg(Register::D0, 2); // selector = FSpOpenDF
call(&mut disp, true, 0x252, &mut cpu, &mut bus).unwrap();
let new_sp = cpu.read_reg(Register::A7);
assert_eq!(new_sp, TEST_SP + 10);
// A refnum should have been written
let refnum = bus.read_word(ref_num_ptr);
assert!(
refnum >= 100,
"refnum should be >= 100 (initial next_refnum)"
);
// Result at new SP should be 0
assert_eq!(bus.read_word(new_sp), 0);
}
// ================================================================
// 13b2. FSpOpenDF returns fnfErr for missing file
// ================================================================
#[test]
fn hlfs_dispatch_fspopendf_fnferr() {
let (mut disp, mut cpu, mut bus) = setup();
let spec_ptr = 0x300000u32;
write_fsspec(&mut bus, spec_ptr, 1, 2, b"Missing.txt");
let ref_num_ptr = 0x300200u32;
let sp = TEST_SP;
bus.write_long(sp, ref_num_ptr);
bus.write_word(sp + 4, 1);
bus.write_long(sp + 6, spec_ptr);
cpu.write_reg(Register::D0, 2);
call(&mut disp, true, 0x252, &mut cpu, &mut bus).unwrap();
let new_sp = cpu.read_reg(Register::A7);
assert_eq!(new_sp, TEST_SP + 10);
// Result should be fnfErr (-43)
assert_eq!(bus.read_word(new_sp) as i16, -43);
}
// ================================================================
// 13b3. HighLevelFSDispatch (0x252) selector 7 — FSpGetFInfo (file)
// ================================================================
#[test]
fn hlfs_dispatch_fspgetfinfo_file() {
let (mut disp, mut cpu, mut bus) = setup();
disp.vfs.insert("InfoFile.bin".to_string(), vec![0xAA]);
disp.set_vfs_entry_finfo(
"InfoFile.bin",
u32::from_be_bytes(*b"APPL"),
u32::from_be_bytes(*b"RSED"),
0x1234,
);
let spec_ptr = 0x300000u32;
let finfo_ptr = 0x300100u32;
write_fsspec(&mut bus, spec_ptr, 1, 2, b"InfoFile.bin");
let sp = TEST_SP;
bus.write_long(sp, finfo_ptr); // fndrInfo pointer
bus.write_long(sp + 4, spec_ptr); // spec pointer
cpu.write_reg(Register::D0, 7);
call(&mut disp, true, 0x252, &mut cpu, &mut bus).unwrap();
let new_sp = cpu.read_reg(Register::A7);
assert_eq!(new_sp, TEST_SP + 8);
assert_eq!(bus.read_word(new_sp), 0); // noErr
assert_eq!(bus.read_long(finfo_ptr), u32::from_be_bytes(*b"APPL"));
assert_eq!(bus.read_long(finfo_ptr + 4), u32::from_be_bytes(*b"RSED"));
assert_eq!(bus.read_word(finfo_ptr + 8), 0x1234);
}
// ================================================================
// 13b4. HighLevelFSDispatch (0x252) selector 7 — FSpGetFInfo (directory)
// ================================================================
#[test]
fn hlfs_dispatch_fspgetfinfo_directory() {
let (mut disp, mut cpu, mut bus) = setup();
// Seed a child file so the parent directory exists in the VFS tree.
disp.vfs
.insert("EV Override 1.0.1/EV Override".to_string(), vec![0x00]);
let spec_ptr = 0x300000u32;
let finfo_ptr = 0x300100u32;
write_fsspec(&mut bus, spec_ptr, 1, 2, b"EV Override 1.0.1");
let sp = TEST_SP;
bus.write_long(sp, finfo_ptr); // fndrInfo pointer
bus.write_long(sp + 4, spec_ptr); // spec pointer
cpu.write_reg(Register::D0, 7);
call(&mut disp, true, 0x252, &mut cpu, &mut bus).unwrap();
let new_sp = cpu.read_reg(Register::A7);
assert_eq!(new_sp, TEST_SP + 8);
assert_eq!(bus.read_word(new_sp), 0); // noErr
assert_eq!(bus.read_long(finfo_ptr), u32::from_be_bytes(*b"fold"));
assert_eq!(bus.read_long(finfo_ptr + 4), u32::from_be_bytes(*b"MACS"));
}
// ================================================================
// 13c. HighLevelFSDispatch (0x252) selector 4 — FSpCreate
// ================================================================
#[test]
fn hlfs_dispatch_fspcreate() {
let (mut disp, mut cpu, mut bus) = setup();
let spec_ptr = 0x300000u32;
write_fsspec(&mut bus, spec_ptr, 1, 2, b"NewFile.dat");
let sp = TEST_SP;
// FSpCreate stack: SP+0..SP+9 = other params, SP+10 = spec_ptr
// Zero out the lower bytes
for i in 0..10u32 {
bus.write_byte(sp + i, 0);
}
bus.write_long(sp + 10, spec_ptr);
cpu.write_reg(Register::D0, 4); // selector = FSpCreate
call(&mut disp, true, 0x252, &mut cpu, &mut bus).unwrap();
let new_sp = cpu.read_reg(Register::A7);
assert_eq!(new_sp, TEST_SP + 14);
assert!(
disp.vfs.contains_key("NewFile.dat"),
"file should be added to VFS"
);
}
// ================================================================
// 13d. HighLevelFSDispatch (0x252) selector 14/13 — FSpCreateResFile/FSpOpenResFile
// ================================================================
#[test]
fn hlfs_dispatch_fspcreateresfile_and_openresfile() {
let (mut disp, mut cpu, mut bus) = setup();
let spec_ptr = 0x300000u32;
write_fsspec(&mut bus, spec_ptr, 1, 2, b"Escape Velocity Prefs");
// FSpCreateResFile stack: [scriptTag(2)] [fileType(4)] [creator(4)] [spec_ptr(4)]
let sp = TEST_SP;
bus.write_word(sp, 0);
bus.write_long(sp + 2, 0);
bus.write_long(sp + 6, 0);
bus.write_long(sp + 10, spec_ptr);
cpu.write_reg(Register::A7, sp);
cpu.write_reg(Register::D0, 14);
call(&mut disp, true, 0x252, &mut cpu, &mut bus).unwrap();
assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 14);
assert!(disp.vfs.contains_key("Escape Velocity Prefs"));
assert!(disp.vfs_rsrc.contains_key("Escape Velocity Prefs"));
assert_eq!(bus.read_word(0x0A60), 0);
// FSpOpenResFile stack: [permission(2)] [spec_ptr(4)] [result(2)]
bus.write_word(sp, 1);
bus.write_long(sp + 2, spec_ptr);
cpu.write_reg(Register::A7, sp);
cpu.write_reg(Register::D0, 13);
call(&mut disp, true, 0x252, &mut cpu, &mut bus).unwrap();
let new_sp = cpu.read_reg(Register::A7);
assert_eq!(new_sp, TEST_SP + 6);
let refnum = bus.read_word(new_sp);
assert!(refnum >= 100);
assert_eq!(cpu.read_reg(Register::D0), refnum as u32);
assert_eq!(bus.read_word(0x0A60), 0);
assert_eq!(bus.read_word(0x0A5A), refnum);
}
#[test]
fn hlfs_dispatch_fspopenresfile_dedup_keeps_current_resource_file() {
// More Macintosh Toolbox 1993, p. 1-63: reopening an already-open
// resource fork returns the existing refnum without making that file
// current. This regression catches the dedup path mutating CurResFile.
let (mut disp, mut cpu, mut bus) = setup();
disp.vfs_rsrc.insert("First".to_string(), vec![]);
disp.vfs_rsrc.insert("Second".to_string(), vec![]);
let first_spec = 0x300080u32;
let second_spec = 0x3000A0u32;
write_fsspec(&mut bus, first_spec, 1, 2, b"First");
write_fsspec(&mut bus, second_spec, 1, 2, b"Second");
let sp = TEST_SP;
bus.write_word(sp, 1);
bus.write_long(sp + 2, first_spec);
bus.write_word(sp + 6, 0xBEEF);
cpu.write_reg(Register::A7, sp);
cpu.write_reg(Register::D0, 13);
call(&mut disp, true, 0x252, &mut cpu, &mut bus).unwrap();
let first_ref = bus.read_word(TEST_SP + 6);
assert_eq!(bus.read_word(0x0A60), 0);
assert_eq!(bus.read_word(0x0A5A), first_ref);
bus.write_word(sp, 1);
bus.write_long(sp + 2, second_spec);
bus.write_word(sp + 6, 0xBEEF);
cpu.write_reg(Register::A7, sp);
cpu.write_reg(Register::D0, 13);
call(&mut disp, true, 0x252, &mut cpu, &mut bus).unwrap();
let second_ref = bus.read_word(TEST_SP + 6);
assert_ne!(second_ref, first_ref);
assert_eq!(bus.read_word(0x0A60), 0);
assert_eq!(bus.read_word(0x0A5A), second_ref);
bus.write_word(sp, 1);
bus.write_long(sp + 2, first_spec);
bus.write_word(sp + 6, 0xBEEF);
cpu.write_reg(Register::A7, sp);
cpu.write_reg(Register::D0, 13);
call(&mut disp, true, 0x252, &mut cpu, &mut bus).unwrap();
assert_eq!(bus.read_word(TEST_SP + 6), first_ref);
assert_eq!(bus.read_word(0x0A60), 0);
assert_eq!(bus.read_word(0x0A5A), second_ref);
}
#[test]
fn openresfile_dedup_keeps_current_resource_file() {
// IM:Volume I p. I-115: OpenResFile reuses an already-open resource
// fork without switching the current resource file.
let (mut disp, mut cpu, mut bus) = setup();
disp.vfs_rsrc.insert("First".to_string(), vec![]);
disp.vfs_rsrc.insert("Second".to_string(), vec![]);
let first_name = 0x3000C0u32;
let second_name = 0x3000E0u32;
write_pstring(&mut bus, first_name, b"First");
write_pstring(&mut bus, second_name, b"Second");
let sp = TEST_SP;
bus.write_long(sp, first_name);
bus.write_word(sp + 4, 0xBEEF);
cpu.write_reg(Register::A7, sp);
call_trap_word(&mut disp, 0xA997, &mut cpu, &mut bus).unwrap();
let first_ref = bus.read_word(TEST_SP + 4);
assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 4);
assert_eq!(bus.read_word(0x0A60), 0);
assert_eq!(bus.read_word(0x0A5A), first_ref);
bus.write_long(sp, second_name);
bus.write_word(sp + 4, 0xBEEF);
cpu.write_reg(Register::A7, sp);
call_trap_word(&mut disp, 0xA997, &mut cpu, &mut bus).unwrap();
let second_ref = bus.read_word(TEST_SP + 4);
assert_ne!(second_ref, first_ref);
assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 4);
assert_eq!(bus.read_word(0x0A60), 0);
assert_eq!(bus.read_word(0x0A5A), second_ref);
bus.write_long(sp, first_name);
bus.write_word(sp + 4, 0xBEEF);
cpu.write_reg(Register::A7, sp);
call_trap_word(&mut disp, 0xA997, &mut cpu, &mut bus).unwrap();
assert_eq!(bus.read_word(TEST_SP + 4), first_ref);
assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 4);
assert_eq!(bus.read_word(0x0A60), 0);
assert_eq!(bus.read_word(0x0A5A), second_ref);
}
#[test]
fn hlfs_dispatch_fspopenresfile_data_only_file_returns_resfnotfound() {
// More Macintosh Toolbox 1993, p. 1-58: FSpOpenResFile opens an
// existing resource fork. If the data fork exists but no resource fork
// has been created, Systemless should not synthesize one on open; the
// current selector contract reports failure via -1 plus resFNotFound.
let (mut disp, mut cpu, mut bus) = setup();
disp.vfs.insert("DataOnly".to_string(), b"DATA".to_vec());
let spec_ptr = 0x300040u32;
write_fsspec(&mut bus, spec_ptr, 0, 0, b"DataOnly");
let sp = TEST_SP;
bus.write_word(sp, 1); // fsRdPerm
bus.write_long(sp + 2, spec_ptr);
bus.write_word(sp + 6, 0xBEEF);
cpu.write_reg(Register::A7, sp);
cpu.write_reg(Register::D0, 13);
call(&mut disp, true, 0x252, &mut cpu, &mut bus).unwrap();
assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 6);
assert_eq!(bus.read_word(TEST_SP + 6), (-1i16) as u16);
assert_eq!(cpu.read_reg(Register::D0), (-1i32) as u32);
assert_eq!(bus.read_word(0x0A60), (-193i16) as u16);
assert!(
!disp.vfs_rsrc.contains_key("DataOnly"),
"missing resource fork should not be synthesized by FSpOpenResFile"
);
}
#[test]
fn hlfs_dispatch_fspopenresfile_missing_file_returns_fnferr() {
// IM:Volume VI 13-20: FSpOpenResFile shares HOpenResFile result
// codes, including fnfErr (-43) when the file itself is missing.
let (mut disp, mut cpu, mut bus) = setup();
let spec_ptr = 0x300060u32;
write_fsspec(&mut bus, spec_ptr, 0, 0, b"TotallyMissing");
let sp = TEST_SP;
bus.write_word(sp, 1); // fsRdPerm
bus.write_long(sp + 2, spec_ptr);
bus.write_word(sp + 6, 0xBEEF);
cpu.write_reg(Register::A7, sp);
cpu.write_reg(Register::D0, 13);
call(&mut disp, true, 0x252, &mut cpu, &mut bus).unwrap();
assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 6);
assert_eq!(bus.read_word(TEST_SP + 6), (-1i16) as u16);
assert_eq!(cpu.read_reg(Register::D0), (-1i32) as u32);
assert_eq!(bus.read_word(0x0A60), (-43i16) as u16);
}
// ================================================================
// 13e. HighLevelFSDispatch (0x252) selector 6 — FSpDelete
// ================================================================
#[test]
fn hlfs_dispatch_fspdelete() {
let (mut disp, mut cpu, mut bus) = setup();
// Pre-populate VFS with the file
disp.vfs.insert("DelMe.txt".to_string(), vec![1, 2, 3]);
disp.vfs_rsrc.insert("DelMe.txt".to_string(), vec![4, 5, 6]);
let spec_ptr = 0x300000u32;
write_fsspec(&mut bus, spec_ptr, 1, 2, b"DelMe.txt");
let sp = TEST_SP;
bus.write_long(sp, spec_ptr);
cpu.write_reg(Register::D0, 6); // selector = FSpDelete
call(&mut disp, true, 0x252, &mut cpu, &mut bus).unwrap();
let new_sp = cpu.read_reg(Register::A7);
assert_eq!(new_sp, TEST_SP + 4);
assert!(
!disp.vfs.contains_key("DelMe.txt"),
"file should be removed from VFS"
);
assert!(
!disp.vfs_rsrc.contains_key("DelMe.txt"),
"resource fork should be removed from VFS"
);
}
// ================================================================
// 14a. Gestalt (0xAD) — sysv
// ================================================================
#[test]
fn gestalt_sysv() {
let (mut disp, mut cpu, mut bus) = setup();
cpu.write_reg(Register::D0, u32::from_be_bytes(*b"sysv"));
call(&mut disp, false, 0xAD, &mut cpu, &mut bus).unwrap();
assert_eq!(
cpu.read_reg(Register::A0),
crate::machine_profile::ORACLE_MACHINE_PROFILE.system_version_bcd as u32
);
assert_eq!(cpu.read_reg(Register::D0), 0);
}
// ================================================================
// 14b. Gestalt (0xAD) — cput
// Per IM:Operating System Utilities 1994 line 1439 / 2299:
// gestaltCPU68040 = $004 under the 'cput' selector.
// ================================================================
#[test]
fn gestalt_cput() {
let (mut disp, mut cpu, mut bus) = setup();
cpu.write_reg(Register::D0, u32::from_be_bytes(*b"cput"));
call(&mut disp, false, 0xAD, &mut cpu, &mut bus).unwrap();
assert_eq!(cpu.read_reg(Register::A0), 4);
assert_eq!(cpu.read_reg(Register::D0), 0);
}
#[test]
fn gestalt_display_manager_selectors_match_basilisk753() {
let (mut disp, mut cpu, mut bus) = setup();
cpu.write_reg(Register::D0, u32::from_be_bytes(*b"dplv"));
call(&mut disp, false, 0xAD, &mut cpu, &mut bus).unwrap();
assert_eq!(cpu.read_reg(Register::A0), 0x0002_0006);
assert_eq!(cpu.read_reg(Register::D0), 0);
cpu.write_reg(Register::D0, u32::from_be_bytes(*b"dply"));
call(&mut disp, false, 0xAD, &mut cpu, &mut bus).unwrap();
assert_eq!(cpu.read_reg(Register::A0), 0x0000_0007);
assert_eq!(cpu.read_reg(Register::D0), 0);
}
// ================================================================
// 14c. Gestalt (0xAD) — unknown selector
// ================================================================
#[test]
fn gestalt_unknown() {
let (mut disp, mut cpu, mut bus) = setup();
cpu.write_reg(Register::D0, u32::from_be_bytes(*b"zzzz"));
call(&mut disp, false, 0xAD, &mut cpu, &mut bus).unwrap();
assert_eq!(cpu.read_reg(Register::D0), 0xFFFFEA51u32);
}
/// Mirrors B1 + B2 of a3ad_a5ad_newgestalt_replacegestalt_strict:
/// dispatches _NewGestalt with five distinct fictional selectors via
/// `call_trap_word(0xA3AD)` and witnesses that A7 is preserved across
/// each call. Per IM:OSUtils 1994 p. 1-32 NewGestalt is an OS-bit
/// FUNCTION with A0+D0 entry registers and D0 exit register; no Pascal
/// stack frame is consumed.
#[test]
fn newgestalt_register_only_calling_convention_preserves_stack() {
let (mut disp, mut cpu, mut bus) = setup();
let sp_before = cpu.read_reg(Register::A7);
let fictional_fn_ptr = 0x0040_0000u32; // outside built-in zone
for &selector_bytes in &[b"kxN1", b"kxN2", b"kxN3", b"kxN4", b"kxN5"] {
cpu.write_reg(Register::A0, fictional_fn_ptr);
cpu.write_reg(Register::D0, u32::from_be_bytes(*selector_bytes));
call_trap_word(&mut disp, 0xA3AD, &mut cpu, &mut bus).unwrap();
assert_eq!(
cpu.read_reg(Register::A7),
sp_before,
"A7 preserved across NewGestalt({:?})",
std::str::from_utf8(selector_bytes).unwrap()
);
}
}
/// Mirrors B3 + B4 of a3ad_a5ad_newgestalt_replacegestalt_strict:
/// dispatches _ReplaceGestalt with five distinct fictional unknown
/// selectors via `call_trap_word(0xA5AD)` (all hit the
/// gestaltUndefSelectorErr path per IM:OSUtils 1994 p. 1-35) and
/// witnesses that A7 is preserved across each call. The bare trap
/// word is register-only ABI; the MPW FOURWORDINLINE glue's A1
/// push/pop is balanced and is not part of the trap itself.
#[test]
fn replacegestalt_register_only_calling_convention_preserves_stack() {
let (mut disp, mut cpu, mut bus) = setup();
let sp_before = cpu.read_reg(Register::A7);
let fictional_fn_ptr = 0x0040_0000u32;
for &selector_bytes in &[b"kxR1", b"kxR2", b"kxR3", b"kxR4", b"kxR5"] {
cpu.write_reg(Register::A0, fictional_fn_ptr);
cpu.write_reg(Register::D0, u32::from_be_bytes(*selector_bytes));
call_trap_word(&mut disp, 0xA5AD, &mut cpu, &mut bus).unwrap();
assert_eq!(
cpu.read_reg(Register::A7),
sp_before,
"A7 preserved across ReplaceGestalt({:?})",
std::str::from_utf8(selector_bytes).unwrap()
);
}
}
// ================================================================
// Helper: set up a param block at pb_addr with a name pointer
// ================================================================
fn setup_param_block(
bus: &mut crate::memory::MacMemoryBus,
cpu: &mut impl CpuOps,
pb_addr: u32,
filename: &[u8],
) -> u32 {
let name_addr = pb_addr + 0x100; // put name just past pb
write_pstring(bus, name_addr, filename);
bus.write_long(pb_addr + 18, name_addr); // ioNamePtr
cpu.write_reg(Register::A0, pb_addr);
name_addr
}
// ================================================================
// 15. PBOpen (0x00)
// ================================================================
#[test]
fn pb_open() {
let (mut disp, mut cpu, mut bus) = setup();
disp.vfs.insert("TestFile".to_string(), vec![1, 2, 3, 4, 5]);
let pb = 0x300000u32;
setup_param_block(&mut bus, &mut cpu, pb, b"TestFile");
call(&mut disp, false, 0x00, &mut cpu, &mut bus).unwrap();
assert_eq!(cpu.read_reg(Register::D0), 0, "D0 should be noErr");
let refnum = bus.read_word(pb + 24);
assert!(refnum >= 100);
assert!(disp.open_files.contains_key(&refnum));
}
// ================================================================
// 15b. PBOpen — file not found
// ================================================================
#[test]
fn pb_open_not_found() {
let (mut disp, mut cpu, mut bus) = setup();
let pb = 0x300000u32;
setup_param_block(&mut bus, &mut cpu, pb, b"NoSuchFile");
call(&mut disp, false, 0x00, &mut cpu, &mut bus).unwrap();
assert_eq!(
cpu.read_reg(Register::D0),
(-43i32) as u32,
"D0 should be fnfErr"
);
}
// ================================================================
// 16. FSRead (0x02)
// ================================================================
#[test]
fn fs_read() {
let (mut disp, mut cpu, mut bus) = setup();
// Insert file and open it
disp.vfs
.insert("ReadMe".to_string(), vec![10, 20, 30, 40, 50]);
disp.open_files.insert(100, "ReadMe".to_string());
disp.file_positions.insert(100, 0);
let pb = 0x300000u32;
let read_buf = 0x310000u32;
cpu.write_reg(Register::A0, pb);
bus.write_word(pb + 24, 100); // ioRefNum
bus.write_long(pb + 32, read_buf); // ioBuffer
bus.write_long(pb + 36, 3); // ioReqCount = 3
bus.write_word(pb + 44, 0); // ioPosMode = fsAtMark
bus.write_long(pb + 46, 0); // ioPosOffset
call(&mut disp, false, 0x02, &mut cpu, &mut bus).unwrap();
assert_eq!(cpu.read_reg(Register::D0), 0);
assert_eq!(bus.read_long(pb + 40), 3, "ioActCount should be 3");
assert_eq!(bus.read_byte(read_buf), 10);
assert_eq!(bus.read_byte(read_buf + 1), 20);
assert_eq!(bus.read_byte(read_buf + 2), 30);
}
// ================================================================
// 17. FSWrite (0x03)
// ================================================================
#[test]
fn fs_write() {
let (mut disp, mut cpu, mut bus) = setup();
disp.vfs.insert("WriteMe".to_string(), Vec::new());
disp.open_files.insert(100, "WriteMe".to_string());
disp.file_positions.insert(100, 0);
let pb = 0x300000u32;
let write_buf = 0x310000u32;
cpu.write_reg(Register::A0, pb);
bus.write_word(pb + 24, 100);
bus.write_long(pb + 32, write_buf);
bus.write_long(pb + 36, 4); // ioReqCount = 4
// Write data to the buffer
bus.write_byte(write_buf, 0xAA);
bus.write_byte(write_buf + 1, 0xBB);
bus.write_byte(write_buf + 2, 0xCC);
bus.write_byte(write_buf + 3, 0xDD);
call(&mut disp, false, 0x03, &mut cpu, &mut bus).unwrap();
assert_eq!(cpu.read_reg(Register::D0), 0);
assert_eq!(bus.read_long(pb + 40), 4);
let file_data = disp.vfs.get("WriteMe").unwrap();
assert_eq!(file_data, &[0xAA, 0xBB, 0xCC, 0xDD]);
}
#[test]
fn fs_write_from_start_overwrites() {
let (mut disp, mut cpu, mut bus) = setup();
disp.vfs
.insert("WriteMe".to_string(), vec![0x11, 0x22, 0x33, 0x44]);
disp.open_files.insert(100, "WriteMe".to_string());
disp.file_positions.insert(100, 4);
let pb = 0x300000u32;
let write_buf = 0x310000u32;
cpu.write_reg(Register::A0, pb);
bus.write_word(pb + 24, 100);
bus.write_long(pb + 32, write_buf);
bus.write_long(pb + 36, 2); // ioReqCount = 2
bus.write_word(pb + 44, 1); // ioPosMode = fsFromStart
bus.write_long(pb + 46, 1); // ioPosOffset = 1
bus.write_byte(write_buf, 0xAA);
bus.write_byte(write_buf + 1, 0xBB);
call(&mut disp, false, 0x03, &mut cpu, &mut bus).unwrap();
assert_eq!(cpu.read_reg(Register::D0), 0);
assert_eq!(bus.read_long(pb + 40), 2);
assert_eq!(
bus.read_long(pb + 46),
3,
"ioPosOffset should advance to new mark"
);
let file_data = disp.vfs.get("WriteMe").unwrap();
assert_eq!(file_data, &[0x11, 0xAA, 0xBB, 0x44]);
assert_eq!(*disp.file_positions.get(&100).unwrap(), 3);
}
// PBOpenRF stores the rsrc fork bytes in self.vfs under the
// "__rsrc__<name>" key so FSRead/FSWrite share the data-fork code
// path. Writes through that key must mirror back into self.vfs_rsrc —
// otherwise a later OpenRFPerm/PBOpenRF reads the stale snapshot.
#[test]
fn fs_write_to_rsrc_fork_mirrors_to_vfs_rsrc() {
let (mut disp, mut cpu, mut bus) = setup();
let rsrc_key = "__rsrc__InstallerTemp".to_string();
disp.vfs.insert(rsrc_key.clone(), Vec::new());
disp.vfs_rsrc
.insert("InstallerTemp".to_string(), Vec::new());
disp.open_files.insert(101, rsrc_key);
disp.file_positions.insert(101, 0);
let pb = 0x300000u32;
let write_buf = 0x310000u32;
cpu.write_reg(Register::A0, pb);
bus.write_word(pb + 24, 101);
bus.write_long(pb + 32, write_buf);
bus.write_long(pb + 36, 4);
bus.write_bytes(write_buf, &[0xDE, 0xAD, 0xBE, 0xEF]);
call(&mut disp, false, 0x03, &mut cpu, &mut bus).unwrap();
assert_eq!(cpu.read_reg(Register::D0), 0);
assert_eq!(
disp.vfs_rsrc.get("InstallerTemp").unwrap(),
&vec![0xDE, 0xAD, 0xBE, 0xEF],
"FSWrite to a __rsrc__ open key must mirror back to \
vfs_rsrc so a later OpenRFPerm sees the new bytes"
);
}
#[test]
fn fs_write_invalid_refnum() {
let (mut disp, mut cpu, mut bus) = setup();
let pb = 0x300000u32;
let write_buf = 0x310000u32;
cpu.write_reg(Register::A0, pb);
bus.write_word(pb + 24, 999); // invalid refnum
bus.write_long(pb + 32, write_buf);
bus.write_long(pb + 36, 2);
call(&mut disp, false, 0x03, &mut cpu, &mut bus).unwrap();
assert_eq!(cpu.read_reg(Register::D0), (-51i32) as u32);
assert_eq!(bus.read_word(pb + 16), (-51i16) as u16);
assert_eq!(bus.read_long(pb + 40), 0);
}
// ================================================================
// 18. FSClose (0x01)
// ================================================================
#[test]
fn fs_close() {
let (mut disp, mut cpu, mut bus) = setup();
disp.open_files.insert(100, "SomeFile".to_string());
disp.file_positions.insert(100, 42);
let pb = 0x300000u32;
cpu.write_reg(Register::A0, pb);
bus.write_word(pb + 24, 100); // ioRefNum
call(&mut disp, false, 0x01, &mut cpu, &mut bus).unwrap();
assert_eq!(cpu.read_reg(Register::D0), 0);
assert!(!disp.open_files.contains_key(&100));
assert!(!disp.file_positions.contains_key(&100));
}
// ================================================================
// 19. PBGetVol (0x14)
// ================================================================
#[test]
fn pb_get_vol() {
let (mut disp, mut cpu, mut bus) = setup();
let pb = 0x300000u32;
let name_buf = 0x300100u32;
cpu.write_reg(Register::A0, pb);
bus.write_long(pb + 18, name_buf); // ioNamePtr
call(&mut disp, false, 0x14, &mut cpu, &mut bus).unwrap();
assert_eq!(cpu.read_reg(Register::D0), 0);
assert_eq!(
bus.read_word(pb + 22),
super::super::dispatch::BOOT_VOLUME_REF_NUM as u16,
"ioVRefNum"
);
// Check Pascal string "MacintoshHD"
let name_bytes = bus.read_pstring(name_buf);
assert_eq!(name_bytes, b"MacintoshHD");
}
// ================================================================
// 20. PBGetVInfo (0x07)
// ================================================================
#[test]
fn pb_get_vinfo() {
let (mut disp, mut cpu, mut bus) = setup();
let pb = 0x300000u32;
let name_buf = 0x300100u32;
cpu.write_reg(Register::A0, pb);
bus.write_long(pb + 18, name_buf);
call(&mut disp, false, 0x07, &mut cpu, &mut bus).unwrap();
assert_eq!(cpu.read_reg(Register::D0), 0);
assert_eq!(
bus.read_word(pb + 22),
super::super::dispatch::BOOT_VOLUME_REF_NUM as u16,
"ioVRefNum"
);
assert_eq!(bus.read_word(pb + 40), 100, "ioVNmFls");
assert_eq!(bus.read_long(pb + 48), 512, "ioVAlBlkSiz");
// Volume name
let len = bus.read_byte(name_buf) as usize;
assert_eq!(len, 11);
}
// ================================================================
// 21. PBGetEOF (0x11)
// ================================================================
#[test]
fn pb_get_eof() {
let (mut disp, mut cpu, mut bus) = setup();
disp.vfs.insert("EOFFile".to_string(), vec![0; 256]);
disp.open_files.insert(100, "EOFFile".to_string());
let pb = 0x300000u32;
cpu.write_reg(Register::A0, pb);
bus.write_word(pb + 24, 100);
call(&mut disp, false, 0x11, &mut cpu, &mut bus).unwrap();
assert_eq!(cpu.read_reg(Register::D0), 0);
assert_eq!(bus.read_long(pb + 28), 256, "ioMisc should be file size");
}
// ================================================================
// 22. PBSetFPos (0x44)
// ================================================================
#[test]
fn pb_set_fpos() {
let (mut disp, mut cpu, mut bus) = setup();
// Open a file first
disp.vfs.insert("TestFile".to_string(), vec![0u8; 256]);
let pb = 0x300000u32;
setup_param_block(&mut bus, &mut cpu, pb, b"TestFile");
call(&mut disp, false, 0x00, &mut cpu, &mut bus).unwrap();
let refnum = bus.read_word(pb + 24);
// Set position to offset 42 from start (posMode=1)
bus.write_word(pb + 24, refnum);
bus.write_word(pb + 44, 1); // fsFromStart
bus.write_long(pb + 46, 42);
cpu.write_reg(Register::A0, pb);
call(&mut disp, false, 0x44, &mut cpu, &mut bus).unwrap();
assert_eq!(cpu.read_reg(Register::D0), 0);
assert_eq!(*disp.file_positions.get(&refnum).unwrap(), 42);
}
// ================================================================
// 23. PBFlushFile (0x45) — validates refnum
// Inside Macintosh Volume II, II-114. rfNumErr (-51) for unknown refnum.
// ================================================================
#[test]
fn pb_flush_file_unknown_refnum_returns_rfnumerr() {
let (mut disp, mut cpu, mut bus) = setup();
let pb = 0x260900u32;
for i in 0u32..32 {
bus.write_byte(pb + i, 0);
}
bus.write_word(pb + 24, 9999); // ioRefNum — not open
cpu.write_reg(Register::A0, pb);
call(&mut disp, false, 0x45, &mut cpu, &mut bus).unwrap();
assert_eq!(cpu.read_reg(Register::D0) as i32, -51);
assert_eq!(bus.read_word(pb + 16) as i16, -51);
}
// ================================================================
// 24. PBGetFPos (0x18)
// ================================================================
#[test]
fn pb_get_fpos() {
let (mut disp, mut cpu, mut bus) = setup();
disp.file_positions.insert(100, 42);
let pb = 0x300000u32;
cpu.write_reg(Register::A0, pb);
bus.write_word(pb + 24, 100);
call(&mut disp, false, 0x18, &mut cpu, &mut bus).unwrap();
assert_eq!(cpu.read_reg(Register::D0), 0);
assert_eq!(bus.read_long(pb + 46), 42, "ioPosOffset should be 42");
assert_eq!(bus.read_word(pb + 44), 0, "ioPosMode should be 0");
}
// PBGetFPos must return rfNumErr for unknown refnums per IM:II-117.
#[test]
fn pb_get_fpos_unknown_refnum_returns_rfnumerr() {
let (mut disp, mut cpu, mut bus) = setup();
let pb = 0x260A00u32;
for i in 0u32..64 {
bus.write_byte(pb + i, 0);
}
bus.write_word(pb + 24, 9999); // ioRefNum — not open
cpu.write_reg(Register::A0, pb);
call(&mut disp, false, 0x18, &mut cpu, &mut bus).unwrap();
assert_eq!(cpu.read_reg(Register::D0) as i32, -51);
assert_eq!(bus.read_word(pb + 16) as i16, -51);
}
// ================================================================
// 25. PBFlushVol (0x13)
// ================================================================
#[test]
fn pb_flush_vol() {
let (mut disp, mut cpu, mut bus) = setup();
let pb = 0x300000u32;
cpu.write_reg(Register::A0, pb);
call(&mut disp, false, 0x13, &mut cpu, &mut bus).unwrap();
assert_eq!(cpu.read_reg(Register::D0), 0);
assert_eq!(bus.read_word(pb + 16), 0);
}
#[test]
fn pb_flush_vol_unknown_refnum_returns_nsverr() {
let (mut disp, mut cpu, mut bus) = setup();
let pb = 0x300000u32;
cpu.write_reg(Register::A0, pb);
bus.write_word(pb + 22, (-999i16) as u16);
call(&mut disp, false, 0x13, &mut cpu, &mut bus).unwrap();
assert_eq!(cpu.read_reg(Register::D0) as i32, -35);
assert_eq!(bus.read_word(pb + 16) as i16, -35);
}
// ================================================================
// 26. PBCreate (0x08)
// ================================================================
#[test]
fn pb_create() {
let (mut disp, mut cpu, mut bus) = setup();
let pb = 0x300000u32;
setup_param_block(&mut bus, &mut cpu, pb, b"Pilot 1");
call(&mut disp, false, 0x08, &mut cpu, &mut bus).unwrap();
assert_eq!(cpu.read_reg(Register::D0), 0);
assert_eq!(bus.read_word(pb + 16), 0);
assert!(disp.vfs.contains_key("Pilot 1"));
assert_eq!(disp.vfs.get("Pilot 1").unwrap().len(), 0);
}
// Real Mac files always have both forks; PBCreate must seed an empty
// rsrc fork so the next PBOpenRF returns noErr instead of fnfErr.
// Files 1992, 1-58.
#[test]
fn pb_create_seeds_empty_rsrc_fork() {
let (mut disp, mut cpu, mut bus) = setup();
let pb = 0x300000u32;
setup_param_block(&mut bus, &mut cpu, pb, b"Installer Temp");
call(&mut disp, false, 0x08, &mut cpu, &mut bus).unwrap();
assert_eq!(cpu.read_reg(Register::D0), 0);
assert!(disp.vfs.contains_key("Installer Temp"));
assert!(
disp.vfs_rsrc.contains_key("Installer Temp"),
"PBCreate must seed an empty resource fork so PBOpenRF \
on the new file returns noErr"
);
assert_eq!(disp.vfs_rsrc.get("Installer Temp").unwrap().len(), 0);
}
#[test]
fn pb_create_truncate_on_exists_preserves_rsrc_fork() {
// Per IM Files 1992, 2-89, PBCreate returns dupFNErr when a
// file with the matching name already exists. Some shareware
// titles (e.g. Meteor Storm's "MS UserKey" marker) ship that
// file inside their install folder yet still call HCreate at
// launch without an intervening HDelete and treat any error
// as fatal. Systemless models the .sit-extracted folder
// directly, so the marker is always pre-existing on first
// launch — we truncate the data fork and report noErr to keep
// these titles bootable, but PRESERVE the resource fork
// because some titles bake registration templates into it
// and read them back via FSpOpenResFile + Get1Resource.
let (mut disp, mut cpu, mut bus) = setup();
disp.vfs.insert("Pilot 1".to_string(), vec![1, 2, 3]);
disp.vfs_rsrc
.insert("Pilot 1".to_string(), vec![9, 9, 9, 9]);
let pb = 0x300000u32;
setup_param_block(&mut bus, &mut cpu, pb, b"Pilot 1");
call(&mut disp, false, 0x08, &mut cpu, &mut bus).unwrap();
assert_eq!(cpu.read_reg(Register::D0), 0, "noErr from truncate path");
assert_eq!(bus.read_word(pb + 16), 0);
assert_eq!(
disp.vfs.get("Pilot 1").unwrap(),
&Vec::<u8>::new(),
"data fork must be empty after truncate"
);
assert_eq!(
disp.vfs_rsrc.get("Pilot 1").unwrap(),
&vec![9, 9, 9, 9],
"resource fork must be preserved across truncate"
);
}
// ================================================================
// 27. PBDelete (0x09)
// ================================================================
#[test]
fn pb_delete() {
let (mut disp, mut cpu, mut bus) = setup();
disp.vfs.insert("Pilot 1".to_string(), vec![1, 2, 3]);
let pb = 0x300000u32;
setup_param_block(&mut bus, &mut cpu, pb, b"Pilot 1");
call(&mut disp, false, 0x09, &mut cpu, &mut bus).unwrap();
assert_eq!(cpu.read_reg(Register::D0), 0);
assert_eq!(bus.read_word(pb + 16), 0);
assert!(!disp.vfs.contains_key("Pilot 1"));
}
#[test]
fn pb_delete_not_found() {
let (mut disp, mut cpu, mut bus) = setup();
let pb = 0x300000u32;
setup_param_block(&mut bus, &mut cpu, pb, b"MissingPilot");
call(&mut disp, false, 0x09, &mut cpu, &mut bus).unwrap();
assert_eq!(cpu.read_reg(Register::D0), (-43i32) as u32);
assert_eq!(bus.read_word(pb + 16), (-43i16) as u16);
}
// ================================================================
// 28. PBGetFInfo (0x0C) — file exists
// ================================================================
#[test]
fn pb_get_finfo_found() {
let (mut disp, mut cpu, mut bus) = setup();
disp.vfs.insert("InfoFile".to_string(), vec![1, 2, 3]);
let pb = 0x300000u32;
setup_param_block(&mut bus, &mut cpu, pb, b"InfoFile");
call(&mut disp, false, 0x0C, &mut cpu, &mut bus).unwrap();
assert_eq!(cpu.read_reg(Register::D0), 0);
assert_eq!(bus.read_word(pb + 16), 0);
}
// ================================================================
// 28b. PBGetFInfo (0x0C) — file not found
// ================================================================
#[test]
fn pb_get_finfo_not_found() {
let (mut disp, mut cpu, mut bus) = setup();
let pb = 0x300000u32;
setup_param_block(&mut bus, &mut cpu, pb, b"NoSuchFile");
call(&mut disp, false, 0x0C, &mut cpu, &mut bus).unwrap();
assert_eq!(cpu.read_reg(Register::D0), (-43i32) as u32);
}
// ================================================================
// 28c. PBAllocate ($A010)
// ================================================================
#[test]
fn pballocate_nominal_call_returns_noerr_and_sets_ioactcount() {
// Inside Macintosh: Files (1992), pp. 2-130 to 2-131:
// PBAllocate reports result via D0/ioResult and writes ioActCount.
let (mut disp, mut cpu, mut bus) = setup();
let pb = 0x300000u32;
cpu.write_reg(Register::A0, pb);
bus.write_word(pb + 16, 0x3FFF); // ioResult poison
bus.write_long(pb + 36, 0x1234); // ioReqCount
bus.write_long(pb + 40, 0xFFFF_FFFF); // ioActCount poison
call_trap_word(&mut disp, 0xA010, &mut cpu, &mut bus).unwrap();
assert_eq!(cpu.read_reg(Register::D0) as i32, 0, "noErr in D0");
assert_eq!(bus.read_word(pb + 16) as i16, 0, "ioResult overwritten");
assert_eq!(bus.read_long(pb + 40), 0x1234, "ioActCount mirrors request");
}
#[test]
fn pballocate_writes_ioresult_noerr_when_paramblock_present() {
// Inside Macintosh: Files (1992), pp. 2-130 to 2-131:
// PBAllocate uses ioResult as the parameter-block mirror of D0.
let (mut disp, mut cpu, mut bus) = setup();
let pb = 0x300000u32;
cpu.write_reg(Register::A0, pb);
bus.write_word(pb + 16, 0x7B7B); // ioResult poison
bus.write_long(pb + 36, 1);
call_trap_word(&mut disp, 0xA010, &mut cpu, &mut bus).unwrap();
assert_eq!(cpu.read_reg(Register::D0) as i32, 0);
assert_eq!(bus.read_word(pb + 16) as i16, 0);
}
/// Mirrors bands B1+B2 of the `a010_pballocate_strict` BasiliskII bake:
/// pre-poisons pb.ioResult at pb+16 with a non-noErr, non-OSErr sentinel
/// (0x3FFF), sets ioRefNum to a clearly-bogus 9999, dispatches $A010,
/// asserts the sentinel was overwritten AND D0 == ioResult per the
/// File Manager dispatcher convention (Inside Macintosh Volume II 1985,
/// p. II-114) AND that A7 is preserved across the call.
#[test]
fn pballocate_writes_same_oserr_to_d0_and_ioresult_preserving_stack() {
let (mut disp, mut cpu, mut bus) = setup();
let pb = 0x300000u32;
cpu.write_reg(Register::A0, pb);
bus.write_word(pb + 16, 0x3FFF); // ioResult pre-poison
bus.write_word(pb + 24, 9999); // ioRefNum bogus
bus.write_long(pb + 36, 256); // ioReqCount
let sp_pre = cpu.read_reg(Register::A7);
call_trap_word(&mut disp, 0xA010, &mut cpu, &mut bus).unwrap();
let sp_post = cpu.read_reg(Register::A7);
let d0 = cpu.read_reg(Register::D0) as i16;
let io_result = bus.read_word(pb + 16) as i16;
assert_ne!(io_result, 0x3FFF, "ioResult must be overwritten");
assert_eq!(d0, io_result, "D0 must mirror pb.ioResult");
assert_eq!(sp_pre, sp_post, "A7 must be preserved");
}
// ================================================================
// 28d. PBAllocContig ($A210)
// ================================================================
#[test]
fn pballoccontig_nominal_call_returns_noerr_and_sets_ioactcount() {
// Inside Macintosh: Files (1992), pp. 2-130 to 2-131:
// PBAllocContig reports result via D0/ioResult and writes ioActCount.
let (mut disp, mut cpu, mut bus) = setup();
let pb = 0x300000u32;
cpu.write_reg(Register::A0, pb);
bus.write_word(pb + 16, 0x3FFF); // ioResult poison
bus.write_long(pb + 36, 0x1234); // ioReqCount
bus.write_long(pb + 40, 0xFFFF_FFFF); // ioActCount poison
call_trap_word(&mut disp, 0xA210, &mut cpu, &mut bus).unwrap();
assert_eq!(cpu.read_reg(Register::D0) as i32, 0, "noErr in D0");
assert_eq!(bus.read_word(pb + 16) as i16, 0, "ioResult overwritten");
assert_eq!(bus.read_long(pb + 40), 0x1234, "ioActCount mirrors request");
}
#[test]
fn pballoccontig_overwrites_ioresult_field_with_function_result() {
// Inside Macintosh: Files (1992), p. 2-131 result-code contract:
// ioResult is the function-result field for PBAllocContig.
let (mut disp, mut cpu, mut bus) = setup();
let pb = 0x300000u32;
cpu.write_reg(Register::A0, pb);
bus.write_word(pb + 16, 0x7B7B); // ioResult poison
bus.write_long(pb + 36, 1);
call_trap_word(&mut disp, 0xA210, &mut cpu, &mut bus).unwrap();
assert_eq!(cpu.read_reg(Register::D0) as i32, 0);
assert_eq!(bus.read_word(pb + 16) as i16, 0);
}
/// Mirrors bands B1+B2 of the `a210_pballoccontig_strict` BasiliskII
/// bake: pre-poisons pb.ioResult at pb+16 with a non-noErr, non-OSErr
/// sentinel (0x3FFF), sets ioRefNum to a clearly-bogus 9999, dispatches
/// $A210, asserts the sentinel was overwritten AND D0 == ioResult per
/// the File Manager dispatcher convention (Inside Macintosh Volume II
/// 1985, p. II-114) AND that A7 is preserved across the call.
#[test]
fn pballoccontig_writes_same_oserr_to_d0_and_ioresult_preserving_stack() {
let (mut disp, mut cpu, mut bus) = setup();
let pb = 0x300000u32;
cpu.write_reg(Register::A0, pb);
bus.write_word(pb + 16, 0x3FFF); // ioResult pre-poison
bus.write_word(pb + 24, 9999); // ioRefNum bogus
bus.write_long(pb + 36, 256); // ioReqCount
let sp_pre = cpu.read_reg(Register::A7);
call_trap_word(&mut disp, 0xA210, &mut cpu, &mut bus).unwrap();
let sp_post = cpu.read_reg(Register::A7);
let d0 = cpu.read_reg(Register::D0) as i16;
let io_result = bus.read_word(pb + 16) as i16;
assert_ne!(io_result, 0x3FFF, "ioResult must be overwritten");
assert_eq!(d0, io_result, "D0 must mirror pb.ioResult");
assert_eq!(sp_pre, sp_post, "A7 must be preserved");
}
// ================================================================
// 28e. Volume / I/O queue stubs ($A006, $A016, $A017)
// ================================================================
#[test]
fn pbkillio_returns_noerr_when_hle_has_no_async_io_queue() {
// Inside Macintosh Volume II (1985), p. II-187:
// PBKillIO reports an OSErr result; HLE succeeds with noErr.
let (mut disp, mut cpu, mut bus) = setup();
let pb = 0x300200u32;
cpu.write_reg(Register::A0, pb);
bus.write_word(pb + 24, 100); // ioRefNum
call(&mut disp, false, 0x06, &mut cpu, &mut bus).unwrap();
assert_eq!(cpu.read_reg(Register::D0) as i32, 0, "noErr in D0");
}
#[test]
fn pbkillio_writes_ioresult_noerr_when_paramblock_present() {
// Inside Macintosh Volume II (1985), p. II-187:
// ioResult is the returned function result field for PBKillIO.
let (mut disp, mut cpu, mut bus) = setup();
let pb = 0x300240u32;
cpu.write_reg(Register::A0, pb);
bus.write_word(pb + 16, 0x6A6A); // ioResult poison
bus.write_word(pb + 24, 200); // ioRefNum
call(&mut disp, false, 0x06, &mut cpu, &mut bus).unwrap();
assert_eq!(cpu.read_reg(Register::D0) as i32, 0);
assert_eq!(bus.read_word(pb + 16) as i16, 0, "ioResult overwritten");
}
#[test]
fn pbkillio_writes_same_oserr_to_d0_and_ioresult_preserving_stack() {
// Mirrors a006_pbkillio_strict B1+B2: pre-poison ioResult with
// 0x3FFF (not noErr and not a documented OSErr), set ioRefNum to
// a clearly-bogus value, dispatch _PBKillIO, witness that the
// sentinel was overwritten AND D0 == ioResult per the Device
// Manager OS-bit FUNCTION dispatcher convention (IM:II 1985,
// p. II-114), AND A7 unchanged across the trap.
let (mut disp, mut cpu, mut bus) = setup();
let pb = 0x300280u32;
cpu.write_reg(Register::A0, pb);
bus.write_word(pb + 16, 0x3FFFu16); // pre-poison ioResult
bus.write_word(pb + 24, 9999u16); // bogus ioRefNum
let sp_pre = cpu.read_reg(Register::A7);
call(&mut disp, false, 0x06, &mut cpu, &mut bus).unwrap();
let sp_post = cpu.read_reg(Register::A7);
let d0 = cpu.read_reg(Register::D0) as i16;
let io_result = bus.read_word(pb + 16) as i16;
assert_ne!(
io_result, 0x3FFFi16,
"ioResult sentinel must be overwritten"
);
assert_eq!(d0, io_result, "D0 == ioResult per dispatcher convention");
assert_eq!(sp_pre, sp_post, "A7 preserved (register-only ABI)");
}
#[test]
fn finitqueue_has_no_parameters_and_preserves_stack_pointer() {
// Inside Macintosh Volume II (1985), p. II-103:
// FInitQueue is declared as PROCEDURE FInitQueue with no parameters.
let (mut disp, mut cpu, mut bus) = setup();
let sp_before = cpu.read_reg(Register::A7);
cpu.write_reg(Register::D0, 0xDEAD_BEEF);
call(&mut disp, false, 0x16, &mut cpu, &mut bus).unwrap();
assert_eq!(cpu.read_reg(Register::A7), sp_before, "A7 preserved");
assert_eq!(
cpu.read_reg(Register::D0) as i32,
0,
"HLE returns noErr in D0"
);
}
#[test]
fn finitqueue_five_call_composition_preserves_stack_and_returns_noerr_each_call() {
// Mirrors B2 of a016_finitqueue_strict: 5
// successive _FInitQueue dispatches inside one StackSpace
// sandwich. Per-call pop discipline errors accumulate; this
// pins cumulative drift even when each individual call's
// drift would be small.
let (mut disp, mut cpu, mut bus) = setup();
let sp_pre = cpu.read_reg(Register::A7);
for _ in 0..5 {
cpu.write_reg(Register::D0, 0xCAFE_F00D);
call(&mut disp, false, 0x16, &mut cpu, &mut bus).unwrap();
assert_eq!(
cpu.read_reg(Register::D0) as i32,
0,
"HLE returns noErr in D0 on each call"
);
}
assert_eq!(
cpu.read_reg(Register::A7),
sp_pre,
"A7 preserved after 5-call composition"
);
}
#[test]
fn adddrive_returns_noerr_and_preserves_stack_pointer() {
// Inside Macintosh: Files (1992), p. 2-236 and Technical Note #108
// define AddDrive as a register-only call with a noErr return.
let (mut disp, mut cpu, mut bus) = setup();
let qel = 0x320400u32;
cpu.write_reg(Register::A0, qel);
cpu.write_reg(Register::D0, (7u32 << 16) | 42u32);
let sp_before = cpu.read_reg(Register::A7);
call_trap_word(&mut disp, 0xA04E, &mut cpu, &mut bus).unwrap();
assert_eq!(cpu.read_reg(Register::A7), sp_before, "A7 preserved");
assert_eq!(cpu.read_reg(Register::D0) as i32, 0, "noErr in D0");
assert_eq!(cpu.read_reg(Register::A0), qel, "A0 preserved");
}
#[test]
fn adddrive_five_call_composition_preserves_stack_and_returns_noerr_each_call() {
// Mirrors the strict-bake AddDrive witness: 5 successive
// register-only dispatches inside one StackSpace sandwich.
let (mut disp, mut cpu, mut bus) = setup();
let qel = 0x320500u32;
cpu.write_reg(Register::A0, qel);
let sp_before = cpu.read_reg(Register::A7);
for _ in 0..5 {
cpu.write_reg(Register::D0, (11u32 << 16) | 99u32);
call_trap_word(&mut disp, 0xA04E, &mut cpu, &mut bus).unwrap();
assert_eq!(cpu.read_reg(Register::D0) as i32, 0, "noErr in D0");
assert_eq!(cpu.read_reg(Register::A0), qel, "A0 preserved");
}
assert_eq!(cpu.read_reg(Register::A7), sp_before, "A7 preserved");
}
#[test]
fn pboffline_writes_same_oserr_to_d0_and_ioresult_preserving_stack() {
// Mirrors B1 + B2 of a035_pboffline_strict:
// pre-poisons pb.ioResult at pb+16 with 0x3FFF (neither noErr nor
// any documented OSErr), dispatches _PBOffLine with a clearly-
// bogus ioVRefNum 9999, witnesses that the trap overwrote the
// sentinel AND that D0 == ioResult (per File Manager dispatcher
// convention IM:II 1985 p. II-114) AND that A7 is preserved
// across the call (register-only OS-bit FUNCTION calling
// convention per IM:Files 1992 pp. 2-141..2-142).
let (mut disp, mut cpu, mut bus) = setup();
let sp_before = cpu.read_reg(Register::A7);
let pb = 0x300300u32;
for i in 0..50u32 {
bus.write_byte(pb + i, 0);
}
bus.write_word(pb + 16, 0x3FFF); // ioResult poison
bus.write_word(pb + 22, 9999); // ioVRefNum bogus
cpu.write_reg(Register::A0, pb);
cpu.write_reg(Register::D0, 0xDEAD_BEEF);
call(&mut disp, false, 0x35, &mut cpu, &mut bus).unwrap();
let d0 = cpu.read_reg(Register::D0) as i16;
let io_result = bus.read_word(pb + 16) as i16;
assert_ne!(io_result, 0x3FFFi16, "trap overwrote ioResult sentinel");
assert_eq!(
d0, io_result,
"D0 mirrors ioResult per dispatcher convention"
);
assert_eq!(cpu.read_reg(Register::A7), sp_before, "A7 preserved");
}
#[test]
fn pbeject_returns_noerr_when_hle_has_no_removable_media() {
// Inside Macintosh: Files (1992), p. 2-141:
// PBEject reports noErr for successful nominal calls.
let (mut disp, mut cpu, mut bus) = setup();
let pb = 0x300280u32;
cpu.write_reg(Register::A0, pb);
bus.write_word(pb + 22, super::super::dispatch::BOOT_VOLUME_REF_NUM as u16);
call(&mut disp, false, 0x17, &mut cpu, &mut bus).unwrap();
assert_eq!(cpu.read_reg(Register::D0) as i32, 0, "noErr in D0");
}
#[test]
fn pbeject_writes_ioresult_noerr_when_paramblock_present() {
// Inside Macintosh: Files (1992), p. 2-141:
// ioResult carries the function result in the parameter block.
let (mut disp, mut cpu, mut bus) = setup();
let pb = 0x3002C0u32;
cpu.write_reg(Register::A0, pb);
bus.write_word(pb + 16, 0x5151); // ioResult poison
bus.write_word(pb + 22, super::super::dispatch::BOOT_VOLUME_REF_NUM as u16);
call(&mut disp, false, 0x17, &mut cpu, &mut bus).unwrap();
assert_eq!(cpu.read_reg(Register::D0) as i32, 0);
assert_eq!(bus.read_word(pb + 16) as i16, 0, "ioResult overwritten");
}
#[test]
fn pbeject_writes_same_oserr_to_d0_and_ioresult_preserving_stack() {
// Mirrors B1 + B2 of a017_pbeject_strict:
// pre-poisons pb.ioResult at pb+16 with 0x3FFF (neither noErr nor
// any documented OSErr), dispatches _PBEject with a clearly-
// bogus ioVRefNum 9999, witnesses that the trap overwrote the
// sentinel AND that D0 == ioResult (per File Manager dispatcher
// convention IM:II 1985 p. II-114) AND that A7 is preserved
// across the call (register-only OS-bit FUNCTION calling
// convention per IM:Files 1992 p. 2-141).
let (mut disp, mut cpu, mut bus) = setup();
let sp_before = cpu.read_reg(Register::A7);
let pb = 0x300340u32;
for i in 0..50u32 {
bus.write_byte(pb + i, 0);
}
bus.write_word(pb + 16, 0x3FFF); // ioResult poison
bus.write_word(pb + 22, 9999); // ioVRefNum bogus
cpu.write_reg(Register::A0, pb);
cpu.write_reg(Register::D0, 0xDEAD_BEEF);
call(&mut disp, false, 0x17, &mut cpu, &mut bus).unwrap();
let d0 = cpu.read_reg(Register::D0) as i16;
let io_result = bus.read_word(pb + 16) as i16;
assert_ne!(io_result, 0x3FFFi16, "trap overwrote ioResult sentinel");
assert_eq!(
d0, io_result,
"D0 mirrors ioResult per dispatcher convention"
);
assert_eq!(cpu.read_reg(Register::A7), sp_before, "A7 preserved");
}
/// Mirrors B1 + B2 of a00f_pbmountvol_strict:
/// pre-poisons pb.ioResult at pb+16 with 0x3FFF (neither noErr nor any
/// documented OSErr), dispatches _PBMountVol with a clearly-bogus
/// ioVRefNum=9999 drive number, witnesses that the trap overwrote the
/// sentinel AND that D0 == ioResult (per File Manager dispatcher
/// convention IM:II 1985 p. II-114) AND that A7 is preserved across
/// the call (register-only OS-bit FUNCTION calling convention per
/// IM:Files 1992 p. 2-139).
#[test]
fn pbmountvol_writes_same_oserr_to_d0_and_ioresult_preserving_stack() {
let (mut disp, mut cpu, mut bus) = setup();
let sp_before = cpu.read_reg(Register::A7);
let pb = 0x300380u32;
for i in 0..50u32 {
bus.write_byte(pb + i, 0);
}
bus.write_word(pb + 16, 0x3FFF); // ioResult poison
bus.write_word(pb + 22, 9999); // ioVRefNum bogus drive number
cpu.write_reg(Register::A0, pb);
cpu.write_reg(Register::D0, 0xDEAD_BEEF);
call_trap_word(&mut disp, 0xA00F, &mut cpu, &mut bus).unwrap();
let d0 = cpu.read_reg(Register::D0) as i16;
let io_result = bus.read_word(pb + 16) as i16;
assert_ne!(io_result, 0x3FFFi16, "trap overwrote ioResult sentinel");
assert_eq!(
d0, io_result,
"D0 mirrors ioResult per dispatcher convention"
);
assert_eq!(cpu.read_reg(Register::A7), sp_before, "A7 preserved");
}
/// Mirrors B1..B4 of a00e_a20e_pbunmountvol_pbunmountvolimmed_strict:
/// pre-poisons pb.ioResult at pb+16 with 0x3FFF on two separate parameter
/// blocks, dispatches _PBUnmountVol ($A00E) then _PBUnmountVolImmed
/// ($A20E) — which share the same low-byte arm — with a clearly-bogus
/// ioVRefNum 9999, witnesses that each trap overwrote its sentinel AND
/// that D0 == ioResult per File Manager dispatcher convention (IM:II
/// 1985 p. II-114) AND that A7 is preserved across both calls
/// (register-only OS-bit FUNCTION calling convention per IM:Files
/// 1992 p. 2-148).
#[test]
fn pbunmountvol_writes_same_oserr_to_d0_and_ioresult_preserving_stack() {
let (mut disp, mut cpu, mut bus) = setup();
let sp_before = cpu.read_reg(Register::A7);
let pb_a00e = 0x3003C0u32;
let pb_a20e = 0x300400u32;
for i in 0..50u32 {
bus.write_byte(pb_a00e + i, 0);
bus.write_byte(pb_a20e + i, 0);
}
bus.write_word(pb_a00e + 16, 0x3FFF);
bus.write_word(pb_a00e + 22, 9999);
bus.write_word(pb_a20e + 16, 0x3FFF);
bus.write_word(pb_a20e + 22, 9999);
cpu.write_reg(Register::A0, pb_a00e);
cpu.write_reg(Register::D0, 0xDEAD_BEEF);
call_trap_word(&mut disp, 0xA00E, &mut cpu, &mut bus).unwrap();
let d0_a00e = cpu.read_reg(Register::D0) as i16;
let io_a00e = bus.read_word(pb_a00e + 16) as i16;
assert_ne!(io_a00e, 0x3FFFi16, "A00E overwrote ioResult sentinel");
assert_eq!(d0_a00e, io_a00e, "A00E D0 mirrors ioResult");
cpu.write_reg(Register::A0, pb_a20e);
cpu.write_reg(Register::D0, 0xDEAD_BEEF);
call_trap_word(&mut disp, 0xA20E, &mut cpu, &mut bus).unwrap();
let d0_a20e = cpu.read_reg(Register::D0) as i16;
let io_a20e = bus.read_word(pb_a20e + 16) as i16;
assert_ne!(io_a20e, 0x3FFFi16, "A20E overwrote ioResult sentinel");
assert_eq!(d0_a20e, io_a20e, "A20E D0 mirrors ioResult");
assert_eq!(
cpu.read_reg(Register::A7),
sp_before,
"A7 preserved across both A00E and A20E dispatches",
);
}
/// Mirrors B1 + B2 of a043_pbsetfvers_strict:
/// pre-poisons pb.ioResult at pb+16 with 0x3FFF, dispatches _PBSetFVers
/// with a bogus ioVRefNum=9999 (no such volume), witnesses that the
/// trap overwrote the sentinel AND that D0 == ioResult per File
/// Manager dispatcher convention (IM:II 1985 p. II-114) AND that A7
/// is preserved across the call (register-only OS-bit FUNCTION
/// calling convention per IM:II 1985 p. II-117). Per IM:IV 1986
/// p. IV-153, PBSetFVers is a documented no-op on hierarchical
/// volumes; Systemless's HFS-only VFS makes the trap always-noErr.
#[test]
fn pbsetfvers_writes_same_oserr_to_d0_and_ioresult_preserving_stack() {
let (mut disp, mut cpu, mut bus) = setup();
let sp_before = cpu.read_reg(Register::A7);
let pb = 0x300440u32;
for i in 0..50u32 {
bus.write_byte(pb + i, 0);
}
bus.write_word(pb + 16, 0x3FFF); // ioResult poison
bus.write_word(pb + 22, 9999); // ioVRefNum bogus
cpu.write_reg(Register::A0, pb);
cpu.write_reg(Register::D0, 0xDEAD_BEEF);
call_trap_word(&mut disp, 0xA043, &mut cpu, &mut bus).unwrap();
let d0 = cpu.read_reg(Register::D0) as i16;
let io_result = bus.read_word(pb + 16) as i16;
assert_ne!(io_result, 0x3FFFi16, "trap overwrote ioResult sentinel");
assert_eq!(
d0, io_result,
"D0 mirrors ioResult per dispatcher convention"
);
assert_eq!(cpu.read_reg(Register::A7), sp_before, "A7 preserved");
}
/// Mirrors B1 + B2 of a20b_pbhrename_strict:
/// pre-poisons pb.ioResult at pb+16 with 0x3FFF, dispatches _PBHRename
/// with bogus old/new filenames guaranteed not to exist in the VFS,
/// witnesses that the trap overwrote the sentinel AND that D0 ==
/// ioResult per File Manager dispatcher convention (IM:II 1985
/// p. II-114) AND that A7 is preserved across the call (register-only
/// OS-bit FUNCTION calling convention per IM:Files 1992 p. 2-118).
#[test]
fn pbhrename_writes_same_oserr_to_d0_and_ioresult_preserving_stack() {
let (mut disp, mut cpu, mut bus) = setup();
let sp_before = cpu.read_reg(Register::A7);
let pb = 0x300480u32;
for i in 0..60u32 {
bus.write_byte(pb + i, 0);
}
let old_name_ptr = 0x301000u32;
let new_name_ptr = 0x301040u32;
let old_name = b"\x1FNoSuchFile_A20B_PBHRename_OLD";
let new_name = b"\x1FNoSuchFile_A20B_PBHRename_NEW";
for (i, &b) in old_name.iter().enumerate() {
bus.write_byte(old_name_ptr + i as u32, b);
}
for (i, &b) in new_name.iter().enumerate() {
bus.write_byte(new_name_ptr + i as u32, b);
}
bus.write_word(pb + 16, 0x3FFF); // ioResult poison
bus.write_long(pb + 18, old_name_ptr); // ioNamePtr
bus.write_word(pb + 22, 0); // ioVRefNum default
bus.write_long(pb + 28, new_name_ptr); // ioMisc -> new name
cpu.write_reg(Register::A0, pb);
cpu.write_reg(Register::D0, 0xDEAD_BEEF);
call_trap_word(&mut disp, 0xA20B, &mut cpu, &mut bus).unwrap();
let d0 = cpu.read_reg(Register::D0) as i16;
let io_result = bus.read_word(pb + 16) as i16;
assert_ne!(io_result, 0x3FFFi16, "trap overwrote ioResult sentinel");
assert_eq!(
d0, io_result,
"D0 mirrors ioResult per dispatcher convention"
);
assert_eq!(cpu.read_reg(Register::A7), sp_before, "A7 preserved");
}
// ================================================================
// 29. PBSetVol (0x15)
// ================================================================
#[test]
fn pb_set_vol() {
let (mut disp, mut cpu, mut bus) = setup();
let dir_id = disp.ensure_vfs_directory("Marathon");
disp.vfs.insert("Marathon/Marathon".to_string(), vec![]);
disp.set_launched_app_path("Marathon/Marathon");
let pb = 0x300000u32;
cpu.write_reg(Register::A0, pb);
bus.write_word(pb + 22, disp.app_wd_refnum as u16);
call(&mut disp, false, 0x15, &mut cpu, &mut bus).unwrap();
assert_eq!(cpu.read_reg(Register::D0), 0);
assert_eq!(bus.read_word(pb + 16), 0);
assert_eq!(disp.default_dir_id, dir_id);
assert_eq!(bus.read_long(addr::CUR_DIR_STORE), dir_id);
assert_eq!(
bus.read_word(addr::SF_SAVE_DISK),
(-super::super::dispatch::BOOT_VOLUME_REF_NUM) as u16
);
}
#[test]
fn pbhsetvol_sets_default_directory_from_iowddirid_for_volume_refnum_calls() {
// Inside Macintosh: Files (1992), pp. 2-153 to 2-154:
// with ioNamePtr = NIL and a volume refnum in ioVRefNum,
// PBHSetVol uses ioWDDirID as the default directory.
let (mut disp, mut cpu, mut bus) = setup();
let dir_id = disp.ensure_vfs_directory("Marathon");
let pb = 0x300000u32;
cpu.write_reg(Register::A0, pb);
bus.write_word(pb + 22, super::super::dispatch::BOOT_VOLUME_REF_NUM as u16);
bus.write_long(pb + 48, dir_id);
bus.write_long(pb + 18, 0); // ioNamePtr = NIL
call_trap_word(&mut disp, 0xA215, &mut cpu, &mut bus).unwrap();
assert_eq!(cpu.read_reg(Register::D0), 0);
assert_eq!(bus.read_word(pb + 16), 0);
assert_eq!(disp.default_dir_id, dir_id);
assert_ne!(
disp.app_wd_refnum,
super::super::dispatch::BOOT_VOLUME_REF_NUM
);
assert_eq!(bus.read_long(addr::CUR_DIR_STORE), dir_id);
}
#[test]
fn pbhsetvol_invalid_vrefnum_returns_nsverr() {
// Inside Macintosh: Files (1992), pp. 2-153 to 2-154:
// unresolved volume references return nsvErr.
let (mut disp, mut cpu, mut bus) = setup();
let pb = 0x300000u32;
cpu.write_reg(Register::A0, pb);
bus.write_word(pb + 22, (-999i16) as u16); // ioVRefNum phantom
bus.write_long(pb + 48, 0); // ioWDDirID = 0 keeps the call on the nsvErr path
call_trap_word(&mut disp, 0xA215, &mut cpu, &mut bus).unwrap();
assert_eq!(cpu.read_reg(Register::D0) as i32, -35, "nsvErr in D0");
assert_eq!(bus.read_word(pb + 16) as i16, -35, "nsvErr in ioResult");
}
#[test]
fn pbhsetvol_writes_same_oserr_to_d0_and_ioresult_preserving_stack() {
// Mirrors B1 + B2 of the a215_pbhsetvol_strict bake.
// Pre-poisons pb.ioResult @ pb+16 with 0x3FFF and sets a phantom
// ioVRefNum (-999); asserts the sentinel is overwritten AND
// D0 == ioResult per the File Manager basic-PB dispatcher
// convention AND A7 is preserved across the call.
let (mut disp, mut cpu, mut bus) = setup();
let pb = 0x300000u32;
cpu.write_reg(Register::A0, pb);
bus.write_word(pb + 16, 0x3FFFu16); // pre-poison ioResult
bus.write_word(pb + 22, (-999i16) as u16); // ioVRefNum phantom
bus.write_long(pb + 48, 0); // ioWDDirID = 0
let sp_pre = cpu.read_reg(Register::A7);
call_trap_word(&mut disp, 0xA215, &mut cpu, &mut bus).unwrap();
let sp_post = cpu.read_reg(Register::A7);
let io_result = bus.read_word(pb + 16) as i16;
let d0_result = cpu.read_reg(Register::D0) as i32 as i16;
assert_ne!(io_result as u16, 0x3FFFu16, "ioResult sentinel overwritten");
assert_eq!(
d0_result, io_result,
"D0 == ioResult per File Manager basic-PB dispatcher convention"
);
assert_eq!(sp_pre, sp_post, "A7 preserved across PBHSetVol");
}
// ================================================================
// 30. PBSetFInfo / HSetFInfo (0x0D)
// ================================================================
#[test]
fn pb_set_finfo() {
// Updated for Files 1992 2-205 contract: existing file →
// noErr, missing file → fnfErr. Prior to the fix this test
// relied on PBSetFInfo silently returning noErr for any
// filename (including ones not in the VFS).
let (mut disp, mut cpu, mut bus) = setup();
disp.vfs.insert("AnyFile".to_string(), Vec::new());
let pb = 0x300000u32;
setup_param_block(&mut bus, &mut cpu, pb, b"AnyFile");
call(&mut disp, false, 0x0D, &mut cpu, &mut bus).unwrap();
assert_eq!(cpu.read_reg(Register::D0), 0);
assert_eq!(bus.read_word(pb + 16), 0);
}
#[test]
fn pb_set_finfo_missing_file_is_fnferr() {
// Regression: previously PBSetFInfo returned noErr for missing
// files, silently masking missing-save bugs in games that
// rely on fnfErr (Files 1992, 2-205).
let (mut disp, mut cpu, mut bus) = setup();
let pb = 0x300000u32;
setup_param_block(&mut bus, &mut cpu, pb, b"NotPresent");
call(&mut disp, false, 0x0D, &mut cpu, &mut bus).unwrap();
assert_eq!(cpu.read_reg(Register::D0) as i32, -43);
assert_eq!(bus.read_word(pb + 16) as i16, -43);
}
// ================================================================
// 31. PBSetEOF / HSetEOF ($A012)
// ================================================================
#[test]
fn pb_set_eof() {
let (mut disp, mut cpu, mut bus) = setup();
disp.vfs
.insert("EOFSet".to_string(), vec![0xAA, 0xBB, 0xCC, 0xDD]);
disp.open_files.insert(100, "EOFSet".to_string());
disp.file_positions.insert(100, 4);
let pb = 0x300000u32;
cpu.write_reg(Register::A0, pb);
bus.write_word(pb + 24, 100); // ioRefNum
bus.write_long(pb + 28, 2); // ioMisc = new EOF
call(&mut disp, false, 0x12, &mut cpu, &mut bus).unwrap();
assert_eq!(cpu.read_reg(Register::D0), 0);
assert_eq!(bus.read_word(pb + 16), 0);
assert_eq!(disp.vfs.get("EOFSet").unwrap(), &vec![0xAA, 0xBB]);
assert_eq!(*disp.file_positions.get(&100).unwrap(), 2);
}
#[test]
fn pb_set_eof_invalid_refnum() {
let (mut disp, mut cpu, mut bus) = setup();
let pb = 0x300000u32;
cpu.write_reg(Register::A0, pb);
bus.write_word(pb + 24, 999);
bus.write_long(pb + 28, 2);
call(&mut disp, false, 0x12, &mut cpu, &mut bus).unwrap();
assert_eq!(cpu.read_reg(Register::D0), (-51i32) as u32);
assert_eq!(bus.read_word(pb + 16), (-51i16) as u16);
}
// ================================================================
// 32. FSDispatch (0x60) — selector 8 (HGetFInfo), file not found
// ================================================================
#[test]
fn fs_dispatch_hgetfinfo_not_found() {
let (mut disp, mut cpu, mut bus) = setup();
let pb = 0x300000u32;
setup_param_block(&mut bus, &mut cpu, pb, b"MissingFile");
bus.write_long(pb + 48, 2); // dirID
cpu.write_reg(Register::D0, 8); // selector = HGetFInfo
call(&mut disp, false, 0x60, &mut cpu, &mut bus).unwrap();
assert_eq!(
cpu.read_reg(Register::D0),
(-43i32) as u32,
"D0 should be fnfErr"
);
assert_eq!(bus.read_word(pb + 16), (-43i16) as u16);
}
// ================================================================
// 32b. FSDispatch (0x60) — selector 8 (HGetFInfo), file found
// ================================================================
#[test]
fn fs_dispatch_hgetfinfo_found() {
let (mut disp, mut cpu, mut bus) = setup();
disp.vfs.insert("FoundFile".to_string(), vec![0xAB]);
let pb = 0x300000u32;
setup_param_block(&mut bus, &mut cpu, pb, b"FoundFile");
bus.write_long(pb + 48, 2);
cpu.write_reg(Register::D0, 8);
call(&mut disp, false, 0x60, &mut cpu, &mut bus).unwrap();
assert_eq!(cpu.read_reg(Register::D0), 0);
assert_eq!(bus.read_word(pb + 16), 0);
}
#[test]
fn initfs_returns_noerr_and_preserves_stack_pointer() {
let (mut disp, mut cpu, mut bus) = setup();
let sp_pre = cpu.read_reg(Register::A7);
call(&mut disp, false, 0x6C, &mut cpu, &mut bus).unwrap();
assert_eq!(cpu.read_reg(Register::A7), sp_pre);
assert_eq!(cpu.read_reg(Register::D0), 0);
}
#[test]
fn fs_dispatch_hgetfinfo_retries_default_directory_for_stale_dirid() {
let (mut disp, mut cpu, mut bus) = setup();
disp.ensure_vfs_directory("Marathon");
disp.vfs.insert("Marathon/Marathon".to_string(), vec![]);
disp.vfs
.insert("Marathon/Physics Model".to_string(), vec![0xAB]);
disp.set_launched_app_path("Marathon/Marathon");
let pb = 0x300000u32;
setup_param_block(&mut bus, &mut cpu, pb, b"Physics Model");
bus.write_long(pb + 48, 0x01FF01FF);
cpu.write_reg(Register::D0, 8);
call(&mut disp, false, 0x60, &mut cpu, &mut bus).unwrap();
assert_eq!(cpu.read_reg(Register::D0), 0);
assert_eq!(bus.read_word(pb + 16), 0);
}
// PBFlushFile (0x45) — valid open refnum returns noErr.
// Files 1992, 2-114: noErr when the refnum identifies an open access path.
#[test]
fn pb_flush_file_valid_refnum_returns_noerr() {
let (mut disp, mut cpu, mut bus) = setup();
disp.vfs.insert("Flush.dat".to_string(), vec![1, 2, 3]);
let pb = 0x270000u32;
for i in 0u32..32 {
bus.write_byte(pb + i, 0);
}
setup_param_block(&mut bus, &mut cpu, pb, b"Flush.dat");
call(&mut disp, false, 0x00, &mut cpu, &mut bus).unwrap(); // PBOpen → assigns refnum
let refnum = bus.read_word(pb + 24);
assert!(refnum >= 100, "expected open refnum");
for i in 0u32..32 {
bus.write_byte(pb + i, 0);
}
bus.write_word(pb + 24, refnum);
cpu.write_reg(Register::A0, pb);
call(&mut disp, false, 0x45, &mut cpu, &mut bus).unwrap();
assert_eq!(cpu.read_reg(Register::D0) as i32, 0, "valid refnum → noErr");
assert_eq!(bus.read_word(pb + 16) as i16, 0);
}
// PBRename (0x0B) — renaming an existing VFS file updates all state.
// Files 1992, 2-118: the file entry and any open access paths follow the rename.
#[test]
fn pb_rename_existing_file_succeeds() {
let (mut disp, mut cpu, mut bus) = setup();
disp.vfs.insert("OldName.dat".to_string(), vec![0xAA, 0xBB]);
let pb = 0x271000u32;
let old_name_addr = pb + 0x100;
let new_name_addr = pb + 0x200;
write_pstring(&mut bus, old_name_addr, b"OldName.dat");
write_pstring(&mut bus, new_name_addr, b"NewName.dat");
for i in 0u32..64 {
bus.write_byte(pb + i, 0);
}
bus.write_long(pb + 18, old_name_addr); // ioNamePtr
bus.write_long(pb + 28, new_name_addr); // ioMisc — new name
cpu.write_reg(Register::A0, pb);
call(&mut disp, false, 0x0B, &mut cpu, &mut bus).unwrap();
assert_eq!(cpu.read_reg(Register::D0) as i32, 0, "rename → noErr");
assert_eq!(bus.read_word(pb + 16) as i16, 0);
assert!(
!disp.vfs.contains_key("OldName.dat"),
"old key must be gone"
);
assert_eq!(disp.vfs.get("NewName.dat"), Some(&vec![0xAA, 0xBBu8]));
}
#[test]
fn pbsetflock_existing_file_returns_noerr_and_sets_ioflattrib_locked_bit() {
// Inside Macintosh: Files (1992), pp. 2-89 and 2-110:
// PBSetFLock locks a file; PBGetFInfo reports lock state via ioFlAttrib bit 0.
let (mut disp, mut cpu, mut bus) = setup();
disp.vfs.insert("Lockable.dat".to_string(), vec![]);
let pb = 0x272000u32;
for i in 0u32..64 {
bus.write_byte(pb + i, 0);
}
setup_param_block(&mut bus, &mut cpu, pb, b"Lockable.dat");
bus.write_word(pb + 16, 0x7B7B); // ioResult poison
call(&mut disp, false, 0x41, &mut cpu, &mut bus).unwrap();
assert_eq!(cpu.read_reg(Register::D0) as i32, 0, "SetFLock noErr in D0");
assert_eq!(
bus.read_word(pb + 16) as i16,
0,
"SetFLock noErr in ioResult"
);
for i in 0u32..64 {
bus.write_byte(pb + i, 0);
}
setup_param_block(&mut bus, &mut cpu, pb, b"Lockable.dat");
bus.write_byte(pb + 30, 0xFF); // ioFlAttrib poison
call(&mut disp, false, 0x0C, &mut cpu, &mut bus).unwrap();
assert_eq!(cpu.read_reg(Register::D0) as i32, 0, "PBGetFInfo noErr");
assert_eq!(
bus.read_byte(pb + 30) & 0x01,
0x01,
"ioFlAttrib bit 0 set after PBSetFLock"
);
}
#[test]
fn pbrstflock_existing_file_returns_noerr_and_clears_ioflattrib_locked_bit() {
// Inside Macintosh: Files (1992), pp. 2-89 and 2-111:
// PBRstFLock unlocks a file; PBGetFInfo reports lock state via ioFlAttrib bit 0.
let (mut disp, mut cpu, mut bus) = setup();
disp.vfs.insert("Lockable.dat".to_string(), vec![]);
let pb = 0x272100u32;
setup_param_block(&mut bus, &mut cpu, pb, b"Lockable.dat");
call(&mut disp, false, 0x41, &mut cpu, &mut bus).unwrap();
for i in 0u32..64 {
bus.write_byte(pb + i, 0);
}
setup_param_block(&mut bus, &mut cpu, pb, b"Lockable.dat");
bus.write_word(pb + 16, 0x7777); // ioResult poison
call(&mut disp, false, 0x42, &mut cpu, &mut bus).unwrap();
assert_eq!(cpu.read_reg(Register::D0) as i32, 0, "RstFLock noErr in D0");
assert_eq!(
bus.read_word(pb + 16) as i16,
0,
"RstFLock noErr in ioResult"
);
for i in 0u32..64 {
bus.write_byte(pb + i, 0);
}
setup_param_block(&mut bus, &mut cpu, pb, b"Lockable.dat");
bus.write_byte(pb + 30, 0xFF); // ioFlAttrib poison
call(&mut disp, false, 0x0C, &mut cpu, &mut bus).unwrap();
assert_eq!(cpu.read_reg(Register::D0) as i32, 0, "PBGetFInfo noErr");
assert_eq!(
bus.read_byte(pb + 30) & 0x01,
0x00,
"ioFlAttrib bit 0 clear after PBRstFLock"
);
}
#[test]
fn pbsetflock_missing_file_returns_fnferr_in_d0_and_ioresult() {
// Inside Macintosh: Files (1992), p. 2-110:
// PBSetFLock returns fnfErr (-43) when the target file is missing.
let (mut disp, mut cpu, mut bus) = setup();
let pb = 0x272200u32;
setup_param_block(&mut bus, &mut cpu, pb, b"MissingLockable.dat");
bus.write_word(pb + 16, 0x2222); // ioResult poison
call(&mut disp, false, 0x41, &mut cpu, &mut bus).unwrap();
assert_eq!(cpu.read_reg(Register::D0) as i32, -43, "fnfErr in D0");
assert_eq!(bus.read_word(pb + 16) as i16, -43, "fnfErr in ioResult");
}
#[test]
fn pbrstflock_missing_file_returns_fnferr_in_d0_and_ioresult() {
// Inside Macintosh: Files (1992), p. 2-111:
// PBRstFLock returns fnfErr (-43) when the target file is missing.
let (mut disp, mut cpu, mut bus) = setup();
let pb = 0x272300u32;
setup_param_block(&mut bus, &mut cpu, pb, b"MissingLockable.dat");
bus.write_word(pb + 16, 0x3333); // ioResult poison
call(&mut disp, false, 0x42, &mut cpu, &mut bus).unwrap();
assert_eq!(cpu.read_reg(Register::D0) as i32, -43, "fnfErr in D0");
assert_eq!(bus.read_word(pb + 16) as i16, -43, "fnfErr in ioResult");
}
#[test]
fn pbsetflock_pbrstflock_write_same_oserr_to_d0_and_ioresult_preserving_stack() {
// Inside Macintosh: Files (1992), pp. 2-110 to 2-111 + Inside
// Macintosh Volume II (1985), p. II-114.
//
// Mirrors B1 + B2 + B3 + B4 of the strict bake fixture
// a041_a042_pbsetflock_pbrstflock_strict: pre-poisons
// pb.ioResult @ pb+16 with 0x3FFF, calls _PBSetFLock then
// _PBRstFLock against bogus filenames not present in the VFS,
// and asserts the dispatcher convention (D0 == ioResult,
// ioResult overwritten away from sentinel) plus the register-
// only ABI (A7 preserved across both calls).
let (mut disp, mut cpu, mut bus) = setup();
// PBSetFLock: bogus filename → fnfErr (-43) per IM:Files; both
// ioResult and D0 receive the same OSErr value.
let pb = 0x272500u32;
setup_param_block(&mut bus, &mut cpu, pb, b"NoSuchFileForSetFLock");
bus.write_word(pb + 16, 0x3FFF); // ioResult pre-poison sentinel
let sp_pre_set = cpu.read_reg(Register::A7);
call(&mut disp, false, 0x41, &mut cpu, &mut bus).unwrap();
let sp_post_set = cpu.read_reg(Register::A7);
let d0_set = cpu.read_reg(Register::D0) as i32;
let io_set = bus.read_word(pb + 16) as i16 as i32;
assert_ne!(
io_set, 0x3FFF,
"PBSetFLock must overwrite ioResult sentinel"
);
assert_eq!(
d0_set, io_set,
"PBSetFLock D0 must mirror ioResult per IM:II II-114"
);
assert_eq!(sp_pre_set, sp_post_set, "PBSetFLock must not consume stack");
// PBRstFLock: same shape, fresh param block.
let pb2 = 0x272600u32;
setup_param_block(&mut bus, &mut cpu, pb2, b"NoSuchFileForRstFLock");
bus.write_word(pb2 + 16, 0x3FFF); // ioResult pre-poison sentinel
let sp_pre_rst = cpu.read_reg(Register::A7);
call(&mut disp, false, 0x42, &mut cpu, &mut bus).unwrap();
let sp_post_rst = cpu.read_reg(Register::A7);
let d0_rst = cpu.read_reg(Register::D0) as i32;
let io_rst = bus.read_word(pb2 + 16) as i16 as i32;
assert_ne!(
io_rst, 0x3FFF,
"PBRstFLock must overwrite ioResult sentinel"
);
assert_eq!(
d0_rst, io_rst,
"PBRstFLock D0 must mirror ioResult per IM:II II-114"
);
assert_eq!(sp_pre_rst, sp_post_rst, "PBRstFLock must not consume stack");
}
#[test]
fn pbhsetflock_pbhrstflock_write_same_oserr_to_d0_and_ioresult_preserving_stack() {
// Inside Macintosh: Files (1992), pp. 2-196 to 2-198 + Inside
// Macintosh Volume II (1985), p. II-114.
//
// Mirrors B1 + B2 + B3 + B4 of the strict bake fixture
// a241_a242_pbhsetflock_pbhrstflock_strict: pre-poisons
// ph.fileParam.ioResult @ pb+16 with 0x3FFF, calls _PBHSetFLock
// then _PBHRstFLock against bogus filenames not present in the
// VFS, and asserts the dispatcher convention (D0 == ioResult,
// ioResult overwritten away from sentinel) plus the register-
// only ABI (A7 preserved across both calls). $A241/$A242 share
// the same low-byte arms (false, 0x41) and (false, 0x42) with
// $A041/$A042 via the OS-trap dispatcher's trap & 0x00FF mask.
let (mut disp, mut cpu, mut bus) = setup();
// PBHSetFLock: bogus filename → fnfErr (-43) per IM:Files; both
// ioResult and D0 receive the same OSErr value.
let pb = 0x272700u32;
setup_param_block(&mut bus, &mut cpu, pb, b"NoSuchFileForHSetFLock");
bus.write_word(pb + 16, 0x3FFF); // ioResult pre-poison sentinel
let sp_pre_set = cpu.read_reg(Register::A7);
call_trap_word(&mut disp, 0xA241, &mut cpu, &mut bus).unwrap();
let sp_post_set = cpu.read_reg(Register::A7);
let d0_set = cpu.read_reg(Register::D0) as i32;
let io_set = bus.read_word(pb + 16) as i16 as i32;
assert_ne!(
io_set, 0x3FFF,
"PBHSetFLock must overwrite ioResult sentinel"
);
assert_eq!(
d0_set, io_set,
"PBHSetFLock D0 must mirror ioResult per IM:II II-114"
);
assert_eq!(
sp_pre_set, sp_post_set,
"PBHSetFLock must not consume stack"
);
// PBHRstFLock: same shape, fresh param block.
let pb2 = 0x272800u32;
setup_param_block(&mut bus, &mut cpu, pb2, b"NoSuchFileForHRstFLock");
bus.write_word(pb2 + 16, 0x3FFF); // ioResult pre-poison sentinel
let sp_pre_rst = cpu.read_reg(Register::A7);
call_trap_word(&mut disp, 0xA242, &mut cpu, &mut bus).unwrap();
let sp_post_rst = cpu.read_reg(Register::A7);
let d0_rst = cpu.read_reg(Register::D0) as i32;
let io_rst = bus.read_word(pb2 + 16) as i16 as i32;
assert_ne!(
io_rst, 0x3FFF,
"PBHRstFLock must overwrite ioResult sentinel"
);
assert_eq!(
d0_rst, io_rst,
"PBHRstFLock D0 must mirror ioResult per IM:II II-114"
);
assert_eq!(
sp_pre_rst, sp_post_rst,
"PBHRstFLock must not consume stack"
);
}
#[test]
fn pbhsetflock_existing_file_returns_noerr_and_sets_ioflattrib_locked_bit() {
// Inside Macintosh: Files (1992), pp. 2-196 to 2-197:
// PBHSetFLock is the HFS variant and trap macro _HSetFLock ($A241).
let (mut disp, mut cpu, mut bus) = setup();
disp.vfs.insert("HLockable.dat".to_string(), vec![]);
let pb = 0x272400u32;
setup_param_block(&mut bus, &mut cpu, pb, b"HLockable.dat");
bus.write_word(pb + 16, 0x4444); // ioResult poison
call_trap_word(&mut disp, 0xA241, &mut cpu, &mut bus).unwrap();
assert_eq!(
cpu.read_reg(Register::D0) as i32,
0,
"PBHSetFLock noErr in D0"
);
assert_eq!(
bus.read_word(pb + 16) as i16,
0,
"PBHSetFLock noErr in ioResult"
);
for i in 0u32..64 {
bus.write_byte(pb + i, 0);
}
setup_param_block(&mut bus, &mut cpu, pb, b"HLockable.dat");
bus.write_byte(pb + 30, 0xFF); // ioFlAttrib poison
call_trap_word(&mut disp, 0xA20C, &mut cpu, &mut bus).unwrap();
assert_eq!(cpu.read_reg(Register::D0) as i32, 0, "PBHGetFInfo noErr");
assert_eq!(
bus.read_byte(pb + 30) & 0x01,
0x01,
"ioFlAttrib bit 0 set after PBHSetFLock"
);
}
#[test]
fn pbhrstflock_existing_file_returns_noerr_and_clears_ioflattrib_locked_bit() {
// Inside Macintosh: Files (1992), pp. 2-197 to 2-198:
// PBHRstFLock is the HFS variant and trap macro _HRstFLock ($A242).
let (mut disp, mut cpu, mut bus) = setup();
disp.vfs.insert("HLockable.dat".to_string(), vec![]);
let pb = 0x272500u32;
setup_param_block(&mut bus, &mut cpu, pb, b"HLockable.dat");
call_trap_word(&mut disp, 0xA241, &mut cpu, &mut bus).unwrap();
for i in 0u32..64 {
bus.write_byte(pb + i, 0);
}
setup_param_block(&mut bus, &mut cpu, pb, b"HLockable.dat");
bus.write_word(pb + 16, 0x5555); // ioResult poison
call_trap_word(&mut disp, 0xA242, &mut cpu, &mut bus).unwrap();
assert_eq!(
cpu.read_reg(Register::D0) as i32,
0,
"PBHRstFLock noErr in D0"
);
assert_eq!(
bus.read_word(pb + 16) as i16,
0,
"PBHRstFLock noErr in ioResult"
);
for i in 0u32..64 {
bus.write_byte(pb + i, 0);
}
setup_param_block(&mut bus, &mut cpu, pb, b"HLockable.dat");
bus.write_byte(pb + 30, 0xFF); // ioFlAttrib poison
call_trap_word(&mut disp, 0xA20C, &mut cpu, &mut bus).unwrap();
assert_eq!(cpu.read_reg(Register::D0) as i32, 0, "PBHGetFInfo noErr");
assert_eq!(
bus.read_byte(pb + 30) & 0x01,
0x00,
"ioFlAttrib bit 0 clear after PBHRstFLock"
);
}
#[test]
fn pbhsetflock_missing_file_returns_fnferr_in_d0_and_ioresult() {
// Inside Macintosh: Files (1992), pp. 2-196 to 2-197:
// PBHSetFLock lists fnfErr (-43) for a missing file.
let (mut disp, mut cpu, mut bus) = setup();
let pb = 0x272600u32;
setup_param_block(&mut bus, &mut cpu, pb, b"MissingHLock.dat");
bus.write_word(pb + 16, 0x6666); // ioResult poison
call_trap_word(&mut disp, 0xA241, &mut cpu, &mut bus).unwrap();
assert_eq!(cpu.read_reg(Register::D0) as i32, -43, "fnfErr in D0");
assert_eq!(bus.read_word(pb + 16) as i16, -43, "fnfErr in ioResult");
}
#[test]
fn pbhrstflock_missing_file_returns_fnferr_in_d0_and_ioresult() {
// Inside Macintosh: Files (1992), pp. 2-197 to 2-198:
// PBHRstFLock lists fnfErr (-43) for a missing file.
let (mut disp, mut cpu, mut bus) = setup();
let pb = 0x272700u32;
setup_param_block(&mut bus, &mut cpu, pb, b"MissingHLock.dat");
bus.write_word(pb + 16, 0x7777); // ioResult poison
call_trap_word(&mut disp, 0xA242, &mut cpu, &mut bus).unwrap();
assert_eq!(cpu.read_reg(Register::D0) as i32, -43, "fnfErr in D0");
assert_eq!(bus.read_word(pb + 16) as i16, -43, "fnfErr in ioResult");
}
#[test]
fn fs_dispatch_pbhopendf_retries_default_directory_for_stale_dirid() {
let (mut disp, mut cpu, mut bus) = setup();
disp.ensure_vfs_directory("Marathon");
disp.vfs.insert("Marathon/Marathon".to_string(), vec![]);
disp.vfs
.insert("Marathon/Physics Model".to_string(), vec![0xAB]);
disp.set_launched_app_path("Marathon/Marathon");
let pb = 0x300000u32;
setup_param_block(&mut bus, &mut cpu, pb, b"Physics Model");
bus.write_long(pb + 48, 0x01FF01FF);
cpu.write_reg(Register::D0, 26);
call(&mut disp, false, 0x60, &mut cpu, &mut bus).unwrap();
assert_eq!(cpu.read_reg(Register::D0), 0);
assert_eq!(bus.read_word(pb + 16), 0);
assert!(bus.read_word(pb + 24) >= 100);
}
// OSDispatch / Process Manager ($A88F) selector contracts.
// Inside Macintosh: Processes (1994), pp. 2-21 to 2-28 and p. 2-31.
#[test]
fn osdispatch_getcurrentprocess_selector_0037_returns_current_psn() {
let (mut disp, mut cpu, mut bus) = setup();
let psn_ptr = 0x2A0000u32;
bus.write_word(TEST_SP, 0x0037);
bus.write_long(TEST_SP + 2, psn_ptr);
call(&mut disp, true, 0x08F, &mut cpu, &mut bus).unwrap();
assert_eq!(bus.read_long(psn_ptr), 0, "highLongOfPSN");
assert_eq!(
bus.read_long(psn_ptr + 4),
2,
"lowLongOfPSN = kCurrentProcess"
);
assert_eq!(bus.read_word(TEST_SP + 6), 0, "noErr result");
assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 6);
}
#[test]
fn osdispatch_getnextprocess_selector_0038_from_knoprocess_returns_first_process() {
let (mut disp, mut cpu, mut bus) = setup();
let psn_ptr = 0x2A0100u32;
bus.write_long(psn_ptr, 0);
bus.write_long(psn_ptr + 4, 0); // kNoProcess
bus.write_word(TEST_SP, 0x0038);
bus.write_long(TEST_SP + 2, psn_ptr);
call(&mut disp, true, 0x08F, &mut cpu, &mut bus).unwrap();
assert_eq!(bus.read_long(psn_ptr), 0);
assert_eq!(bus.read_long(psn_ptr + 4), 2, "first process PSN");
assert_eq!(bus.read_word(TEST_SP + 6), 0, "noErr result");
assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 6);
}
#[test]
fn osdispatch_getnextprocess_selector_0038_end_of_list_returns_procnotfound_and_knoprocess() {
let (mut disp, mut cpu, mut bus) = setup();
let psn_ptr = 0x2A0200u32;
bus.write_long(psn_ptr, 0);
bus.write_long(psn_ptr + 4, 2); // current process PSN
bus.write_word(TEST_SP, 0x0038);
bus.write_long(TEST_SP + 2, psn_ptr);
call(&mut disp, true, 0x08F, &mut cpu, &mut bus).unwrap();
assert_eq!(bus.read_word(TEST_SP + 6) as i16, -600, "procNotFound");
assert_eq!(bus.read_long(psn_ptr), 0);
assert_eq!(bus.read_long(psn_ptr + 4), 0, "kNoProcess at end of list");
assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 6);
}
#[test]
fn osdispatch_getfrontprocess_selector_0039_returns_foreground_psn() {
// MPW GetFrontProcess glue pushes a 4-byte init slot ($FFFFFFFF) before
// psn_ptr. After selector pop: sp+0=init(4B), sp+4=psn_ptr(4B),
// sp+6=result(2B). A7 advances to sp+6 so MOVE.W (SP)+,D0 restores stack.
let (mut disp, mut cpu, mut bus) = setup();
let psn_ptr = 0x2A0300u32;
bus.write_word(TEST_SP, 0x0039);
bus.write_long(TEST_SP + 2, 0xFFFFFFFF); // MPW init slot
bus.write_long(TEST_SP + 6, psn_ptr);
call(&mut disp, true, 0x08F, &mut cpu, &mut bus).unwrap();
assert_eq!(bus.read_long(psn_ptr), 0);
assert_eq!(bus.read_long(psn_ptr + 4), 2, "foreground process PSN");
assert_eq!(bus.read_word(TEST_SP + 8), 0, "noErr result");
assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 8);
}
#[test]
fn osdispatch_getprocessinformation_invalid_psn_returns_procnotfound() {
let (mut disp, mut cpu, mut bus) = setup();
let info_ptr = 0x2A0400u32;
let psn_ptr = 0x2A0500u32;
bus.write_long(psn_ptr, 0);
bus.write_long(psn_ptr + 4, 3); // invalid in single-process HLE
bus.write_word(TEST_SP, 0x003A);
bus.write_long(TEST_SP + 2, info_ptr);
bus.write_long(TEST_SP + 6, psn_ptr);
call(&mut disp, true, 0x08F, &mut cpu, &mut bus).unwrap();
assert_eq!(bus.read_word(TEST_SP + 10) as i16, -600, "procNotFound");
assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 10);
}
#[test]
fn osdispatch_getprocessinformation_current_process_returns_populated_info() {
let (mut disp, mut cpu, mut bus) = setup();
let info_ptr = 0x2A0900u32;
let psn_ptr = 0x2A0A00u32;
let name_ptr = 0x2A0B00u32;
let app_spec_ptr = 0x2A0C00u32;
let expected_psn_high = 0;
let expected_psn_low = 2;
bus.write_long(psn_ptr, expected_psn_high);
bus.write_long(psn_ptr + 4, expected_psn_low);
bus.write_word(info_ptr, 60);
bus.write_long(info_ptr + 4, name_ptr);
bus.write_long(info_ptr + 56, app_spec_ptr);
bus.write_word(TEST_SP, 0x003A);
bus.write_long(TEST_SP + 2, info_ptr);
bus.write_long(TEST_SP + 6, psn_ptr);
call(&mut disp, true, 0x08F, &mut cpu, &mut bus).unwrap();
assert_eq!(bus.read_word(TEST_SP + 10), 0, "noErr");
assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 10);
assert_eq!(bus.read_long(info_ptr + 8), expected_psn_high);
assert_eq!(bus.read_long(info_ptr + 12), expected_psn_low);
assert_eq!(bus.read_long(info_ptr + 24), 0, "processMode");
assert_eq!(bus.read_long(info_ptr + 28), 0x0010_0000, "processLocation");
assert_eq!(bus.read_long(info_ptr + 32), bus.ram_size(), "processSize");
assert_eq!(bus.read_long(info_ptr + 36), bus.ram_size() / 2, "processFreeMem");
assert_eq!(bus.read_long(info_ptr + 40), 0, "processLauncher.highLongOfPSN");
assert_eq!(bus.read_long(info_ptr + 44), 0, "processLauncher.lowLongOfPSN");
assert_eq!(bus.read_long(info_ptr + 48), 0, "processLaunchDate");
assert_ne!(bus.read_byte(name_ptr), 0, "processName should be populated");
assert_ne!(bus.read_word(app_spec_ptr), 0, "processAppSpec.vRefNum");
}
#[test]
fn osdispatch_sameprocess_selector_003d_writes_boolean_result() {
let (mut disp, mut cpu, mut bus) = setup();
let psn1_ptr = 0x2A0600u32;
let psn2_ptr = 0x2A0700u32;
let result_ptr = 0x2A0800u32;
// Equal PSNs => TRUE
bus.write_long(psn1_ptr, 0);
bus.write_long(psn1_ptr + 4, 2);
bus.write_long(psn2_ptr, 0);
bus.write_long(psn2_ptr + 4, 2);
bus.write_byte(result_ptr, 0xFF);
bus.write_word(TEST_SP, 0x003D);
bus.write_long(TEST_SP + 2, result_ptr);
bus.write_long(TEST_SP + 6, psn2_ptr);
bus.write_long(TEST_SP + 10, psn1_ptr);
call(&mut disp, true, 0x08F, &mut cpu, &mut bus).unwrap();
assert_eq!(bus.read_byte(result_ptr), 1, "equal PSNs => TRUE");
assert_eq!(bus.read_word(TEST_SP + 14), 0, "noErr result");
assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 14);
// Different PSNs => FALSE
cpu.write_reg(Register::A7, TEST_SP);
bus.write_long(psn2_ptr + 4, 0);
bus.write_byte(result_ptr, 0xFF);
bus.write_word(TEST_SP, 0x003D);
bus.write_long(TEST_SP + 2, result_ptr);
bus.write_long(TEST_SP + 6, psn2_ptr);
bus.write_long(TEST_SP + 10, psn1_ptr);
call(&mut disp, true, 0x08F, &mut cpu, &mut bus).unwrap();
assert_eq!(bus.read_byte(result_ptr), 0, "different PSNs => FALSE");
assert_eq!(bus.read_word(TEST_SP + 14), 0, "noErr result");
assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 14);
}
}