use crate::loader::LoadedApp;
use crate::managers::resource::ResourceFork;
use crate::memory::MemoryBus;
use crate::runner::{FixtureRunner, FixtureRunnerConfig};
use stuffit::SitArchive;
const WEB_PACK_MAGIC: &[u8; 4] = b"KPK1";
pub const RAM_SIZE: u32 = crate::machine_profile::ORACLE_MACHINE_PROFILE.ram_size_bytes;
pub const MAX_INSTRUCTIONS_PER_FRAME: usize = 2_000_000;
pub fn new_runner() -> FixtureRunner {
FixtureRunner::new(
RAM_SIZE as usize,
FixtureRunnerConfig {
load_address: 0x10000,
max_instructions: MAX_INSTRUCTIONS_PER_FRAME,
..FixtureRunnerConfig::default()
},
)
}
pub fn load_game(runner: &mut FixtureRunner, file_data: &[u8]) -> Result<LoadedApp, String> {
if file_data.starts_with(WEB_PACK_MAGIC) {
load_web_pack(runner, file_data)
} else if is_stuffit_archive(file_data) {
load_stuffit(runner, file_data)
} else if crate::disk_image::looks_like_dc42_or_hfs(file_data) {
load_disk_image(runner, file_data)
} else {
load_macbinary(runner, file_data)
}
}
pub fn pack_stuffit_for_web(file_data: &[u8]) -> Result<Vec<u8>, String> {
let archive =
SitArchive::parse(file_data).map_err(|e| format!("Failed to parse StuffIt: {:?}", e))?;
let file_entries = collect_stuffit_payload_files(&archive)?;
let mut out = Vec::new();
out.extend_from_slice(WEB_PACK_MAGIC);
out.extend_from_slice(&(file_entries.len() as u32).to_be_bytes());
for entry in file_entries {
let name_bytes = entry.name.as_bytes();
if name_bytes.len() > u16::MAX as usize {
return Err(format!(
"Entry name too long for web pack: {} ({} bytes)",
entry.name,
name_bytes.len()
));
}
out.extend_from_slice(&(name_bytes.len() as u16).to_be_bytes());
out.extend_from_slice(name_bytes);
out.extend_from_slice(&entry.file_type);
out.extend_from_slice(&(entry.data.len() as u32).to_be_bytes());
out.extend_from_slice(&entry.data);
out.extend_from_slice(&(entry.rsrc.len() as u32).to_be_bytes());
out.extend_from_slice(&entry.rsrc);
}
Ok(out)
}
pub fn load_game_from_path(
runner: &mut FixtureRunner,
path: &std::path::Path,
) -> Result<LoadedApp, String> {
let file_data =
std::fs::read(path).map_err(|e| format!("Failed to read {}: {}", path.display(), e))?;
if file_data.starts_with(WEB_PACK_MAGIC)
|| is_stuffit_archive(&file_data)
|| crate::disk_image::looks_like_dc42_or_hfs(&file_data)
{
return load_game(runner, &file_data);
}
let rsrc_path = path.join("..namedfork/rsrc");
if let Ok(rsrc_data) = std::fs::read(&rsrc_path) {
if !rsrc_data.is_empty() {
if crate::runner::trace_load_enabled() {
eprintln!("[LOAD] Loading resource fork from {}", rsrc_path.display());
}
let app_name = path
.file_name()
.and_then(|name| name.to_str())
.unwrap_or("FixtureGen");
runner.dispatcher_mut().set_launched_app_path(app_name);
let fork = ResourceFork::parse(&rsrc_data).ok_or("Failed to parse Resource Fork")?;
return runner
.load_app(&fork)
.ok_or_else(|| "Failed to load app".to_string());
}
}
load_game(runner, &file_data)
}
pub fn init_game(runner: &mut FixtureRunner, app: &LoadedApp) {
runner.init_app(app);
{
let (scrn_base, row_bytes, _, scrn_height, _) = runner.dispatcher().screen_mode;
let bus = runner.bus_mut();
for i in 0..(row_bytes * scrn_height as u32) {
bus.write_byte(scrn_base + i, 0xFF);
}
}
}
fn load_stuffit(runner: &mut FixtureRunner, file_data: &[u8]) -> Result<LoadedApp, String> {
let archive =
SitArchive::parse(file_data).map_err(|e| format!("Failed to parse StuffIt: {:?}", e))?;
let mut executable_entry: Option<ExecutableCandidate> = None;
for entry in &archive.entries {
if entry.is_folder {
let normalized = crate::trap::dispatch::TrapDispatcher::normalize_vfs_path(&entry.name);
runner.dispatcher_mut().ensure_vfs_directory(&normalized);
continue;
}
let (data, rsrc) = entry
.decompressed_forks()
.map_err(|e| format!("Decompress error: {:?}", e))?;
let payload = payload_from_forks(
&entry.name,
data,
rsrc,
entry.file_type,
entry.creator,
entry.finder_flags,
)?;
insert_payload_into_vfs(runner, payload, &mut executable_entry);
}
log_vfs(runner);
let executable = executable_entry.ok_or("No executable found in archive")?;
if crate::runner::trace_load_enabled() {
eprintln!("[LOAD] Selected executable: {}", executable.name);
}
runner
.dispatcher_mut()
.set_launched_app_path(&executable.name);
let fork = ResourceFork::parse(&executable.rsrc).ok_or("Failed to parse resource fork")?;
runner
.load_app(&fork)
.ok_or_else(|| "Failed to load app".to_string())
}
fn load_macbinary(runner: &mut FixtureRunner, file_data: &[u8]) -> Result<LoadedApp, String> {
if file_data.len() < 128 {
return Err("File too small for MacBinary".to_string());
}
let data_len =
u32::from_be_bytes([file_data[83], file_data[84], file_data[85], file_data[86]]) as usize;
let rsrc_len =
u32::from_be_bytes([file_data[87], file_data[88], file_data[89], file_data[90]]) as usize;
let data_start = 128;
let data_end_padded = data_start + ((data_len + 127) & !127);
let rsrc_start = data_end_padded;
if rsrc_start + rsrc_len > file_data.len() {
return Err("MacBinary truncated".to_string());
}
let data_end = data_start
.checked_add(data_len)
.ok_or_else(|| "MacBinary data offset overflow".to_string())?;
let data_fork = &file_data[data_start..data_end];
if is_stuffit_archive(data_fork) {
if crate::runner::trace_load_enabled() {
eprintln!("[LOAD] MacBinary data fork contains StuffIt archive");
}
return load_stuffit(runner, data_fork);
}
if crate::disk_image::looks_like_dc42_or_hfs(data_fork) {
if crate::runner::trace_load_enabled() {
eprintln!("[LOAD] MacBinary data fork contains HFS disk image");
}
return load_disk_image(runner, data_fork);
}
if crate::runner::trace_load_enabled() {
eprintln!("[LOAD] Loading from MacBinary format");
}
let name_len = (file_data[1] as usize).min(63);
let name_bytes = &file_data[2..2 + name_len];
let app_name = std::str::from_utf8(name_bytes).unwrap_or("FixtureGen");
runner.dispatcher_mut().set_launched_app_path(app_name);
let rsrc_data = &file_data[rsrc_start..rsrc_start + rsrc_len];
let fork = ResourceFork::parse(rsrc_data).ok_or("Failed to parse resource fork")?;
runner
.load_app(&fork)
.ok_or_else(|| "Failed to load app".to_string())
}
fn load_disk_image(runner: &mut FixtureRunner, file_data: &[u8]) -> Result<LoadedApp, String> {
let image = crate::disk_image::extract_dc42_or_hfs(file_data)?
.ok_or_else(|| "Not a DC42/raw HFS disk image".to_string())?;
let mut executable_entry: Option<ExecutableCandidate> = None;
insert_payload_into_vfs(
runner,
payload_from_disk_image(image)?,
&mut executable_entry,
);
log_vfs(runner);
let executable = executable_entry.ok_or("No executable found in disk image")?;
if crate::runner::trace_load_enabled() {
eprintln!("[LOAD] Selected executable: {}", executable.name);
}
runner
.dispatcher_mut()
.set_launched_app_path(&executable.name);
let fork = ResourceFork::parse(&executable.rsrc).ok_or("Failed to parse resource fork")?;
runner
.load_app(&fork)
.ok_or_else(|| "Failed to load app".to_string())
}
fn load_web_pack(runner: &mut FixtureRunner, file_data: &[u8]) -> Result<LoadedApp, String> {
let mut offset = WEB_PACK_MAGIC.len();
let entry_count = read_u32_be(file_data, &mut offset)? as usize;
let mut executable_entry: Option<ExecutableCandidate> = None;
for _ in 0..entry_count {
let name_len = read_u16_be(file_data, &mut offset)? as usize;
let name_bytes = read_exact(file_data, &mut offset, name_len)?;
let name = String::from_utf8(name_bytes.to_vec())
.map_err(|_| "Invalid UTF-8 in web pack entry name".to_string())?;
let file_type = read_exact(file_data, &mut offset, 4)?;
let mut file_type_code = [0u8; 4];
file_type_code.copy_from_slice(file_type);
let is_appl = file_type_code == *b"APPL";
let data_len = read_u32_be(file_data, &mut offset)? as usize;
let data = read_exact(file_data, &mut offset, data_len)?.to_vec();
let rsrc_len = read_u32_be(file_data, &mut offset)? as usize;
let rsrc = read_exact(file_data, &mut offset, rsrc_len)?.to_vec();
insert_forks_into_vfs(
runner,
&name,
data,
rsrc.clone(),
file_type_code,
*b"????",
0,
);
maybe_select_executable(&mut executable_entry, &name, &rsrc, is_appl, data_len);
}
log_vfs(runner);
let executable = executable_entry.ok_or("No executable found in web pack")?;
if crate::runner::trace_load_enabled() {
eprintln!("[LOAD] Selected executable: {}", executable.name);
}
runner
.dispatcher_mut()
.set_launched_app_path(&executable.name);
let fork = ResourceFork::parse(&executable.rsrc).ok_or("Failed to parse resource fork")?;
runner
.load_app(&fork)
.ok_or_else(|| "Failed to load app".to_string())
}
fn insert_forks_into_vfs(
runner: &mut FixtureRunner,
name: &str,
data: Vec<u8>,
rsrc: Vec<u8>,
file_type: [u8; 4],
creator: [u8; 4],
finder_flags: u16,
) {
let normalized_name = crate::trap::dispatch::TrapDispatcher::normalize_vfs_path(name);
if data.is_empty() && !rsrc.is_empty() && ResourceFork::parse(&rsrc).is_none() {
runner
.dispatcher_mut()
.vfs
.insert(normalized_name.clone(), rsrc.clone());
} else {
runner
.dispatcher_mut()
.vfs
.insert(normalized_name.clone(), data);
}
let data_backed_rsrc = rsrc.is_empty()
&& name.to_ascii_lowercase().ends_with(".rsrc")
&& ResourceFork::parse(
runner
.dispatcher()
.vfs
.get(&normalized_name)
.map_or(&[][..], |bytes| bytes.as_slice()),
)
.is_some();
if !rsrc.is_empty() {
runner
.dispatcher_mut()
.vfs_rsrc
.insert(normalized_name.clone(), rsrc);
} else if data_backed_rsrc {
let data = runner
.dispatcher()
.vfs
.get(&normalized_name)
.cloned()
.unwrap_or_default();
runner
.dispatcher_mut()
.vfs_rsrc
.insert(normalized_name.clone(), data);
}
runner.dispatcher_mut().set_vfs_entry_metadata(
&normalized_name,
file_type,
creator,
finder_flags,
);
}
#[derive(Debug)]
struct Payload {
dirs: Vec<String>,
files: Vec<PayloadFile>,
}
#[derive(Debug)]
struct PayloadFile {
name: String,
data: Vec<u8>,
rsrc: Vec<u8>,
file_type: [u8; 4],
creator: [u8; 4],
finder_flags: u16,
}
fn collect_stuffit_payload_files(archive: &SitArchive) -> Result<Vec<PayloadFile>, String> {
let mut files = Vec::new();
for entry in archive.entries.iter().filter(|entry| !entry.is_folder) {
let (data, rsrc) = entry
.decompressed_forks()
.map_err(|e| format!("Decompress error: {:?}", e))?;
let payload = payload_from_forks(
&entry.name,
data,
rsrc,
entry.file_type,
entry.creator,
entry.finder_flags,
)?;
files.extend(payload.files);
}
Ok(files)
}
fn expand_squz_payload_file(
name: &str,
data: &[u8],
file_type: [u8; 4],
creator: [u8; 4],
finder_flags: u16,
) -> Result<Option<PayloadFile>, String> {
if file_type != *b"SQUZ" || creator != *b"BrSq" {
return Ok(None);
}
let Some(magic_pos) = data.windows(2).position(|window| window == b"KG") else {
return Ok(None);
};
if magic_pos < 12 || magic_pos + 12 > data.len() {
return Err(format!("SQUZ {name}: invalid header"));
}
let method = [data[magic_pos + 2], data[magic_pos + 3]];
let mut target_type = [0u8; 4];
target_type.copy_from_slice(&data[4..8]);
let mut target_creator = [0u8; 4];
target_creator.copy_from_slice(&data[8..12]);
let header_finder_flags = u16::from_be_bytes([data[12], data[13]]);
let target_finder_flags = if header_finder_flags != 0 {
header_finder_flags
} else {
finder_flags
};
let resource_like_file = name.to_ascii_lowercase().ends_with(".rsrc");
let uncompressed_len = u32::from_be_bytes([
data[magic_pos + 4],
data[magic_pos + 5],
data[magic_pos + 6],
data[magic_pos + 7],
]) as usize;
let compressed_len = u32::from_be_bytes([
data[magic_pos + 8],
data[magic_pos + 9],
data[magic_pos + 10],
data[magic_pos + 11],
]) as usize;
let stream_start = magic_pos + 12;
let stream_end = stream_start
.checked_add(compressed_len)
.ok_or_else(|| format!("SQUZ {name}: compressed length overflow"))?;
if stream_end > data.len() {
return Err(format!(
"SQUZ {name}: compressed stream truncated ({} > {})",
stream_end,
data.len()
));
}
let expanded = match method {
[0x00, 0x00] => {
if compressed_len != uncompressed_len {
return Err(format!(
"SQUZ {name}: uncompressed stream length mismatch ({compressed_len} != {uncompressed_len})"
));
}
data[stream_start..stream_end].to_vec()
}
[0x03, 0x03] => {
decode_broderbund_squz_0303_stream(&data[stream_start..stream_end], uncompressed_len)
.map_err(|err| format!("SQUZ {name}: {err}"))?
}
[0x03, 0x04] => {
decode_broderbund_squz_0304_stream(&data[stream_start..stream_end], uncompressed_len)
.map_err(|err| format!("SQUZ {name}: {err}"))?
}
_ => {
if target_type == *b"APPL" {
return Err(format!(
"SQUZ {name}: unsupported KG method {:02X}{:02X}",
method[0], method[1]
));
}
if resource_like_file {
if crate::runner::trace_load_enabled() {
eprintln!(
"[LOAD] Mounting SQUZ \"{}\" as empty resource fork: unsupported KG method {:02X}{:02X}",
name, method[0], method[1]
);
}
return Ok(Some(PayloadFile {
name: name.to_string(),
data: Vec::new(),
rsrc: empty_resource_fork_bytes(),
file_type: target_type,
creator: target_creator,
finder_flags: target_finder_flags,
}));
}
if crate::runner::trace_load_enabled() {
eprintln!(
"[LOAD] Leaving SQUZ \"{}\" packed: unsupported KG method {:02X}{:02X}",
name, method[0], method[1]
);
}
return Ok(None);
}
};
let expanded_is_rsrc = ResourceFork::parse(&expanded).is_some();
if target_type == *b"APPL" && !expanded_is_rsrc {
return Err(format!(
"SQUZ {name}: decoded application resource fork is invalid"
));
}
let rsrc = if expanded_is_rsrc {
expanded.clone()
} else if resource_like_file {
if crate::runner::trace_load_enabled() {
eprintln!(
"[LOAD] Mounting SQUZ \"{}\" as empty resource fork: decoded payload is not a parseable resource fork",
name
);
}
empty_resource_fork_bytes()
} else {
Vec::new()
};
let data = if expanded_is_rsrc || resource_like_file {
Vec::new()
} else {
expanded
};
if crate::runner::trace_load_enabled() {
eprintln!(
"[LOAD] Expanded SQUZ \"{}\" {} -> {} bytes",
name, compressed_len, uncompressed_len
);
}
Ok(Some(PayloadFile {
name: name.to_string(),
data,
rsrc,
file_type: target_type,
creator: target_creator,
finder_flags: target_finder_flags,
}))
}
fn decode_broderbund_squz_0303_stream(
stream: &[u8],
expected_len: usize,
) -> Result<Vec<u8>, String> {
const WINDOW_SIZE: usize = 8192;
const LOOKAHEAD_SIZE: usize = 10;
decode_broderbund_squz_lzss_stream(
stream,
expected_len,
WINDOW_SIZE,
LOOKAHEAD_SIZE,
|first, second| {
let copy_pos = (((first & 0x1F) as usize) << 8) | second as usize;
let copy_len = ((first >> 5) as usize) + 3;
(copy_pos, copy_len)
},
)
}
fn decode_broderbund_squz_0304_stream(
stream: &[u8],
expected_len: usize,
) -> Result<Vec<u8>, String> {
const WINDOW_SIZE: usize = 4096;
const LOOKAHEAD_SIZE: usize = 18;
decode_broderbund_squz_lzss_stream(
stream,
expected_len,
WINDOW_SIZE,
LOOKAHEAD_SIZE,
|first, second| {
let copy_pos = (((first & 0x0F) as usize) << 8) | second as usize;
let copy_len = ((first >> 4) as usize) + 3;
(copy_pos, copy_len)
},
)
}
fn decode_broderbund_squz_lzss_stream<F>(
stream: &[u8],
expected_len: usize,
window_size: usize,
lookahead_size: usize,
decode_ref: F,
) -> Result<Vec<u8>, String>
where
F: Fn(u8, u8) -> (usize, usize),
{
let mut window = vec![0u8; window_size];
let window_mask = window_size - 1;
let mut write_pos = window_size - lookahead_size;
let mut out = Vec::with_capacity(expected_len);
let mut pos = 0usize;
while pos < stream.len() && out.len() < expected_len {
let flags = stream[pos];
pos += 1;
for bit in 0..8 {
if out.len() >= expected_len {
break;
}
if (flags & (1 << bit)) != 0 {
let Some(&byte) = stream.get(pos) else {
return Err("literal truncated".to_string());
};
pos += 1;
out.push(byte);
window[write_pos] = byte;
write_pos = (write_pos + 1) & window_mask;
} else {
if pos + 1 >= stream.len() {
return Err("back-reference truncated".to_string());
}
let first = stream[pos];
let second = stream[pos + 1];
pos += 2;
let (copy_pos, copy_len) = decode_ref(first, second);
for i in 0..copy_len {
if out.len() >= expected_len {
break;
}
let byte = window[(copy_pos + i) & window_mask];
out.push(byte);
window[write_pos] = byte;
write_pos = (write_pos + 1) & window_mask;
}
}
}
}
if out.len() != expected_len {
return Err(format!(
"decoded {} bytes, expected {}",
out.len(),
expected_len
));
}
Ok(out)
}
fn empty_resource_fork_bytes() -> Vec<u8> {
let data_offset = 16u32;
let data_length = 0u32;
let map_offset = 16u32;
let map_length = 32u32;
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 map_start = map_offset as usize;
bytes[map_start..map_start + 16].copy_from_slice(&header);
bytes[map_start + 24..map_start + 26].copy_from_slice(&30u16.to_be_bytes());
bytes[map_start + 26..map_start + 28].copy_from_slice(&32u16.to_be_bytes());
bytes[map_start + 28..map_start + 30].copy_from_slice(&0xFFFFu16.to_be_bytes());
bytes
}
fn payload_from_forks(
name: &str,
data: Vec<u8>,
rsrc: Vec<u8>,
file_type: [u8; 4],
creator: [u8; 4],
finder_flags: u16,
) -> Result<Payload, String> {
if let Some(file) = expand_squz_payload_file(name, &data, file_type, creator, finder_flags)? {
return Ok(Payload {
dirs: Vec::new(),
files: vec![file],
});
}
if let Some(image) = crate::disk_image::extract_dc42_or_hfs(&data)
.map_err(|e| format!("Disk image {name}: {e}"))?
{
if crate::runner::trace_load_enabled() {
eprintln!(
"[LOAD] Extracting HFS disk image \"{}\" from data fork: volume \"{}\", {} files",
name,
image.volume_name,
image.files.len()
);
}
return payload_from_disk_image(image);
}
if let Some(image) = crate::disk_image::extract_dc42_or_hfs(&rsrc)
.map_err(|e| format!("Disk image {name}: {e}"))?
{
if crate::runner::trace_load_enabled() {
eprintln!(
"[LOAD] Extracting HFS disk image \"{}\" from resource fork: volume \"{}\", {} files",
name,
image.volume_name,
image.files.len()
);
}
return payload_from_disk_image(image);
}
Ok(Payload {
dirs: Vec::new(),
files: vec![PayloadFile {
name: name.to_string(),
data,
rsrc,
file_type,
creator,
finder_flags,
}],
})
}
fn payload_from_disk_image(image: crate::disk_image::DiskImageContents) -> Result<Payload, String> {
let mut files = Vec::new();
for file in image.files {
if let Some(expanded) = expand_squz_payload_file(
&file.path,
&file.data,
file.file_type,
file.creator,
file.finder_flags,
)? {
files.push(expanded);
} else {
files.push(PayloadFile {
name: file.path,
data: file.data,
rsrc: file.rsrc,
file_type: file.file_type,
creator: file.creator,
finder_flags: file.finder_flags,
});
}
}
Ok(Payload {
dirs: image.dirs,
files,
})
}
fn insert_payload_into_vfs(
runner: &mut FixtureRunner,
payload: Payload,
executable_entry: &mut Option<ExecutableCandidate>,
) {
for dir in payload.dirs {
let normalized = crate::trap::dispatch::TrapDispatcher::normalize_vfs_path(&dir);
runner.dispatcher_mut().ensure_vfs_directory(&normalized);
}
for file in payload.files {
let data_len = file.data.len();
let is_appl = file.file_type == *b"APPL";
let rsrc = file.rsrc;
insert_forks_into_vfs(
runner,
&file.name,
file.data,
rsrc.clone(),
file.file_type,
file.creator,
file.finder_flags,
);
maybe_select_executable(executable_entry, &file.name, &rsrc, is_appl, data_len);
}
}
fn maybe_select_executable(
executable_entry: &mut Option<ExecutableCandidate>,
name: &str,
rsrc: &[u8],
is_appl: bool,
data_len: usize,
) {
if rsrc.is_empty() {
return;
}
let is_executable = if let Some(fork) = ResourceFork::parse(rsrc) {
fork.get_code(0).is_some()
} else {
false
};
if !is_executable {
return;
}
let override_match = executable_name_override()
.map(|needle| name.contains(needle.as_str()))
.unwrap_or(false);
let prev_override_match = match (executable_entry.as_ref(), executable_name_override()) {
(Some(prev), Some(needle)) => prev.name.contains(needle.as_str()),
_ => false,
};
let candidate = ExecutableCandidate {
name: name.to_string(),
rsrc: rsrc.to_vec(),
is_appl,
has_data_fork: data_len > 0,
score: data_len.max(rsrc.len()),
};
let take = if override_match && !prev_override_match {
true
} else if !override_match && prev_override_match {
false
} else {
match executable_entry.as_ref() {
Some(prev) => candidate.selection_key() > prev.selection_key(),
None => true,
}
};
if take {
*executable_entry = Some(candidate);
}
}
fn executable_name_override() -> Option<String> {
std::env::var("SYSTEMLESS_LOAD_EXECUTABLE")
.ok()
.filter(|s| !s.is_empty())
}
#[derive(Clone, Debug)]
struct ExecutableCandidate {
name: String,
rsrc: Vec<u8>,
is_appl: bool,
has_data_fork: bool,
score: usize,
}
impl ExecutableCandidate {
fn selection_key(&self) -> (bool, bool, usize) {
(self.is_appl, self.has_data_fork, self.score)
}
}
fn is_stuffit_archive(bytes: &[u8]) -> bool {
bytes.len() >= 80 && (&bytes[0..4] == b"SIT!" || &bytes[0..7] == b"StuffIt")
}
fn log_vfs(runner: &FixtureRunner) {
if !crate::runner::trace_load_enabled() {
return;
}
eprintln!("[VFS] Data fork entries:");
for key in runner.dispatcher().vfs.keys() {
let size = runner
.dispatcher()
.vfs
.get(key)
.map(|v| v.len())
.unwrap_or(0);
eprintln!(" \"{}\" ({} bytes)", key, size);
}
eprintln!("[VFS] Resource fork entries:");
for key in runner.dispatcher().vfs_rsrc.keys() {
let size = runner
.dispatcher()
.vfs_rsrc
.get(key)
.map(|v| v.len())
.unwrap_or(0);
eprintln!(" \"{}\" ({} bytes)", key, size);
}
}
fn read_exact<'a>(buf: &'a [u8], offset: &mut usize, len: usize) -> Result<&'a [u8], String> {
let end = offset
.checked_add(len)
.ok_or_else(|| "Web pack offset overflow".to_string())?;
if end > buf.len() {
return Err("Web pack truncated".to_string());
}
let slice = &buf[*offset..end];
*offset = end;
Ok(slice)
}
fn read_u16_be(buf: &[u8], offset: &mut usize) -> Result<u16, String> {
let bytes = read_exact(buf, offset, 2)?;
Ok(u16::from_be_bytes([bytes[0], bytes[1]]))
}
fn read_u32_be(buf: &[u8], offset: &mut usize) -> Result<u32, String> {
let bytes = read_exact(buf, offset, 4)?;
Ok(u32::from_be_bytes([bytes[0], bytes[1], bytes[2], bytes[3]]))
}
#[cfg(test)]
mod tests {
use super::*;
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 + 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());
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 + 5..ref_list_start + 8].copy_from_slice(&0u32.to_be_bytes()[1..4]);
bytes
}
#[test]
fn executable_selection_prefers_real_data_fork_app_over_larger_manual() {
let manual_rsrc = make_single_resource_fork_bytes(*b"CODE", 0, &[0; 1024]);
let app_rsrc = make_single_resource_fork_bytes(*b"CODE", 0, &[0; 128]);
let mut selected = None;
maybe_select_executable(
&mut selected,
"Marathon/Marathon Manual",
&manual_rsrc,
true,
0,
);
maybe_select_executable(
&mut selected,
"Marathon/Marathon v1.2",
&app_rsrc,
true,
322_352,
);
let selected = selected.expect("expected an executable candidate");
assert_eq!(selected.name, "Marathon/Marathon v1.2");
}
#[test]
fn broderbund_squz_0304_decodes_literals_and_backrefs() {
let stream = [
0xFF, b'A', b'B', b'C', b'D', b'E', b'F', b'G', b'H', 0x00, 0xFF, 0xEE,
];
let decoded = decode_broderbund_squz_0304_stream(&stream, 26).unwrap();
assert_eq!(decoded, b"ABCDEFGHABCDEFGHABCDEFGHAB");
}
#[test]
fn broderbund_squz_0303_uses_8k_window_and_13_bit_offsets() {
let stream = [
0x4E, 0x1F, 0xF5, 0x02, 0x00, 0x01, 0x1F, 0xFA, 0x7F, 0xF3, 0x01,
];
let decoded = decode_broderbund_squz_0303_stream(&stream, 16).unwrap();
assert_eq!(decoded, [0, 0, 0, 2, 0, 1, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1]);
}
#[test]
fn broderbund_squz_uncompressed_payload_becomes_resource_fork() {
let rsrc = make_single_resource_fork_bytes(*b"TEST", 128, b"payload");
let mut file = Vec::new();
file.extend_from_slice(&(rsrc.len() as u32).to_be_bytes());
file.extend_from_slice(b"PLR1");
file.extend_from_slice(b"PLRM");
file.extend_from_slice(&0x0500u16.to_be_bytes());
file.extend_from_slice(&[0; 42]);
file.extend_from_slice(b"KG\0\0");
file.extend_from_slice(&(rsrc.len() as u32).to_be_bytes());
file.extend_from_slice(&(rsrc.len() as u32).to_be_bytes());
file.extend_from_slice(&rsrc);
let expanded = expand_squz_payload_file("AllSounds1.rsrc", &file, *b"SQUZ", *b"BrSq", 0)
.unwrap()
.unwrap();
assert!(expanded.data.is_empty());
assert_eq!(expanded.rsrc, rsrc);
assert_eq!(expanded.file_type, *b"PLR1");
assert_eq!(expanded.creator, *b"PLRM");
assert_eq!(expanded.finder_flags, 0x0500);
assert!(ResourceFork::parse(&expanded.rsrc).is_some());
}
#[test]
fn broderbund_squz_compressed_non_resource_payload_stays_data_fork() {
let stream = [0xFF, b'H', b'e', b'l', b'l', b'o'];
let mut file = Vec::new();
file.extend_from_slice(&5u32.to_be_bytes());
file.extend_from_slice(b"TEXT");
file.extend_from_slice(b"PLRM");
file.extend_from_slice(&0x0100u16.to_be_bytes());
file.extend_from_slice(&[0; 42]);
file.extend_from_slice(b"KG\x03\x03");
file.extend_from_slice(&5u32.to_be_bytes());
file.extend_from_slice(&(stream.len() as u32).to_be_bytes());
file.extend_from_slice(&stream);
let expanded = expand_squz_payload_file("PR Scrapbook", &file, *b"SQUZ", *b"BrSq", 0)
.unwrap()
.unwrap();
assert_eq!(expanded.data, b"Hello");
assert!(expanded.rsrc.is_empty());
assert_eq!(expanded.file_type, *b"TEXT");
assert_eq!(expanded.creator, *b"PLRM");
assert_eq!(expanded.finder_flags, 0x0100);
}
#[test]
fn empty_resource_fork_is_parseable() {
assert!(ResourceFork::parse(&empty_resource_fork_bytes()).is_some());
}
#[test]
fn broderbund_squz_unparseable_rsrc_payload_mounts_empty_resource_fork() {
let stream = [0xFF, b'H', b'e', b'l', b'l', b'o'];
let mut file = Vec::new();
file.extend_from_slice(&5u32.to_be_bytes());
file.extend_from_slice(b"PLR2");
file.extend_from_slice(b"PLRM");
file.extend_from_slice(&0x0500u16.to_be_bytes());
file.extend_from_slice(&[0; 42]);
file.extend_from_slice(b"KG\x03\x03");
file.extend_from_slice(&5u32.to_be_bytes());
file.extend_from_slice(&(stream.len() as u32).to_be_bytes());
file.extend_from_slice(&stream);
let expanded = expand_squz_payload_file("ABCBook.rsrc", &file, *b"SQUZ", *b"BrSq", 0)
.unwrap()
.unwrap();
assert!(expanded.data.is_empty());
assert!(ResourceFork::parse(&expanded.rsrc).is_some());
}
}