Skip to content

Commit 40bef11

Browse files
committed
Add unit test framework for apple2 and initial test for fn_clock
1 parent e008e0b commit 40bef11

File tree

11 files changed

+564
-13
lines changed

11 files changed

+564
-13
lines changed

apple2/apple2-6502/fn_clock/clock_get_time.s

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ time_format = *-1
4646
; ensure the value is valid
4747
cpx #$06
4848
bcs error
49-
pusha tmp1
49+
jsr pusha
5050
lda code_table, x
5151

5252
jsr _sp_status

scripts/as_info.py

Lines changed: 298 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,298 @@
1+
#!/usr/bin/env python3
2+
# displays information about the specified apple single file.
3+
4+
import sys
5+
import struct
6+
from typing import List, Union, Tuple, Optional
7+
from dataclasses import dataclass
8+
from enum import IntEnum
9+
from datetime import datetime
10+
11+
class ASEntryType(IntEnum):
12+
DATA_FORK = 1
13+
RESOURCE_FORK = 2
14+
REAL_NAME = 3
15+
COMMENT = 4
16+
ICON_BW = 5
17+
ICON_COLOR = 6
18+
FILE_DATES_INFO = 8
19+
FINDER_INFO = 9
20+
MACINTOSH_FILE_INFO = 10
21+
PRODOS_FILE_INFO = 11
22+
MSDOS_FILE_INFO = 12
23+
AFP_SHORT_NAME = 13
24+
AFP_FILE_INFO = 14
25+
AFP_DIRECTORY_ID = 15
26+
27+
@dataclass
28+
class Point:
29+
x: int
30+
y: int
31+
32+
@dataclass
33+
class FinderInfo:
34+
file_type: bytes
35+
file_creator: bytes
36+
flags: int
37+
location: Point
38+
folder_id: int
39+
40+
@dataclass
41+
class ProDosInfo:
42+
access: int
43+
file_type: int
44+
auxiliary_type: int
45+
46+
@dataclass
47+
class DataFork:
48+
data: bytes
49+
50+
@dataclass
51+
class ResourceFork:
52+
data: bytes
53+
54+
@dataclass
55+
class RealName:
56+
name: str
57+
58+
@dataclass
59+
class Comment:
60+
text: str
61+
62+
@dataclass
63+
class IconBW:
64+
data: bytes
65+
66+
@dataclass
67+
class IconColor:
68+
data: bytes
69+
70+
@dataclass
71+
class FileDatesInfo:
72+
creation_date: datetime
73+
modification_date: datetime
74+
backup_date: datetime
75+
access_date: datetime
76+
77+
@dataclass
78+
class MacintoshFileInfo:
79+
data: bytes
80+
81+
@dataclass
82+
class MSDOSFileInfo:
83+
data: bytes
84+
85+
@dataclass
86+
class AFPShortName:
87+
name: str
88+
89+
@dataclass
90+
class AFPFileInfo:
91+
data: bytes
92+
93+
@dataclass
94+
class AFPDirectoryID:
95+
id: int
96+
97+
class AppleSingle:
98+
def __init__(self, file_path: str):
99+
with open(file_path, 'rb') as f:
100+
self.bytes = f.read()
101+
102+
self.entries: List[Union[DataFork, ResourceFork, RealName, Comment, IconBW,
103+
IconColor, FileDatesInfo, FinderInfo, MacintoshFileInfo,
104+
ProDosInfo, MSDOSFileInfo, AFPShortName, AFPFileInfo,
105+
AFPDirectoryID]] = []
106+
self.load_address: int = 0
107+
108+
self._parse()
109+
110+
def _parse(self):
111+
# Check magic number (00051600 or 00051607)
112+
if len(self.bytes) < 4:
113+
raise ValueError("File too short for magic number")
114+
115+
magic = struct.unpack('>I', self.bytes[0:4])[0]
116+
if magic not in [0x00051600, 0x00051607]:
117+
header_hex = self.bytes[0:4].hex()
118+
raise ValueError(f"Invalid magic number: {header_hex}")
119+
120+
# Check version number (00020000)
121+
if len(self.bytes) < 8 or self.bytes[4:8] != b'\x00\x02\x00\x00':
122+
version_hex = self.bytes[4:8].hex()
123+
raise ValueError(f"Unsupported version: {version_hex}")
124+
125+
# Check filler (16 bytes of zeros)
126+
if len(self.bytes) < 24:
127+
raise ValueError("File too short for header")
128+
if any(self.bytes[8:24]):
129+
raise ValueError("Invalid filler bytes")
130+
131+
# Get number of entries (2 bytes, big-endian)
132+
if len(self.bytes) < 26:
133+
raise ValueError("File too short for entry count")
134+
num_entries = struct.unpack('>H', self.bytes[24:26])[0]
135+
136+
# Parse entries
137+
offset = 26 # Start after header
138+
for _ in range(num_entries):
139+
if offset + 12 > len(self.bytes):
140+
raise ValueError("File too short for entry descriptor")
141+
142+
entry_id = struct.unpack('>I', self.bytes[offset:offset+4])[0]
143+
entry_offset = struct.unpack('>I', self.bytes[offset+4:offset+8])[0]
144+
entry_length = struct.unpack('>I', self.bytes[offset+8:offset+12])[0]
145+
146+
if entry_offset + entry_length > len(self.bytes):
147+
raise ValueError(f"Entry extends beyond file: type {entry_id}")
148+
149+
entry_data = self.bytes[entry_offset:entry_offset + entry_length]
150+
151+
try:
152+
entry_type = ASEntryType(entry_id)
153+
except ValueError:
154+
raise ValueError(f"Unsupported entry type: {entry_id}")
155+
156+
if entry_type == ASEntryType.DATA_FORK:
157+
self.entries.append(DataFork(data=entry_data))
158+
159+
elif entry_type == ASEntryType.RESOURCE_FORK:
160+
self.entries.append(ResourceFork(data=entry_data))
161+
162+
elif entry_type == ASEntryType.REAL_NAME:
163+
self.entries.append(RealName(name=entry_data.decode('utf-8')))
164+
165+
elif entry_type == ASEntryType.COMMENT:
166+
self.entries.append(Comment(text=entry_data.decode('utf-8')))
167+
168+
elif entry_type == ASEntryType.ICON_BW:
169+
self.entries.append(IconBW(data=entry_data))
170+
171+
elif entry_type == ASEntryType.ICON_COLOR:
172+
self.entries.append(IconColor(data=entry_data))
173+
174+
elif entry_type == ASEntryType.FILE_DATES_INFO:
175+
if entry_length != 16:
176+
raise ValueError(f"File dates info must be 16 bytes, got {entry_length}")
177+
dates = struct.unpack('>IIII', entry_data)
178+
self.entries.append(FileDatesInfo(
179+
creation_date=datetime.fromtimestamp(dates[0]),
180+
modification_date=datetime.fromtimestamp(dates[1]),
181+
backup_date=datetime.fromtimestamp(dates[2]),
182+
access_date=datetime.fromtimestamp(dates[3])
183+
))
184+
185+
elif entry_type == ASEntryType.FINDER_INFO:
186+
if entry_length != 16:
187+
raise ValueError(f"Finder info must be 16 bytes, got {entry_length}")
188+
finder_info = struct.unpack('>4s4sHHHH', entry_data)
189+
self.entries.append(FinderInfo(
190+
file_type=finder_info[0],
191+
file_creator=finder_info[1],
192+
flags=finder_info[2],
193+
location=Point(x=finder_info[3], y=finder_info[4]),
194+
folder_id=finder_info[5]
195+
))
196+
197+
elif entry_type == ASEntryType.PRODOS_FILE_INFO:
198+
if entry_length != 8:
199+
raise ValueError(f"ProDOS info must be 8 bytes, got {entry_length}")
200+
access, file_type, auxiliary_type = struct.unpack('>HHI', entry_data)
201+
self.load_address = auxiliary_type & 0xFFFF
202+
self.entries.append(ProDosInfo(
203+
access=access,
204+
file_type=file_type,
205+
auxiliary_type=auxiliary_type
206+
))
207+
208+
elif entry_type == ASEntryType.MACINTOSH_FILE_INFO:
209+
self.entries.append(MacintoshFileInfo(data=entry_data))
210+
211+
elif entry_type == ASEntryType.MSDOS_FILE_INFO:
212+
self.entries.append(MSDOSFileInfo(data=entry_data))
213+
214+
elif entry_type == ASEntryType.AFP_SHORT_NAME:
215+
self.entries.append(AFPShortName(name=entry_data.decode('utf-8')))
216+
217+
elif entry_type == ASEntryType.AFP_FILE_INFO:
218+
self.entries.append(AFPFileInfo(data=entry_data))
219+
220+
elif entry_type == ASEntryType.AFP_DIRECTORY_ID:
221+
if entry_length != 4:
222+
raise ValueError(f"AFP directory ID must be 4 bytes, got {entry_length}")
223+
self.entries.append(AFPDirectoryID(id=struct.unpack('>I', entry_data)[0]))
224+
225+
offset += 12
226+
227+
def dump(self):
228+
print(f"AppleSingle, loadAddress: 0x{self.load_address:04x}")
229+
for entry in self.entries:
230+
if isinstance(entry, DataFork):
231+
up_to_7 = min(7, len(entry.data) - 1)
232+
first_8 = ' '.join(f"{b:02x}" for b in entry.data[:up_to_7+1])
233+
print(f" DataForkEntry, len: 0x{len(entry.data):04x}: {first_8}")
234+
235+
elif isinstance(entry, ResourceFork):
236+
up_to_7 = min(7, len(entry.data) - 1)
237+
first_8 = ' '.join(f"{b:02x}" for b in entry.data[:up_to_7+1])
238+
print(f" ResourceForkEntry, len: 0x{len(entry.data):04x}: {first_8}")
239+
240+
elif isinstance(entry, RealName):
241+
print(f" RealName: {entry.name}")
242+
243+
elif isinstance(entry, Comment):
244+
print(f" Comment: {entry.text}")
245+
246+
elif isinstance(entry, IconBW):
247+
print(f" IconBW, len: 0x{len(entry.data):04x}")
248+
249+
elif isinstance(entry, IconColor):
250+
print(f" IconColor, len: 0x{len(entry.data):04x}")
251+
252+
elif isinstance(entry, FileDatesInfo):
253+
print(f" FileDatesInfo:")
254+
print(f" Creation: {entry.creation_date}")
255+
print(f" Modification: {entry.modification_date}")
256+
print(f" Backup: {entry.backup_date}")
257+
print(f" Access: {entry.access_date}")
258+
259+
elif isinstance(entry, FinderInfo):
260+
print(f" FinderInfo:")
261+
print(f" Type: {entry.file_type.decode('ascii', errors='replace')}")
262+
print(f" Creator: {entry.file_creator.decode('ascii', errors='replace')}")
263+
print(f" Flags: 0x{entry.flags:04x}")
264+
print(f" Location: ({entry.location.x}, {entry.location.y})")
265+
print(f" Folder ID: 0x{entry.folder_id:04x}")
266+
267+
elif isinstance(entry, ProDosInfo):
268+
print(f" ProDosEntry, access: 0x{entry.access:04x}, fileType: 0x{entry.file_type:04x}, auxiliaryType: 0x{entry.auxiliary_type:08x}")
269+
270+
elif isinstance(entry, MacintoshFileInfo):
271+
print(f" MacintoshFileInfo, len: 0x{len(entry.data):04x}")
272+
273+
elif isinstance(entry, MSDOSFileInfo):
274+
print(f" MSDOSFileInfo, len: 0x{len(entry.data):04x}")
275+
276+
elif isinstance(entry, AFPShortName):
277+
print(f" AFPShortName: {entry.name}")
278+
279+
elif isinstance(entry, AFPFileInfo):
280+
print(f" AFPFileInfo, len: 0x{len(entry.data):04x}")
281+
282+
elif isinstance(entry, AFPDirectoryID):
283+
print(f" AFPDirectoryID: 0x{entry.id:08x}")
284+
285+
def main():
286+
if len(sys.argv) != 2:
287+
print(f"Usage: {sys.argv[0]} <apple-single-file>")
288+
sys.exit(1)
289+
290+
try:
291+
apple_single = AppleSingle(sys.argv[1])
292+
apple_single.dump()
293+
except Exception as e:
294+
print(f"Error: {e}", file=sys.stderr)
295+
sys.exit(1)
296+
297+
if __name__ == "__main__":
298+
main()

0 commit comments

Comments
 (0)