-
Notifications
You must be signed in to change notification settings - Fork 7
NKDS NkFs Format
created: 2025-06-12 modified: 2025-06-12 tags: [format, nkds-format, datastore, nkfs] type: format status: active
Compact binary representation of filesystem trees — fixed-size entries with O(1) lookup, inspired by GameCube/Wii FST.
NkFs is the binary format used to represent filesystem trees within NKDS image metadata. It replaces the variable-length YAML text format (filesystem.yaml via FsYaml) with a fixed-size binary format. Produces smaller output than UTF-8 YAML for non-trivial trees (100+ entries).
Key characteristics:
- 12-byte fixed entries — O(1) index-based lookup
- Flat navigation — No in-memory tree built; traversal via parent/next-entry indices
- Big-endian — Matches GC/Wii FST convention
- Directory name deduplication — Shared names reference same string table offset
- Multi-extent file support — Non-contiguous files via entry chaining
- Round-trip fidelity — Bidirectional conversion with FsYaml
Offset 0x00:
┌──────────────────────────────────────────────────────────┐
│ Header (12 bytes) │
│ Magic, Version, Reserved, EntryCount │
├──────────────────────────────────────────────────────────┤ ← 0x0C
│ Entry Table (N × 12 bytes) │
│ Entry 0: Root directory │
│ Entry 1..N-1: depth-first pre-order │
├──────────────────────────────────────────────────────────┤ ← 12 + N×12
│ String Table (variable length, 2-byte aligned) │
│ Directories: [name\0] + optional pad │
│ Files: [prefix byte][fields][name\0] + optional pad │
└──────────────────────────────────────────────────────────┘
String table offset = 12 + EntryCount × 12 (computed, not stored in header).
| Offset | Size | Type | Field | Description |
|---|---|---|---|---|
| 0x00 | 4 | uint32 | Magic |
0x4E4B4653 (ASCII "NKFS") |
| 0x04 | 2 | uint16 | Version | Currently 1
|
| 0x06 | 2 | uint16 | Reserved | Must be 0
|
| 0x08 | 4 | int32 | EntryCount | Total entries including root |
| Offset | Size | Type | Field | Description |
|---|---|---|---|---|
| 0x00 | 4 | uint32 | FlagsAndNameOffset | High 3 bits = Flags, low 29 bits = NameOffset (×2 for byte offset) |
| 0x04 | 8 | varies | — | File: ImageOffset (int64). Directory: ParentIndex (int32) + NextEntryIndex (int32) |
| Bit | Name | Description |
|---|---|---|
| 0 | is_directory | 1 = directory, 0 = file |
| 1 | system_flag | System marker (hidden from normal VFS views) |
| 2 | image_file | Image file system reference |
packed = (flags << 29) | (nameOffset & 0x1FFFFFFF)
actualByteOffset = nameOffset × 2 // 2-byte alignment, 1 GiB addressable
| Field | Size | Description |
|---|---|---|
| ImageOffset | 8 bytes (int64) | Byte offset in source image |
Size, checksums, and image ID stored in string table prefix.
| Field | Size | Description |
|---|---|---|
| ParentIndex | 4 bytes (int32) | Parent directory entry index |
| NextEntryIndex | 4 bytes (int32) | First entry NOT a descendant |
Root: index 0, ParentIndex = 0 (self), NextEntryIndex = EntryCount.
[name\0] + optional pad byte (2-byte alignment)
Directory names eligible for deduplication (identical names share same offset).
[1 byte prefix flags]
[size_width bytes: file size, big-endian]
[if has_checksums: 8 bytes xxHash64 + 4 bytes CRC32, big-endian]
[imageid_width bytes: image index, big-endian]
[name\0]
+ optional pad byte
Bit: 7 6 5 4 3 2 1 0
[res][res][HME][iw1][iw0][sw1][sw0][chk]
| Bits | Name | Description |
|---|---|---|
| 0 | has_checksums | 1 = 12 bytes xxHash64+CRC32 present |
| 1-2 | size_width | 0/1/2/3 → 0/2/4/8 bytes |
| 3-4 | imageid_width | 0/1/2/3 → 0/2/4/8 bytes |
| 5 | has_more_extents | 1 = next sibling is continuation extent |
| 6-7 | reserved | Must be 0 |
| Value | Bytes | Range |
|---|---|---|
| 0 | 0 | value = 0 |
| 1 | 2 | ≤ 0xFFFF |
| 2 | 4 | ≤ 0xFFFFFFFF |
| 3 | 8 | > 0xFFFFFFFF |
children(N):
nextEntry = entry[N].NextEntryIndex
i = N + 1
while i < nextEntry:
yield entry[i]
if entry[i].is_directory:
i = entry[i].NextEntryIndex // skip subtree
else:
i = i + 1
resolve("dir1/dir2/file.txt"):
current = 0 (root)
for each segment in path.split('/'):
scan children of entry[current]
if match found: current = child index
else: return -1
return current
Files spanning non-contiguous disc regions use consecutive same-name entries linked by has_more_extents (bit 5):
Entry i: Name="video.m2ts", Offset=0x1000, Size=0x8000, HME=1
Entry i+1: Name="video.m2ts", Offset=0xF000, Size=0x6000, HME=1
Entry i+2: Name="video.m2ts", Offset=0x20000, Size=0x4000, HME=0 (final)
Total logical size = sum of all extent sizes.
Readers unaware of bit 5 see each extent as an independent file with the same name — degraded but non-crashing interpretation.