@@ -60,14 +60,16 @@ class MBRInfo:
6060
6161@dataclass
6262class 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
7375def 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+
254328def 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+
378501def 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 ("\n Warnings:" )
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