Skip to content

Commit

Permalink
added asf state file support
Browse files Browse the repository at this point in the history
  • Loading branch information
cnvogelg committed Dec 7, 2024
1 parent 17e796c commit 72c8b5c
Show file tree
Hide file tree
Showing 13 changed files with 474 additions and 1 deletion.
Binary file added amitools/state/.DS_Store
Binary file not shown.
2 changes: 2 additions & 0 deletions amitools/state/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
from .asffile import ASFFile
from .asfparser import ASFParser, MemChunk, MemType
188 changes: 188 additions & 0 deletions amitools/state/asffile.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,188 @@
import os
import io
import struct
import zlib
from dataclasses import dataclass


class BadASFFile(Exception):
pass


@dataclass
class Chunk:
tag: str = None
size: int = 0
flags: int = 0
offset: int = 0
total_size: int = 0
uncompressed_size: int = 0

def read(self, fp):
self.fp = fp
buf = fp.read(8)
raw_tag, size = struct.unpack(">4sI", buf)
self.offset = fp.tell()
self.tag = raw_tag.decode("ascii")
self.size = size - 8
self.uncompressed_size = size
self.flags = 0
if size > 8:
buf = fp.read(4)
self.flags = struct.unpack(">I", buf)[0]
self.offset += 4
self.size -= 4
if self.flags & 1 == 1:
buf = fp.read(4)
self.uncompressed_size = struct.unpack(">I", buf)[0]
self.offset += 4
self.size -= 4

# bogus END: no align?
if self.tag == "END ":
self.total_size = 8
else:
self.total_size = self._align(size) + size

def read_data(self):
if self.size == 0:
return None
if self.fp.tell() != self.offset:
self.fp.seek(self.offset, 0)
data = self.fp.read(self.size)
# align?
align = self._align(self.size)
self.fp.read(align)
# uncompress data?
if self.flags & 1 == 1:
data = zlib.decompress(data)
assert len(data) == self.uncompressed_size
return data

def skip_data(self):
if self.size == 0:
return
# move forward without header
size = self.size + self._align(self.size)
self.fp.seek(size, os.SEEK_CUR)

def _align(self, size):
# bogus alignment: if already aligned on long it adds 4 dummy bytes???
align = 4 - (size & 3)
return align


class ASFFile:
def __init__(self, file, mode="r"):
if mode not in ("r", "w"):
raise ValueError("ASSFile mode must be 'r' or 'w'")
self.file = file
self.fp = None
if isinstance(file, os.PathLike):
file = os.fspath(file)
if isinstance(file, str):
self.filename = file
self.fp = io.open(file, mode + "+b")
else:
self.fp = file
self.filename = getattr(file, "name", None)

if mode == "r":
self.fp.seek(0, os.SEEK_END)
self.total_size = self.fp.tell()
self.fp.seek(0, os.SEEK_SET)
self._read_header()
self.chunks = None

def __enter__(self):
return self

def __exit__(self, type, value, traceback):
self.close()

def __repr__(self):
result = ["<%s.%s" % (self.__class__.__module__, self.__class__.__qualname__)]
if self.fp is not None:
if self._filePassed:
result.append(" file=%r" % self.fp)
elif self.filename is not None:
result.append(" filename=%r" % self.filename)
result.append(" mode=%r" % self.mode)
else:
result.append(" [closed]")
result.append(">")
return "".join(result)

def __del__(self):
self.close()

def close(self):
if self.fp:
self.fp.close()
self.fp = None

def chunklist(self):
"""return list of chunks found in file"""
if not self.chunks:
self._read_chunklist()
return self.chunks

def get_chunk(self, tag):
chunks = self.chunklist()
for chunk in chunks:
if chunk.tag == tag:
return chunk

def get_all_chunks(self, tag):
result = []
chunks = self.chunklist()
for chunk in chunks:
if chunk.tag == tag:
result.append(chunk)
return result

def load_chunk(self, tag):
chunk = self.get_chunk(tag)
if chunk:
return chunk.read_data()

def load_all_chunks(self, tag):
chunks = self.get_all_chunks(tag)
if chunks:
return list(map(lambda x: x.read_data(), chunks))

def _read_chunklist(self):
chunks = []
left = self.total_size
self.fp.seek(self.main_offset, 0)
while left > 0:
chunk = Chunk()
chunk.read(self.fp)
chunk.skip_data()
left -= chunk.total_size
chunks.append(chunk)
self.chunks = chunks

def _read_header(self):
hdr = Chunk()
hdr.read(self.fp)
if hdr.tag != "ASF ":
raise BadASFFile()
data = hdr.read_data()
# skip long
offset = 4
# header
self.emu_name, offset = self._read_string(data, offset)
self.emu_version, offset = self._read_string(data, offset)
self.desc, offset = self._read_string(data, offset)
assert offset == hdr.size
self.total_size -= hdr.total_size
self.main_offset = hdr.total_size

def _read_string(self, buf, off):
data = bytearray()
while buf[off] != 0:
data.append(buf[off])
off += 1
txt = data.decode("utf-8")
return txt, off + 1
115 changes: 115 additions & 0 deletions amitools/state/asfparser.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
from dataclasses import dataclass
from enum import IntEnum
import struct


class MemType(IntEnum):
CHIP = 0
BOGO = 1
Z2RAM = 2
Z3RAM = 3
Z3CHIP = 4
A3KLO = 5
A3KHI = 6


@dataclass
class MemChunk:
address: int
size: int
type: MemType
data: bytes = None


@dataclass
class Expansion:
z2ram_base: int
z3ram_base: int
gfx_base: int
boro_base: int
z2ram_base2: int


class ASFParser:
def __init__(self, asf_file):
self.asf_file = asf_file

def get_expansion(self):
data = self.asf_file.load_chunk("EXPA")
if not data:
return None
assert len(data) >= 5 * 4
z2b, z3b, gfx, boro, z2b2 = struct.unpack(">IIIII", data)
return Expansion(z2b, z3b, gfx, boro, z2b2)

def get_ram_layout(self, load_ram=False):
layout = []
# chip
chip = self._get_mem_chunk("CRAM", MemType.CHIP, load_ram, 0)
if chip:
layout.append(chip)
bogo = self._get_mem_chunk("BRAM", MemType.BOGO, load_ram, 0xC00000)
# slow/bogo ram
if bogo:
layout.append(bogo)
# a3k
a3klo = self._get_mem_chunk("A3K1", MemType.A3KLO, load_ram, 0x08000000)
if a3klo:
# fixup lo address
a3klo.address -= a3klo.size
layout.append(a3klo)
a3khi = self._get_mem_chunk("A3K2", MemType.A3KHI, load_ram, 0x08000000)
if a3khi:
layout.append(a3khi)
# z2ram/z3ram
expansion = self.get_expansion()
if expansion:
# z2ram
z2rams = self._get_all_mem_chunks("FRAM", MemType.Z2RAM, load_ram)
n = len(z2rams)
if n > 0:
z2ram = z2rams[0]
z2ram.address = expansion.z2ram_base
layout.append(z2ram)
if n > 1:
z2ram = z2rams[1]
z2ram.address = expansion.z2ram_base2
layout.append(z2ram)
# z2ram
z3rams = self._get_all_mem_chunks("ZRAM", MemType.Z3RAM, load_ram)
n = len(z3rams)
if n > 0:
z3ram = z3rams[0]
z3ram.address = expansion.z3ram_base
layout.append(z3ram)
return layout

def _get_all_mem_chunks(self, tag, type, load_ram):
result = []
if load_ram:
datas = self.asf_file.load_all_chunks(tag)
if datas:
for data in datas:
size = len(data)
result.append(MemChunk(0, size, type, data))
else:
chunks = self.asf_file.get_all_chunks(tag)
if chunks:
for chunk in chunks:
size = chunk.uncompressed_size
result.append(MemChunk(0, size, type))
return result

def _get_mem_chunk(self, tag, type, load_ram, addr=0):
if load_ram:
data = self.asf_file.load_chunk(tag)
if not data:
return None
size = len(data)
else:
data = None
chunk = self.asf_file.get_chunk(tag)
if not chunk:
return None
size = chunk.uncompressed_size
return MemChunk(addr, size, type, data)
2 changes: 1 addition & 1 deletion amitools/tools/vamostool.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ def main(args=None):
# then in home dir
os.path.expanduser("~/.vamosrc"),
)
tools = [PathTool(), TypeTool(), LibProfilerTool()]
tools = [PathTool(), TypeTool(), LibProfilerTool(), StateTool()]
return tools_main(tools, cfg_files, args)


Expand Down
1 change: 1 addition & 0 deletions amitools/vamos/tools/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,4 @@
from .path import PathTool
from .type import TypeTool
from .libprof import LibProfilerTool
from .state import StateTool
58 changes: 58 additions & 0 deletions amitools/vamos/tools/state.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import sys

from .tool import Tool
from amitools.vamos.astructs import AmigaStructTypes, TypeDumper
from amitools.state import ASFFile, ASFParser


class StateTool(Tool):
def __init__(self):
Tool.__init__(self, "state", "inspect system state")

def add_args(self, arg_parser):
sub = arg_parser.add_subparsers(dest="type_cmd")
# info
parser = sub.add_parser("info", help="give info on state file")
parser.add_argument("file", help="UAE state file (.uss)")
parser.add_argument(
"-d", "--dump", default=False, action="store_true", help="dump state file"
)
parser.add_argument("-s", "--save", help="save RAM to files with prefix")

def run(self, args):
type_cmd = args.type_cmd
# info
if type_cmd == "info":
asf = ASFFile(args.file)
chunks = asf.chunklist()

# dump state file?
if args.dump:
for chunk in chunks:
print(chunk)

# parse state file
parser = ASFParser(asf)

# dump expansion
if args.dump:
expansion = parser.get_expansion()
if expansion:
print(expansion)

# show ram layout
print("RAM:")
load_ram = args.save is not None
ram_layout = parser.get_ram_layout(load_ram)
for ram in ram_layout:
if args.save:
file = args.save + f".{ram.type.name}.{ram.address:08x}.dump"
with open(file, "wb") as fh:
fh.write(ram.data)
else:
file = ""
print(
f"@{ram.address:08x} +{ram.size:08x} {ram.type.name:6s} {file}"
)

return 0
Binary file added test/state/a4000.uss
Binary file not shown.
Binary file added test/state/a4000z3ram.uss
Binary file not shown.
Binary file added test/state/a500.uss
Binary file not shown.
Binary file added test/state/a500z2ram.uss
Binary file not shown.
Loading

0 comments on commit 72c8b5c

Please sign in to comment.