Skip to content

Commit be0f8e2

Browse files
committed
rdb: support Parceiro-style MBR+RDB coexistence and tolerate corrupt LSEG blocks
The Parceiro II+ uses a disk layout where an MBR partition table at block 0 coexists with an Amiga RDB starting at block 1. Non-Amiga partitions (e.g. FAT32 for data transfer) live in MBR entries while Amiga partitions are defined in the RDB. This is distinct from the Emu68 scheme where the RDB lives inside an MBR partition of type 0x76. Additionally, amitools' RDisk.open() fails entirely when any filesystem driver's LSEG chain contains a block with a bad checksum, even though the partition table is perfectly valid. The Parceiro image has two such corrupt blocks (FAT1 driver at block 64, PFS3 driver at block 197), both off by exactly one in the checksum. Add _lenient_rdisk_open() as a fallback that parses partition blocks strictly but skips corrupt filesystem entries with warnings instead of aborting. Detect Parceiro-style layout when an MBR is present at block 0 and the RDSK signature is found at block 1+. Report the disk scheme and any corrupt FS entries in both text and JSON output.
1 parent 3e14523 commit be0f8e2

File tree

2 files changed

+169
-24
lines changed

2 files changed

+169
-24
lines changed

amifuse/fuse_fs.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1923,6 +1923,12 @@ def cmd_inspect(args):
19231923
print("\nFilesystem drivers:")
19241924
for line in fs_lines:
19251925
print(" ", line)
1926+
1927+
warnings = getattr(rdisk, 'rdb_warnings', [])
1928+
if warnings:
1929+
print("\nWarnings:")
1930+
for w in warnings:
1931+
print(f" {w}")
19261932
finally:
19271933
if rdisk is not None:
19281934
rdisk.close()

amifuse/rdb_inspect.py

Lines changed: 163 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -60,14 +60,16 @@ class MBRInfo:
6060

