nx_save_file_format.nx
buildroot/runtime/hub/nx_save_file_format.nx
about
nx_save_file_format.nx -- NSAV save file format encoder + decoder.
V-CLIENT-STORE-1 per NISHI_CLIENT_STORE_CHARTER §2.2 + §8 M1.
CARDINAL CONTEXT: per CS-2 client-owned storage cardinal, game saves
live on the user's OWN FILESYSTEM, NEVER on substrate servers. This
module provides the canonical wire format so saves are:
- portable across browsers / devices / save paths (Path A / B / C)
- integrity-checked (HMAC-SHA-256 footer detects tampering)
- versioned (forward-compat via header version field)
- cross-game distinguishable (game_id field; SHA-256 of identifier)
- sovereign substrate-canonical (no proprietary blob format)
FILE LAYOUT (per charter §2.2):
offset 0..4 magic ascii "NSAV"
offset 4..6 version u16 BE (V1 = 0x0001)
offset 6..8 flags u16 BE (bit 0: encrypted [V+1]; bits 1-15 reserved)
offset 8..40 game_id SHA-256("site_id|||game_name") 32 bytes
offset 40..48 created_at i64 BE unix seconds since epoch
offset 48..56 payload_len i64 BE byte length of payload
offset 56..56+payload_len payload (opaque to substrate; game-defined)
offset (..)..(..)+32 hmac_sha256 over bytes[0..(end-32)]
with file_key = SHA-256("nishi-save|||"
|| game_id
|| passphrase)
FILE_KEY derivation: HMAC key = SHA-256("nishi-save|||" || game_id ||
user_passphrase). For unencrypted V1 saves, passphrase = "" (empty);
the key still provides integrity binding to game_id so a save for
game A cannot be passed off as a save for game B.
COMPOSES (per "avoid duplicate primitives" cardinal):
nx_hmac hmac_sha256 (RFC 2104)
sha256 digest helper for game_id derivation
nx_syscalls sys_mmap
COMPOSED BY:
hub/nx_client_storage_emit V-CLIENT-STORE-3/4 emits JS that
serializes/deserializes NSAV via this
bench/nx_client_store_smoke V-CLIENT-STORE-6 vector tests
dependencies 3 imports · 0 importers
imports: nx_syscalls.nxnx_hmac.nxsha256.nx
imported by: nobody (leaf or entry point)
structs
| none |
consts
| 60 | const NX_NSAV_OK: i64 = 0 |
| 61 | const NX_NSAV_BAD_INPUT: i64 = 1360 |
| 62 | const NX_NSAV_BUF_OVERFLOW: i64 = 1361 |
| 63 | const NX_NSAV_MAGIC_MISMATCH: i64 = 1362 |
| 64 | const NX_NSAV_VERSION_UNSUPPORTED: i64 = 1363 |
| 65 | const NX_NSAV_GAME_ID_MISMATCH: i64 = 1364 |
| 66 | const NX_NSAV_HMAC_MISMATCH: i64 = 1365 |
| 67 | const NX_NSAV_TRUNCATED: i64 = 1366 |
| 68 | const NX_NSAV_PAYLOAD_TOO_LARGE: i64 = 1367 |
| 69 | const NX_NSAV_FLAG_UNSUPPORTED: i64 = 1368 |
| 72 | const NX_NSAV_MAGIC_BYTES: i64 = 4 |
| 73 | const NX_NSAV_VERSION_BYTES: i64 = 2 |
| 74 | const NX_NSAV_FLAGS_BYTES: i64 = 2 |
| 75 | const NX_NSAV_GAME_ID_BYTES: i64 = 32 |
| 76 | const NX_NSAV_CREATED_AT_BYTES: i64 = 8 |
| 77 | const NX_NSAV_PAYLOAD_LEN_BYTES: i64 = 8 |
| 78 | const NX_NSAV_HMAC_BYTES: i64 = 32 |
| 79 | const NX_NSAV_HEADER_BYTES: i64 = 56 // sum of fields before payload |
| 80 | const NX_NSAV_MIN_FILE_BYTES: i64 = 88 // header (56) + min payload (0) + hmac (32) |
| 82 | const NX_NSAV_OFF_MAGIC: i64 = 0 |
| 83 | const NX_NSAV_OFF_VERSION: i64 = 4 |
| 84 | const NX_NSAV_OFF_FLAGS: i64 = 6 |
| 85 | const NX_NSAV_OFF_GAME_ID: i64 = 8 |
| 86 | const NX_NSAV_OFF_CREATED_AT: i64 = 40 |
| 87 | const NX_NSAV_OFF_PAYLOAD_LEN: i64 = 48 |
| 88 | const NX_NSAV_OFF_PAYLOAD: i64 = 56 |
| 90 | const NX_NSAV_VERSION_V1: i64 = 0x0001 |
| 91 | const NX_NSAV_FLAG_ENCRYPTED: i64 = 0x0001 // V+1 |
| 92 | const NX_NSAV_FLAGS_VALID_MASK: i64 = 0x0000 // V1 supports no flags yet |
| 94 | const NX_NSAV_MAX_PAYLOAD_BYTES: i64 = 16777216 // 16 MiB; sanity cap |
| 96 | const NX_NSAV_FILE_KEY_BYTES: i64 = 32 |
functions
| 100 | func _nsav_put_u16_be(out: *u8, v: i64) -> i64 called by 1: nx_nsav_encode |
| 108 | func _nsav_get_u16_be(b: *u8) -> i64 called by 1: nx_nsav_decode_and_verify |
| 112 | func _nsav_put_i64_be(out: *u8, v: i64) -> i64 called by 1: nx_nsav_encode |
| 124 | func _nsav_get_i64_be(b: *u8) -> i64 called by 1: nx_nsav_decode_and_verify |
| 144 | func nx_nsav_derive_game_id( calls 1: sys_mmap |
| 181 | func nx_nsav_derive_file_key( calls 1: sys_mmap |
| 224 | func nx_nsav_encode( |
| 299 | func nx_nsav_decode_and_verify( |