# Wii partitions

Wii disc images contain encrypted partitions. RVZ stores partition data **decrypted** to
make it compress well, plus *hash exceptions* — the minimal set of original hash values
that differ from a freshly recomputed hash tree. This page describes the machinery;
`PartitionRegionBuilder` (reader) and `WiiPartitionExtractor` (writer) implement it.

## Disc header facts

| Offset | Field | Meaning |
|---|---|---|
| 0x18 | u32 BE | `0x5D1C9EA3` = Wii disc (partition table offset, shifted <<2, normally 0x10000 → 0x40000) |
| 0x1C | u32 BE | partition table entry count |
| 0x60 | byte | `0` = hash trees present |
| 0x61 | byte | `0` = partition data encrypted |
| 0x1C (GC) | u32 BE | `0xC2339F3D` = GameCube DVD magic (no partitions) |

The writer only applies partition conversion when the disc has Wii magic **and** hashes
**and** encryption; otherwise everything is stored as raw data (still byte-exact).

## Partition table (at 0x40000)

Four groups, each `[count u32 BE][table_offset u32 BE << 2]` at `0x40000 + 8·group`; each
entry is `[partition_offset u32 BE << 2][type u32 BE]`.

The partition header (at `partition_offset`) holds:

| Offset | Field |
|---|---|
| 0x00 | ticket: signature type — `0x10001` (RSA2048) |
| 0x1BF | 16-byte title key (AES-CBC encrypted with the console common key) |
| 0x1DC | title ID (u64 BE) — also the IV for the title-key decryption |
| 0x1F1 | common key index (0 = retail, 1 = Korean) |
| 0x2B8 | data_offset (u32 BE, << 2) |
| 0x2BC | data_size (u32 BE, << 2) |
| 0x424 | FST offset within the partition (u32 BE, << 2) |
| 0x428 | FST size (u32 BE, << 2) |

The title key is stored encrypted: `title_key = AES-128-CBC-decrypt(common_key, IV =
title_id ‖ 0⁸, ticket[0x1BF..0x1CF])`, with the common key selected by the ticket issuer
(RVT/iQue) or the common-key index (Dolphin: `TicketReader::GetTitleKey` /
`IOSC::Decrypt`; RVZSharp: `WiiVolume.GetTitleKey`). RVZ/WIA containers store the
**decrypted** key in their partition table, which is authoritative even when a re-signed
ticket carries a different one.

The partition data lives at `partition_offset + data_offset`, `data_size` bytes long.
Invalid partitions (wrong alignment, zero/undersized size) are encoded as raw data instead.

## Sector layout

```
0x0000 ┌───────────────────────┐
       │ hash area   (0x400)   │  AES-128-CBC, IV = 16 zero bytes
0x0400 ├───────────────────────┤
       │ data area   (0x7C00)  │  AES-128-CBC, IV = ciphertext at 0x3D0
0x3D0  │ … (IV source) …       │
0x8000 └───────────────────────┘
```

A region is 64 sectors (2 MiB) of encrypted data. The final region of a partition is
usually partial; missing sectors are **zero-filled** (both for hashing and for output), and
the corresponding disc bytes beyond the partition are covered by raw data areas instead.

## Hash tree

Inside the 0x400-byte hash area:

| Offset | Size | Content |
|---|---|---|
| 0x000 | 0x26C | `h0`: 31 × SHA-1 over each 0x7C00 data block of the sector |
| 0x26C | 0x14 | padding |
| 0x280 | 0xA0 | `h1`: 8 × SHA-1, one per sector of the 8-sector group; `h1[j] = SHA1(h0 of sector 8g+j)` |
| 0x320 | 0x20 | padding |
| 0x340 | 0xA0 | `h2`: 8 × SHA-1, one per 8-sector group of the region; `h2[g] = SHA1(h1 array of group g)` |
| 0x3E0 | 0x20 | padding |

The `h1`/`h2` arrays are replicated into every sector's hash area of their group/region.
For sectors beyond the partition end, `h0` is hashed over 0x26C zero bytes (this convention
is shared by Dolphin, the Go reader and RVZSharp).

## Hash exceptions

When the writer stores decrypted data, the original encrypted hash area is *not* stored.
Instead it compares, per sector, every 20-byte stride of the six fields above
(`h0`, padding, `h1`, padding, `h2`, padding — with partial final strides) between the
**decrypted original** hashes and the **recalculated** tree, and stores an exception for
each difference:

```
u16 hash_offset = block_index_in_chunk × 0x400 + offset_in_block
20 bytes        = the original hash
```

- Exceptions are only recorded for sectors that actually exist (the zero-filled tail never
  produces exceptions for itself; its `h1`/`h2` mismatches on the *real* sectors do, which
  is expected and correct).
- Stored offsets are **chunk-relative**; the reader converts them to region-relative with
  `additional_offset = (chunkPayloadOffset % 0x1F0000) / 0x7C00 × 0x400` before matching a
  sector (`offset >> 10 == sector % 64`).

The reader rebuilds a sector as: decrypt payload → recompute tree → apply exceptions →
AES-128-CBC re-encrypt (hash area with zero IV, data area with the encrypted hash bytes at
0x3D0 as IV) — byte-identical to the original disc.

## FST split

Dolphin's converter splits each partition into two data entries at the end of the FST
(rounded up to a 2 MiB boundary relative to the partition data start). The split keeps the
FST and its referenced files in the first entry; RVZSharp replicates it:

```
split_point = data_start + alignUp(fst_end - data_start, 2 MiB)
segment 0   = [data_start, split_point)
segment 1   = [split_point, data_end)
```

Both segments are stored decrypted with their own exception lists and group ranges.