6161
@dataclass
6262
class MBRContext:
63-
"""Context for an RDB opened within an MBR partition.
63+
"""Context for an RDB opened alongside or within an MBR partition.
6464
65-
Stored so callers can understand the disk layout and adjust block
66-
operations accordingly.
65+
For Emu68-style disks, the RDB lives inside an MBR partition of type 0x76.
66+
For Parceiro-style disks, the MBR and RDB coexist at the disk level: the
67+
MBR occupies block 0 and the RDB starts at block 1+, with no block offset.
6768
"""
6869
mbr_info: MBRInfo # Full MBR partition table info
69-
mbr_partition: MBRPartition # The specific 0x76 partition containing the RDB
70-
offset_blocks: int # Block offset from start of disk
70+
mbr_partition: Optional[MBRPartition] # 0x76 partition (Emu68), None for Parceiro
71+
offset_blocks: int # Block offset from start of disk (0 for Parceiro)
72+
scheme: str = "emu68" # "emu68" or "parceiro"
7173

7274

7375
def detect_mbr(image: Path) -> Optional[MBRInfo]:
@@ -251,6 +253,78 @@ def _scan_for_rdb(blkdev, block_size: Optional[int] = None):
251253
return None, None
252254

253255

256+
def _lenient_rdisk_open(rdisk) -> List[str]:
257+
"""Open an RDisk leniently, tolerating corrupt filesystem blocks.
258+
259+
Replicates the logic of RDisk.open() but continues past corrupt
260+
filesystem (LSEG) blocks instead of failing. Partition blocks are
261+
still required to be valid.
262+
263+
Returns a list of warning strings (empty if everything parsed cleanly).
264+
"""
265+
from amitools.fs.block.Block import Block
266+
from amitools.fs.block.rdb.PartitionBlock import PartitionBlock
267+
from amitools.fs.rdb.Partition import Partition
268+
from amitools.fs.rdb.FileSystem import FileSystem
269+
270+
rdb = rdisk.rdb
271+
if rdb.block_size != rdisk.rawblk.block_bytes:
272+
raise ValueError(
273+
"block size mismatch: rdb=%d != device=%d"
274+
% (rdb.block_size, rdisk.rawblk.block_bytes)
275+
)
276+
rdisk.block_bytes = rdb.block_size
277+
rdisk.used_blks = [rdb.blk_num]
278+
warnings = []
279+
280+
# Read partitions (critical — fail on errors)
281+
part_blk = rdb.part_list
282+
rdisk.parts = []
283+
num = 0
284+
while part_blk != Block.no_blk:
285+
p = Partition(rdisk.rawblk, part_blk, num, rdb.log_drv.cyl_blks, rdisk)
286+
num += 1
287+
if not p.read():
288+
raise IOError(f"Corrupt partition block at block {part_blk}")
289+
rdisk.parts.append(p)
290+
rdisk.used_blks.append(p.get_blk_num())
291+
part_blk = p.get_next_partition_blk()
292+
293+
# Read filesystems (non-critical — warn on errors)
294+
fs_blk = rdb.fs_list
295+
rdisk.fs = []
296+
num = 0
297+
while fs_blk != PartitionBlock.no_blk:
298+
fs = FileSystem(rdisk.rawblk, fs_blk, num)
299+
num += 1
300+
if not fs.read():
301+
# The FSHD itself may have read OK (fs.fshd.valid) even though
302+
# the LSEG data chain is corrupt.
303+
if fs.fshd is not None and fs.fshd.valid:
304+
dt = fs.fshd.dos_type
305+
dt_str = DosType.num_to_tag_str(dt)
306+
warnings.append(
307+
f"Filesystem #{fs.num} ({dt_str}/0x{dt:08x}): "
308+
f"corrupt data block in LSEG chain (driver data unavailable)"
309+
)
310+
# We can still follow the next-FS pointer from the header
311+
fs_blk = fs.fshd.next
312+
else:
313+
warnings.append(
314+
f"Corrupt filesystem header at block {fs_blk} "
315+
f"(remaining filesystem entries skipped)"
316+
)
317+
break
318+
continue
319+
rdisk.fs.append(fs)
320+
rdisk.used_blks += fs.get_blk_nums()
321+
fs_blk = fs.get_next_fs_blk()
322+
323+
rdisk.valid = True
324+
rdisk.max_blks = rdb.log_drv.rdb_blk_hi + 1
325+
return warnings
326+
327+
254328
def open_rdisk(
255329
image: Path, block_size: Optional[int] = None, mbr_partition_index: Optional[int] = None
256330
) -> Tuple[Union[RawBlockDevice, 'OffsetBlockDevice'], RDisk, Optional[MBRContext]]:
@@ -259,10 +333,16 @@ def open_rdisk(
259333
Scans blocks 0-15 for the RDB signature (RDSK), as the RDB can be located
260334
at any of these blocks depending on the disk geometry.
261335
262-
For MBR-partitioned disks (e.g., Emu68 SD cards), if no direct RDB is found
263-
but the disk has MBR partitions of type 0x76, the RDB is searched within
264-
those partitions. The returned block device will be an OffsetBlockDevice
265-
that maps to the partition boundaries.
336+
Supports three disk layouts:
337+
- Plain RDB: RDSK at block 0 (standard Amiga hard disk)
338+
- Parceiro-style: MBR at block 0, RDSK at block 1+, coexisting at the
339+
disk level. Non-Amiga partitions (e.g. FAT32) live in MBR entries.
340+
- Emu68-style: MBR with 0x76 (Amiga RDB) partition, RDSK inside that
341+
partition.
342+
343+
Tolerates corrupt filesystem driver (LSEG) blocks in the RDB — partition
344+
data is parsed strictly, but corrupt FS entries are skipped with warnings
345+
stored in ``rdisk.rdb_warnings``.
266346
267347
Args:
268348
image: Path to the disk image
@@ -272,7 +352,7 @@ def open_rdisk(
272352
273353
Returns:
274354
Tuple of (block_device, rdisk, mbr_context).
275-
mbr_context is None for direct RDB disks, or MBRContext for MBR partitions.
355+
mbr_context is None for plain RDB disks, or MBRContext for MBR disks.
276356
"""
277357
initial_block_size = block_size or 512
278358
blkdev = RawBlockDevice(str(image), read_only=True, block_bytes=initial_block_size)
@@ -289,13 +369,32 @@ def open_rdisk(
289369
rdb_block, _ = _scan_for_rdb(blkdev, block_size)
290370

291371
if rdb_block is not None:
292-
# Found direct RDB
372+
# Found direct RDB — try strict open first, then lenient fallback
293373
rdisk = RDisk(blkdev)
294374
rdisk.rdb = rdb_block
375+
rdisk.rdb_warnings = []
295376
if not rdisk.open():
296-
blkdev.close()
297-
raise IOError(f"Failed to parse RDB at {image}")
298-
return blkdev, rdisk, None
377+
# Strict open failed — try lenient parse (tolerates corrupt FS blocks)
378+
rdisk2 = RDisk(blkdev)
379+
rdisk2.rdb = rdb_block
380+
try:
381+
rdisk2.rdb_warnings = _lenient_rdisk_open(rdisk2)
382+
except IOError:
383+
blkdev.close()
384+
raise IOError(f"Failed to parse RDB at {image}")
385+
rdisk = rdisk2
386+
387+
# Check for Parceiro-style MBR+RDB coexistence
388+
mbr_ctx = None
389+
mbr_info = detect_mbr(image)
390+
if mbr_info is not None and rdb_block.blk_num > 0:
391+
mbr_ctx = MBRContext(
392+
mbr_info=mbr_info,
393+
mbr_partition=None,
394+
offset_blocks=0,
395+
scheme="parceiro",
396+
)
397+
return blkdev, rdisk, mbr_ctx
299398

300399
# No direct RDB - check for MBR with 0x76 partitions
301400
mbr_info = detect_mbr(image)
@@ -330,8 +429,16 @@ def open_rdisk(
330429
# Found RDB in this partition
331430
rdisk = RDisk(offset_dev)
332431
rdisk.rdb = rdb_block
432+
rdisk.rdb_warnings = []
333433
if not rdisk.open():
334-
continue # Try next partition
434+
# Try lenient parse
435+
rdisk2 = RDisk(offset_dev)
436+
rdisk2.rdb = rdb_block
437+
try:
438+
rdisk2.rdb_warnings = _lenient_rdisk_open(rdisk2)
439+
except IOError:
440+
continue # Try next partition
441+
rdisk = rdisk2
335442

336443
mbr_ctx = MBRContext(
337444
mbr_info=mbr_info,
@@ -375,17 +482,37 @@ def format_fs_summary(rdisk: RDisk):
375482
return lines
376483

377484

485+
_MBR_TYPE_NAMES = {
486+
MBR_TYPE_AMIGA_RDB: "Amiga RDB",
487+
0x01: "FAT12",
488+
0x04: "FAT16 <32M",
489+
0x06: "FAT16",
490+
0x07: "NTFS/exFAT",
491+
0x0B: "W95 FAT32",
492+
0x0C: "W95 FAT32 (LBA)",
493+
0x0E: "W95 FAT16 (LBA)",
494+
0x0F: "W95 Extended (LBA)",
495+
0x82: "Linux swap",
496+
0x83: "Linux",
497+
0xEE: "GPT protective",
498+
}
499+
500+
378501
def format_mbr_info(mbr_ctx: MBRContext) -> List[str]:
379502
"""Format MBR partition info as a list of lines for display."""
380503
lines = []
381-
lines.append("MBR Partition Table detected (Emu68-style)")
382-
lines.append(f" Active partition: MBR slot {mbr_ctx.mbr_partition.index}")
383-
lines.append(f" Partition offset: {mbr_ctx.offset_blocks} sectors ({mbr_ctx.offset_blocks * 512 // 1024 // 1024} MB)")
384-
lines.append(f" Partition size: {mbr_ctx.mbr_partition.num_sectors} sectors ({mbr_ctx.mbr_partition.num_sectors * 512 // 1024 // 1024} MB)")
504+
if mbr_ctx.scheme == "parceiro":
505+
lines.append("MBR + RDB coexistence detected (Parceiro-style)")
506+
lines.append(" MBR at block 0, RDB at block 1+")
507+
else:
508+
lines.append("MBR Partition Table detected (Emu68-style)")
509+
lines.append(f" Active partition: MBR slot {mbr_ctx.mbr_partition.index}")
510+
lines.append(f" Partition offset: {mbr_ctx.offset_blocks} sectors ({mbr_ctx.offset_blocks * 512 // 1024 // 1024} MB)")
511+
lines.append(f" Partition size: {mbr_ctx.mbr_partition.num_sectors} sectors ({mbr_ctx.mbr_partition.num_sectors * 512 // 1024 // 1024} MB)")
385512
lines.append("")
386-
lines.append(" All MBR partitions:")
513+
lines.append(" MBR partitions:")
387514
for p in mbr_ctx.mbr_info.partitions:
388-
type_str = "Amiga RDB" if p.partition_type == MBR_TYPE_AMIGA_RDB else f"0x{p.partition_type:02x}"
515+
type_str = _MBR_TYPE_NAMES.get(p.partition_type, f"0x{p.partition_type:02x}")
389516
boot_str = " (bootable)" if p.bootable else ""
390517
size_mb = p.num_sectors * 512 // 1024 // 1024
391518
lines.append(f" [{p.index}] Type: {type_str}{boot_str}, Start: {p.start_lba}, Size: {p.num_sectors} ({size_mb} MB)")
@@ -440,13 +567,14 @@ def main(argv=None):
440567
try:
441568
blkdev, rdisk, mbr_ctx = open_rdisk(args.image, block_size=args.block_size)
442569

570+
warnings = getattr(rdisk, 'rdb_warnings', [])
571+
443572
if args.json:
444573
desc = rdisk.get_desc()
445574
if mbr_ctx is not None:
446-
desc["mbr"] = {
447-
"partition_index": mbr_ctx.mbr_partition.index,
575+
mbr_desc = {
576+
"scheme": mbr_ctx.scheme,
448577
"offset_blocks": mbr_ctx.offset_blocks,
449-
"partition_size": mbr_ctx.mbr_partition.num_sectors,
450578
"all_partitions": [
451579
{
452580
"index": p.index,
@@ -458,6 +586,12 @@ def main(argv=None):
458586
for p in mbr_ctx.mbr_info.partitions
459587
],
460588
}
589+
if mbr_ctx.mbr_partition is not None:
590+
mbr_desc["partition_index"] = mbr_ctx.mbr_partition.index
591+
mbr_desc["partition_size"] = mbr_ctx.mbr_partition.num_sectors
592+
desc["mbr"] = mbr_desc
593+
if warnings:
594+
desc["warnings"] = warnings
461595
print(json.dumps(desc, indent=2))
462596
else:
463597
# Show MBR info if present
@@ -474,6 +608,11 @@ def main(argv=None):
474608
for line in fs_lines:
475609
print(" ", line)
476610

611+
if warnings:
612+
print("\nWarnings:")
613+
for w in warnings:
614+
print(f" {w}")
615+
477616
if args.extract_fs is not None:
478617
fs_obj = rdisk.get_filesystem(args.extract_fs)
479618
if fs_obj is None:

0 commit comments

Comments
 (0)