Skip to content

fix: bounds-check module lists and drop <sstream> from JsonSettings - #18

Merged
kinonn merged 1 commit into
kinonn:mainfrom
kinonn-bot:fix/module-list-bounds-and-no-sstream
Sep 3, 2026
Merged

kinonn merged 1 commit into
kinonn:mainfrom
kinonn-bot:fix/module-list-bounds-and-no-sstream

Conversation

@kinonn-bot

Copy link
Copy Markdown

Summary

Ports the two real fixes from jhoff#47 to this fork, rebuilt from first principles rather than cherry-picked — the PR's JsonSettings.cpp hunks do not apply here: this fork has getIntMatrix/putIntMatrix (absent upstream), which also used <sstream>, so the PR as written would not compile on this tree.

1. Bounds-check the module address/offset lists (from the PR)

SplitFlapDisplay::init() and reloadOffsets() sized their loops from numModules but indexed settingAddresses/settingOffsets without checking their length: a short or malformed stored list read past the end of the vector. Now:

  • init(): missing entries fall back to the stock address chain (0x20 + i, the schema default for moduleAddresses) and zero offsets.
  • reloadOffsets(): missing entries keep the module's current offset instead of zeroing it — zeroing would physically re-home modules whose calibration never changed. (The PR's approach would cause that re-home here.)

charOffsets was already guarded in both functions; the web/ESP-NOW call sites (SplitFlapEspNow.cpp, SplitFlapWebServer.cpp) were audited and are already fully bounds-checked.

2. Drop <sstream> from JsonSettings (from the PR, extended)

All four functions — getIntVector, getIntMatrix, putIntVector, putIntMatrix — no longer use std::istringstream/std::ostringstream/std::stoi, which dragged the C++ iostreams and locale machinery (and newlib's wide-char/floating-point printf/scanf families) into the image. Parsing is strtol-based; serialization is String concatenation.

Measured on esp32_c3 (no_ota.csv partition, clean A/B against main@394f331):

before after
Flash 1,235,920 B (58.9%) 1,034,856 B (49.3%)
RAM 53,780 B 49,172 B

-196 KB flash, −4.6 KB RAM.

Behavior changes (deliberate)

The old code threw std::runtime_error on junk/out-of-range values; the new parser is lenient so a malformed stored value can never throw while settings load:

  • Junk between numbers is skipped ("1,x,3" → {1, 3}), never spins.
  • Out-of-range values clamp to 0 in place (position preserved) — keeps the old code's guarantee that absurd input can't produce a bogus large offset.
  • Matrix row splitting on ; preserves the old std::getline semantics exactly: a separator emits the row before it even when empty; an empty tail after the last ; emits nothing — row indexing stays positionally stable.

The bounds checks in (1) replace the exception as the length-safety guarantee, matching the PR's design.

Testing

  • New host test test/jsonsettings_parser_test.cpp (40 checks, all passing) compiles the real src/JsonSettings.cpp against the stubs and drives the parser through the public API: round-trips, whitespace tolerance, empty/trailing separators, junk skipping, overflow clamping, adversarial never-throw inputs, and matrix row semantics.
  • Existing suites still pass: jsonsettings_test (37 checks), jsonsettings_threads_test (hammer PASS), splitflap_module_test (28 checks), motor_scheduler_test, step_math_test.
  • pio run -e esp32_c3 builds clean after the change.

SplitFlapDisplay::init()/reloadOffsets() sized their loops from numModules
but indexed the address/offset vectors without checking their length: a
short or malformed stored list read past the end of the vector. init() now
falls back to the stock address chain (0x20+i) and zero offsets; reload
keeps the module's current offset instead of zeroing it, which would
physically re-home calibrated modules.

getIntVector/getIntMatrix/putIntVector/putIntMatrix no longer use
<sstream>, which dragged the C++ iostreams and locale machinery (and
newlib's wide-char/float printf/scanf families) into the image: strtol and
Arduino String do the same job. esp32_c3 flash drops from 58.9% to 49.3%
of the no_ota partition (-196 KB); RAM -4.6 KB.

The new parser is deliberately lenient where the old std::stoi threw:
junk between numbers is skipped and out-of-range values clamp to 0 in
place, so a malformed value can never throw while settings load. The
bounds checks above replace the throw as the length-safety guarantee.
Matrix row splitting on ';' preserves the old std::getline row semantics.

Host tests: test/jsonsettings_parser_test.cpp covers round-trips,
whitespace, empty entries, junk, overflow clamping and the never-throw
contract against the real JsonSettings.cpp.
@kinonn
kinonn merged commit 748c3a2 into kinonn:main Sep 3, 2026
@kinonn
kinonn deleted the fix/module-list-bounds-and-no-sstream branch September 5, 2026 13:56
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