| """ |
| Disc scanner. |
| |
| Reads a source sector by sector into bytes. The source can be a raw optical |
| drive (Windows: r"\\\\.\\D:", may need admin) or an .iso / image file (for |
| testing). Unreadable sectors (scratches / disc rot) are recorded as BAD and |
| zero-filled -- ddrescue-style -- rather than aborting the scan. |
| |
| Important: bad sectors are data you never read. RS self-healing (archive.py) |
| protects the STORED copy against future corruption; it cannot invent sectors the |
| drive could not read. To recover those, re-scan the disc and merge good sectors |
| across attempts (`merge_scans`). |
| """ |
| from __future__ import annotations |
|
|
| SECTOR = 2048 |
|
|
|
|
| def scan_image(path: str, sector: int = SECTOR): |
| """-> (data: bytes, bad_sectors: list[int]). Robust to unreadable sectors.""" |
| data = bytearray() |
| bad = [] |
| with open(path, "rb", buffering=0) as f: |
| idx = 0 |
| while True: |
| try: |
| blk = f.read(sector) |
| except OSError: |
| bad.append(idx) |
| data += bytes(sector) |
| try: |
| f.seek((idx + 1) * sector) |
| except OSError: |
| break |
| idx += 1 |
| continue |
| if not blk: |
| break |
| data += blk |
| idx += 1 |
| return bytes(data), bad |
|
|
|
|
| def merge_scans(scans, sector: int = SECTOR): |
| """Combine multiple (data, bad) scans of the same failing disc: a sector is |
| good if ANY scan read it. Returns (merged_data, still_bad_sectors).""" |
| n = max(len(d) // sector for d, _ in scans) |
| merged = bytearray(n * sector) |
| known = [False] * n |
| for data, bad in scans: |
| badset = set(bad) |
| for s in range(len(data) // sector): |
| if s not in badset and not known[s]: |
| merged[s * sector:(s + 1) * sector] = data[s * sector:(s + 1) * sector] |
| known[s] = True |
| still_bad = [s for s in range(n) if not known[s]] |
| return bytes(merged), still_bad |
|
|