Skip to content

fix: preserve unrecognized, chunked, and cross-page NVS entries on read-modify-write - #50

Merged
deadprogram merged 1 commit into
tinygo-org:mainfrom
jgangemi:jae/nvs-lossless-codec
Jul 11, 2026
Merged

fix: preserve unrecognized, chunked, and cross-page NVS entries on read-modify-write#50
deadprogram merged 1 commit into
tinygo-org:mainfrom
jgangemi:jae/nvs-lossless-codec

Conversation

@jgangemi

@jgangemi jgangemi commented Jul 8, 2026

Copy link
Copy Markdown
Contributor

Problem

nvs.ParseNVS silently dropped any entry it didn't fully decode — unrecognized entry types, blob-index / blob-data chunks, and multi-slot items — returning only the recognized subset. Round-tripping a partition through ParseNVSGenerateNVS (a read-modify-write) therefore re-encoded only the survivors, destroying the dropped entries (e.g. Wi-Fi credentials, chunked blobs) while reporting success.

Fix

  • Preserve every written entry losslessly: entries that aren't fully decoded are kept as raw passthrough and re-encoded generically (recomputed CRC, reassigned namespace index).
  • Keep variable-length items page-contained on generate, mirroring esp-idf's Page::writeItem (which bumps an oversized item wholly to the next page) — removes an earlier incorrect cross-page carry model.
  • Order pages by header sequence number so latest-wins dedup is correct regardless of physical page order.
  • Deterministic namespace indexing.

Validation

  • Round-trips a real partition produced by esp-idf's own nvs_partition_gen (string + int namespaces plus a 6144-byte chunked blob = BLOB_IDX + 2 BLOB_DATA chunks) byte-identically — added as an in-tree fixture + TestParseNVSRealESPIDFBlobPartitionRoundTrips.
  • Added tests for chunked-blob multi-page layout, oversized-tail-item page bump, and sequence-number-vs-physical-order dedup.

…ad-modify-write

ParseNVS silently dropped or truncated anything it didn't fully model, so
GenerateNVS's re-encode of a parsed partition would wipe real data on a
read-modify-write:

- unknown/unmodeled entry types hit `default: continue` and were dropped
  entirely (e.g. any future or vendor NVS type)
- ESP-IDF's blob-index + chunked blob-data entries (used for esp_wifi
  credentials) share one key across several chunk-indexed slots; keying
  dedup on namespace+key alone collapsed all chunks into one, discarding
  every chunk but the last scanned
- string/blob spans that cross a page boundary were truncated at "assume
  fits in current page", even though GenerateNVS's own writePage already
  produces such spans for long values
- a key whose namespace declaration lived on a later-scanned page than the
  key itself was dropped ("namespace not yet defined")
- GenerateNVS ranged over a Go map to pick namespace write order, making
  output nondeterministic across otherwise-identical calls

Fixes:
- parse.go: two-phase parse — a structural walk decodes every entry record
  (namespace declarations resolved immediately, everything else queued),
  then a resolution phase looks up namespace names against the now-complete
  map, so scan order can no longer drop a key
- parse.go: readSpanData follows an entry's continuation slots across page
  boundaries instead of stopping at the current page's last slot
- parse.go: entries of a type the switch doesn't decode are captured
  generically (Entry.Raw/TypeByte/Span/ChunkIndex/Data) instead of dropped
- parse.go: dedup key is (namespace, key, chunkIndex) instead of
  (namespace, key), so chunked entries sharing a key survive independently
- generate.go: buildRawEntry re-emits Raw entries byte-for-byte from their
  captured type/span/chunkIndex/data, without needing to understand the
  value's semantics
- generate.go: namespace write order follows first-seen order in the input
  slice instead of Go map iteration, making output deterministic

Entry gains Raw/TypeByte/Span/ChunkIndex/Data fields, additive only;
ParseNVS/GenerateNVS signatures are unchanged.

Adds pkg/nvs/lossless_test.go with hand-built raw NVS page fixtures
covering: namespace declared on a later page than its key, unknown-type
passthrough, blob-index/chunked-blob round trip, a blob spanning a page
boundary, GenerateNVS determinism, and a full parse->modify-one-key->
generate->reparse round trip asserting every other entry is
byte-preserved.
@deadprogram

Copy link
Copy Markdown
Member

Thanks for the fixes here @jgangemi now merging.

@deadprogram
deadprogram merged commit d6afc4b into tinygo-org:main Jul 11, 2026
3 checks passed
@jgangemi
jgangemi deleted the jae/nvs-lossless-codec branch July 20, 2026 04:17
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants