138 lines
5 KiB
Python
Executable file
138 lines
5 KiB
Python
Executable file
#!/usr/bin/env python3
|
|
"""adfTool.py - read files out of an Amiga OFS/FFS disk image (.adf).
|
|
|
|
adfTool.py list IMAGE every file, one path per line
|
|
adfTool.py extract IMAGE PATH OUT one file (PATH like Devs/serial.device)
|
|
adfTool.py extractall IMAGE OUTDIR every file, keeping the drawer tree
|
|
|
|
Read-only. Used by the installer's `import` (a Workbench disk image is how
|
|
most people own AmigaOS files such as Devs/serial.device) and by the Amiga
|
|
gates. Handles DD (880 KB) and HD (1.76 MB) images; names match case-
|
|
insensitively, as AmigaDOS does.
|
|
"""
|
|
|
|
import os
|
|
import struct
|
|
import sys
|
|
|
|
BLOCK_SIZE = 512
|
|
HASH_SLOTS = 72 # hash-table entries in a 512-byte header block
|
|
TYPE_HEADER = 2 # primary type of a root / drawer / file header
|
|
SEC_TYPE_ROOT = 1
|
|
SEC_TYPE_USERDIR = 2
|
|
SEC_TYPE_FILE = -3
|
|
OFS_DATA_HEADER = 24 # OFS data blocks start with a 24-byte header
|
|
|
|
|
|
class AdfImage:
|
|
def __init__(self, path):
|
|
with open(path, "rb") as f:
|
|
self.image = f.read()
|
|
if len(self.image) % BLOCK_SIZE != 0 or self.image[0:3] != b"DOS":
|
|
raise ValueError("%s is not an AmigaDOS disk image" % path)
|
|
self.isFfs = (self.image[3] & 1) != 0
|
|
self.rootBlock = (len(self.image) // BLOCK_SIZE) // 2
|
|
|
|
def block(self, number):
|
|
return self.image[number * BLOCK_SIZE:(number + 1) * BLOCK_SIZE]
|
|
|
|
@staticmethod
|
|
def long(data, offset):
|
|
# Negative offsets count from the end of the block, as the
|
|
# AmigaDOS structure definitions do.
|
|
if offset < 0:
|
|
offset += BLOCK_SIZE
|
|
return struct.unpack(">l", data[offset:offset + 4])[0]
|
|
|
|
@staticmethod
|
|
def name(data):
|
|
length = data[BLOCK_SIZE - 80]
|
|
return data[BLOCK_SIZE - 79:BLOCK_SIZE - 79 + length].decode("latin-1")
|
|
|
|
def entries(self, dirBlock):
|
|
# (name, header block, secondary type) for everything in a drawer.
|
|
data = self.block(dirBlock)
|
|
found = []
|
|
for slot in range(HASH_SLOTS):
|
|
number = self.long(data, 24 + 4 * slot)
|
|
while number:
|
|
header = self.block(number)
|
|
if self.long(header, 0) == TYPE_HEADER:
|
|
found.append((self.name(header), number, self.long(header, -4)))
|
|
number = self.long(header, -16)
|
|
return sorted(found, key=lambda e: e[0].upper())
|
|
|
|
def walk(self, dirBlock=None, prefix=""):
|
|
# Yield (path, header block) for every file under a drawer.
|
|
if dirBlock is None:
|
|
dirBlock = self.rootBlock
|
|
for entryName, number, secType in self.entries(dirBlock):
|
|
path = prefix + entryName
|
|
if secType == SEC_TYPE_USERDIR:
|
|
yield from self.walk(number, path + "/")
|
|
elif secType == SEC_TYPE_FILE:
|
|
yield path, number
|
|
|
|
def find(self, path):
|
|
current = self.rootBlock
|
|
for part in path.split("/"):
|
|
match = [n for entryName, n, _ in self.entries(current) if entryName.upper() == part.upper()]
|
|
if not match:
|
|
raise KeyError("not found in image: %s" % path)
|
|
current = match[0]
|
|
return current
|
|
|
|
def read(self, headerBlock):
|
|
header = self.block(headerBlock)
|
|
size = self.long(header, -188)
|
|
data = bytearray()
|
|
chunk = header
|
|
while True:
|
|
count = self.long(chunk, 8)
|
|
# Data-block pointers fill the table from the END (slot 71 first).
|
|
pointers = [self.long(chunk, 24 + 4 * i) for i in range(HASH_SLOTS)][::-1][:count]
|
|
for pointer in pointers:
|
|
blockData = self.block(pointer)
|
|
if self.isFfs:
|
|
data += blockData
|
|
else:
|
|
data += blockData[OFS_DATA_HEADER:OFS_DATA_HEADER + self.long(blockData, 12)]
|
|
extension = self.long(chunk, -8)
|
|
if not extension:
|
|
break
|
|
chunk = self.block(extension)
|
|
return bytes(data[:size])
|
|
|
|
|
|
def main(argv):
|
|
if len(argv) < 3 or argv[1] not in ("list", "extract", "extractall"):
|
|
sys.stderr.write(__doc__)
|
|
return 2
|
|
try:
|
|
image = AdfImage(argv[2])
|
|
if argv[1] == "list":
|
|
for path, _ in image.walk():
|
|
print(path)
|
|
elif argv[1] == "extract":
|
|
if len(argv) != 5:
|
|
sys.stderr.write(__doc__)
|
|
return 2
|
|
with open(argv[4], "wb") as f:
|
|
f.write(image.read(image.find(argv[3])))
|
|
else:
|
|
if len(argv) != 4:
|
|
sys.stderr.write(__doc__)
|
|
return 2
|
|
for path, number in image.walk():
|
|
out = os.path.join(argv[3], *path.split("/"))
|
|
os.makedirs(os.path.dirname(out), exist_ok=True)
|
|
with open(out, "wb") as f:
|
|
f.write(image.read(number))
|
|
except (OSError, ValueError, KeyError) as error:
|
|
sys.stderr.write("adfTool.py: %s\n" % error)
|
|
return 1
|
|
return 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
sys.exit(main(sys.argv))
|