Skip to main content

systemless/loader/
mod.rs

1//! 68k loader data types: CODE 0 header, jump table entries, and the
2//! [`LoadedApp`] state record returned by
3//! [`FixtureRunner::load_app`](crate::runner::FixtureRunner::load_app).
4
5use std::collections::HashMap;
6
7/// Application `'SIZE'` resource data used by the Process Manager to
8/// choose the app's launch partition. The standard application resource
9/// is ID -1 and stores a 16-bit mode flag word followed by preferred
10/// and minimum partition sizes.
11#[derive(Debug, Clone, Copy, PartialEq, Eq)]
12pub struct ApplicationSizeResource {
13    pub flags: u16,
14    pub preferred_size: u32,
15    pub minimum_size: u32,
16}
17
18impl ApplicationSizeResource {
19    pub fn parse(data: &[u8]) -> Option<Self> {
20        if data.len() < 10 {
21            return None;
22        }
23        Some(Self {
24            flags: u16::from_be_bytes([data[0], data[1]]),
25            preferred_size: u32::from_be_bytes([data[2], data[3], data[4], data[5]]),
26            minimum_size: u32::from_be_bytes([data[6], data[7], data[8], data[9]]),
27        })
28    }
29
30    pub fn preferred_partition_size(self) -> Option<u32> {
31        if self.preferred_size >= self.minimum_size && self.preferred_size >= 128 * 1024 {
32            Some(self.preferred_size)
33        } else {
34            None
35        }
36    }
37}
38
39/// CODE 0 resource header — 16 bytes parsed from the start of every
40/// 68k application's `CODE` resource ID 0. Defines the A5-world layout
41/// (above + below sizes) and where the jump table lives within it.
42/// Inside Macintosh: Memory 1992, 7-31 ("CODE Resource Format").
43#[derive(Debug, Clone, Default)]
44pub struct Code0Header {
45    /// Bytes of A5-world space above A5 (application globals, not
46    /// counting the jump table itself).
47    pub above_a5: u32,
48    /// Bytes of A5-world space below A5 (parameter area + initial SP).
49    pub below_a5: u32,
50    /// Total size in bytes of the jump table region (8 bytes per entry).
51    pub jump_table_size: u32,
52    /// Byte offset from A5 to the jump table base (typically 32).
53    pub jump_table_offset: u32,
54}
55
56impl Code0Header {
57    /// Parse a 16-byte CODE 0 header from `data` (4 big-endian
58    /// `u32` fields). Returns `None` if `data` is shorter than 16
59    /// bytes; otherwise infallible.
60    pub fn parse(data: &[u8]) -> Option<Self> {
61        if data.len() < 16 {
62            return None;
63        }
64        Some(Self {
65            above_a5: u32::from_be_bytes([data[0], data[1], data[2], data[3]]),
66            below_a5: u32::from_be_bytes([data[4], data[5], data[6], data[7]]),
67            jump_table_size: u32::from_be_bytes([data[8], data[9], data[10], data[11]]),
68            jump_table_offset: u32::from_be_bytes([data[12], data[13], data[14], data[15]]),
69        })
70    }
71
72    /// Number of jump-table entries (each entry is 8 bytes).
73    pub fn num_entries(&self) -> usize {
74        (self.jump_table_size / 8) as usize
75    }
76}
77
78/// One slot in the application's jump table. The Mac OS Segment Loader
79/// patches each slot's `loaded` + `address` lazily as `LoadSeg` faults
80/// pull CODE segments into memory.
81#[derive(Debug, Clone)]
82pub struct JumpTableEntry {
83    /// Byte offset within the target segment of the call destination.
84    pub offset: u16,
85    /// CODE resource ID containing the call destination.
86    pub segment: i16,
87    /// True once the segment has been loaded and the slot patched.
88    pub loaded: bool,
89    /// Resolved guest address of the call destination (valid when
90    /// `loaded == true`).
91    pub address: u32,
92}
93
94/// Header stored at the front of each nonzero `CODE` resource.
95///
96/// MPW-style near segments use the documented `tabOff, nEntries`
97/// format. Symantec/THINK far CODE uses the same four bytes differently:
98/// word 0 stores the first jump-table entry index plus the relocation
99/// flag, and word 1 has bit `$4000` set plus the entry count.
100#[derive(Debug, Clone, Copy, PartialEq, Eq)]
101pub enum CodeSegmentHeader {
102    /// MPW far-model segment with a 40-byte header (`$FFFF` marker).
103    MpwFar,
104    /// Standard near-model segment: byte offset from the current jump
105    /// table base, plus number of entries owned by the segment.
106    Near { table_offset: u16, entry_count: u16 },
107    /// Symantec/THINK far CODE segment.
108    ThinkFar {
109        has_relocations: bool,
110        first_entry_index: u16,
111        entry_count: u16,
112    },
113}
114
115impl CodeSegmentHeader {
116    const THINK_RELOC_FLAG: u16 = 0x8000;
117    const THINK_FAR_FLAG: u16 = 0x4000;
118
119    pub fn parse(data: &[u8]) -> Option<Self> {
120        if data.len() < 4 {
121            return None;
122        }
123
124        let first = u16::from_be_bytes([data[0], data[1]]);
125        let second = u16::from_be_bytes([data[2], data[3]]);
126        Some(Self::from_words(first, second))
127    }
128
129    pub fn from_words(first: u16, second: u16) -> Self {
130        if first == 0xFFFF {
131            Self::MpwFar
132        } else if (second & Self::THINK_FAR_FLAG) != 0 {
133            Self::ThinkFar {
134                has_relocations: (first & Self::THINK_RELOC_FLAG) != 0,
135                first_entry_index: first & !Self::THINK_RELOC_FLAG,
136                entry_count: second & 0x3FFF,
137            }
138        } else {
139            Self::Near {
140                table_offset: first,
141                entry_count: second,
142            }
143        }
144    }
145
146    pub fn code_header_size(self) -> u32 {
147        match self {
148            Self::MpwFar => 40,
149            Self::Near { .. } | Self::ThinkFar { .. } => 4,
150        }
151    }
152
153    pub fn jump_table_start_offset(self) -> Option<u32> {
154        match self {
155            Self::MpwFar => None,
156            Self::Near { table_offset, .. } => Some(table_offset as u32),
157            Self::ThinkFar {
158                first_entry_index, ..
159            } => Some(first_entry_index as u32 * 8),
160        }
161    }
162
163    pub fn jump_table_entry_count(self) -> Option<u32> {
164        match self {
165            Self::MpwFar => None,
166            Self::Near { entry_count, .. } | Self::ThinkFar { entry_count, .. } => {
167                Some(entry_count as u32)
168            }
169        }
170    }
171}
172
173#[cfg(test)]
174mod tests {
175    use super::{ApplicationSizeResource, CodeSegmentHeader};
176
177    #[test]
178    fn parses_application_size_resource_flags_and_partition_sizes() {
179        let bytes = [0x51, 0x80, 0x00, 0x30, 0x00, 0x00, 0x00, 0x20, 0x00, 0x00];
180        let size = ApplicationSizeResource::parse(&bytes).expect("SIZE resource should parse");
181
182        assert_eq!(size.flags, 0x5180);
183        assert_eq!(size.preferred_size, 0x0030_0000);
184        assert_eq!(size.minimum_size, 0x0020_0000);
185        assert_eq!(size.preferred_partition_size(), Some(0x0030_0000));
186    }
187
188    #[test]
189    fn rejects_truncated_or_inverted_application_size_resources() {
190        assert!(ApplicationSizeResource::parse(&[0; 9]).is_none());
191
192        let inverted = ApplicationSizeResource {
193            flags: 0,
194            preferred_size: 0x0010_0000,
195            minimum_size: 0x0020_0000,
196        };
197        assert_eq!(inverted.preferred_partition_size(), None);
198    }
199
200    #[test]
201    fn parses_think_far_header_entry_index_and_count_flags() {
202        let header = CodeSegmentHeader::from_words(0x8051, 0x4085);
203
204        assert_eq!(
205            header,
206            CodeSegmentHeader::ThinkFar {
207                has_relocations: true,
208                first_entry_index: 0x0051,
209                entry_count: 0x0085,
210            }
211        );
212        assert_eq!(header.code_header_size(), 4);
213        assert_eq!(header.jump_table_start_offset(), Some(0x0051 * 8));
214        assert_eq!(header.jump_table_entry_count(), Some(0x0085));
215    }
216
217    #[test]
218    fn parses_near_and_mpw_far_segment_headers() {
219        let near = CodeSegmentHeader::from_words(0x0018, 0x0002);
220        assert_eq!(
221            near,
222            CodeSegmentHeader::Near {
223                table_offset: 0x0018,
224                entry_count: 2,
225            }
226        );
227        assert_eq!(near.code_header_size(), 4);
228        assert_eq!(near.jump_table_start_offset(), Some(0x0018));
229        assert_eq!(near.jump_table_entry_count(), Some(2));
230
231        let mpw_far = CodeSegmentHeader::from_words(0xFFFF, 0x0000);
232        assert_eq!(mpw_far, CodeSegmentHeader::MpwFar);
233        assert_eq!(mpw_far.code_header_size(), 40);
234        assert_eq!(mpw_far.jump_table_start_offset(), None);
235        assert_eq!(mpw_far.jump_table_entry_count(), None);
236    }
237}
238
239/// State produced by loading a 68k application: parsed CODE 0 header,
240/// resolved A5 placement, jump-table slot vector, per-segment load
241/// addresses, and the initial stack pointer the runner will seed.
242///
243/// Returned by
244/// [`FixtureRunner::load_app`](crate::runner::FixtureRunner::load_app)
245/// and consumed by
246/// [`FixtureRunner::init_app`](crate::runner::FixtureRunner::init_app).
247#[derive(Default)]
248pub struct LoadedApp {
249    /// Parsed CODE 0 header bytes (above_a5 / below_a5 / jt_size / jt_offset).
250    pub code0_header: Code0Header,
251    /// Guest address chosen for A5; A5-relative globals + jump table
252    /// are placed relative to this base.
253    pub a5_base: u32,
254    /// Materialised jump-table slot vector; one entry per CODE call site.
255    pub jump_table: Vec<JumpTableEntry>,
256    /// Map from CODE resource ID to the guest address where each
257    /// segment was loaded.
258    pub segment_bases: HashMap<i16, u32>,
259    /// Initial stack pointer (top of below-A5 region) the runner
260    /// seeds A7 with before the first instruction.
261    pub initial_sp: u32,
262    /// Parsed application `'SIZE'` resource ID -1, when present.
263    pub size_resource: Option<ApplicationSizeResource>,
264}
265
266impl LoadedApp {
267    pub fn entry_point(&self, a5_base: u32) -> u32 {
268        a5_base + self.code0_header.jump_table_offset + 2
269    }
270}