Skip to content

refactor: Simplify new setting introduction#1086

Merged
daveallie merged 13 commits intocrosspoint-reader:masterfrom
jpirnay:refactor-json
Mar 1, 2026
Merged

refactor: Simplify new setting introduction#1086
daveallie merged 13 commits intocrosspoint-reader:masterfrom
jpirnay:refactor-json

Conversation

@jpirnay
Copy link
Contributor

@jpirnay jpirnay commented Feb 22, 2026

Summary

  • What is the goal of this PR? Eliminate the 3-file / 4-location overhead for adding a new setting. Previously, every new setting required manually editing JsonSettingsIO.cpp in two places (save + load), duplicating knowledge already present in SettingsList.h. After this PR, JsonSettingsIO.cpp never needs to be touched again for standard settings.
  • What changes are included?
    • SettingInfo (in SettingsActivity.h) gains one new field: bool obfuscated (base64 save/load for passwords), with a fluent builder method .withObfuscated(). The previously proposed defaultValue/withDefault() approach was dropped in favour of reading the struct field's own initializer value as the fallback (see below).
    • SettingsList.h entries are annotated with .withObfuscated() on the OPDS password entry. The list is now returned as a static const singleton (const std::vector<SettingInfo>&), so it is constructed exactly once. A missing key/category on the statusBarProgressBarThickness entry was also fixed — it was previously skipped by the generic save loop, so changes were silently lost on restart.
    • JsonSettingsIO::saveSettings and loadSettings replace their ~90 lines of manual per-field code with a single generic loop over getSettingsList(). The loop uses info.key, info.valuePtr/info.stringOffset+info.stringMaxLen (for char-array string fields), info.enumValues.size() (for enum clamping), and info.obfuscated.
    • Default values: instead of a duplicated defaultValue field in SettingInfo, loadSettings reads s.*(info.valuePtr) before overwriting it. Because CrossPointSettings is default-constructed before loadSettings is called, this captures each field's struct initializer value as the JSON-absent fallback. The single source of truth for defaults is CrossPointSettings.h.
    • One post-loop special case remains explicitly: the four frontButton* remap fields (managed by the RemapFrontButtons sub-activity, not in SettingsList) and validateFrontButtonMapping().
    • One pre-loop migration guard handles legacy settings files that predate the status bar refactor: if statusBarChapterPageCount is absent from the JSON, applyLegacyStatusBarSettings() is called first so the generic loop picks up the migrated values as defaults and applies its normal clamping.
    • OPDS password backward-compat migration (plain opdsPassword → obfuscated opdsPassword_obf) is preserved inside the generic obfuscated-string path.

Additional Context

Say we want to add a new bookmarkStyle enum setting with options DOT, LINE, NONE and a default of DOT:

  1. src/CrossPointSettings.h — add enum and member:
enum BOOKMARK_STYLE { BOOKMARK_DOT = 0, BOOKMARK_LINE = 1, BOOKMARK_NONE = 2 };
uint8_t bookmarkStyle = BOOKMARK_DOT;
  1. lib/I18n/translations/english.yaml — add display strings:
STR_BOOKMARK_STYLE: "Bookmark Style"
STR_BOOKMARK_DOT: "Dot"
STR_BOOKMARK_LINE: "Line"

(Other language files will fall back to English if not translated. Run gen_i18n.py to regenerate I18nKeys.h.)

  1. src/SettingsList.h — add one entry in the appropriate category:
SettingInfo::Enum(StrId::STR_BOOKMARK_STYLE, &CrossPointSettings::bookmarkStyle,
                  {StrId::STR_BOOKMARK_DOT, StrId::STR_BOOKMARK_LINE, StrId::STR_NONE_OPT},
                  "bookmarkStyle", StrId::STR_CAT_READER),

That's it — no default annotation needed anywhere, because bookmarkStyle = BOOKMARK_DOT in the struct already provides the fallback. The setting will automatically persist to JSON on save, load with clamping on boot, appear in the device settings UI under the Reader category, and be exposed via the web API — all with no further changes.


AI Usage

While CrossPoint doesn't have restrictions on AI tools in contributing, please be transparent about their usage as it
helps set the right context for reviewers.

Did you use AI tools to help write this code? < PARTIALLY>

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Feb 22, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Replaces explicit field-by-field JSON (de)serialization with a SettingsList-driven loop in JsonSettingsIO, adds per-setting obfuscation metadata and string-offset usage, updates loadSettings to return a needsResave flag, and preserves existing special-case handling for KOReader, Wifi, RecentBooks, and front-button mapping.

Changes

Cohort / File(s) Summary
Settings metadata
src/activities/settings/SettingsActivity.h, src/SettingsList.h
Added SettingInfo::obfuscated and withObfuscated(); replaced stringPtr with stringOffset; changed getSettingsList() to return const std::vector<SettingInfo>& (static list); marked OPDS password as obfuscated.
Settings (de)serialization
src/JsonSettingsIO.cpp
Rewrote saveSettings/loadSettings to iterate getSettingsList(), serialize string/numeric fields via pointers/offsets, support obfuscated string fields (key_obf/base64), apply per-entry length limits and numeric clamping, set needsResave when defaults or deobfuscation used; kept KOReader/Wifi/RecentBooks and front-button mapping as explicit special cases; updated loadSettings signature to accept bool* needsResave.
UI / server iteration changes
src/activities/settings/SettingsActivity.cpp, src/network/CrossPointWebServer.cpp
Switched loops to use const references and stopped moving elements when categorizing settings; use const-ref to settings list in server handlers to avoid copies.

Sequence Diagram(s)

mermaid
sequenceDiagram
participant UI as Settings UI
participant SL as SettingsList
participant IO as JsonSettingsIO
participant CP as CrossPointSettings
UI->>SL: request settings list
SL-->>UI: return const reference to static list
UI->>IO: saveSettings(CP -> json)
IO->>CP: read values via pointers/offsets
IO->>IO: obfuscate marked strings / clamp numeric values
IO->>IO: write JSON
UI->>IO: loadSettings(json)
IO->>IO: parse JSON, deobfuscate marked strings
IO->>CP: write parsed/clamped values via pointers/offsets
IO-->>UI: return needsResave flag if defaults/deobfuscation occurred

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Suggested reviewers

  • daveallie
🚥 Pre-merge checks | ✅ 2
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title 'refactor: Simplify new setting introduction' accurately describes the main objective of eliminating duplication in the settings addition workflow.
Description check ✅ Passed The PR description is directly related to the changeset, detailing the refactoring of settings management to eliminate per-field duplication in JsonSettingsIO.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

📜 Review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between ecb5b1b and fe49fd0.

📒 Files selected for processing (3)
  • src/JsonSettingsIO.cpp
  • src/SettingsList.h
  • src/activities/settings/SettingsActivity.h
🧰 Additional context used
🧬 Code graph analysis (1)
src/JsonSettingsIO.cpp (2)
src/SettingsList.h (3)
  • string (99-102)
  • string (106-109)
  • string (113-116)
lib/Serialization/ObfuscationUtils.cpp (4)
  • obfuscateToBase64 (41-46)
  • obfuscateToBase64 (41-41)
  • deobfuscateFromBase64 (48-74)
  • deobfuscateFromBase64 (48-48)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
  • GitHub Check: cppcheck
  • GitHub Check: build
🔇 Additional comments (4)
src/activities/settings/SettingsActivity.h (1)

42-62: LGTM!

The new defaultValue and obfuscated fields with their fluent setters follow a clean builder pattern. The implementation correctly returns *this for chaining, and the default initializations are appropriate.

src/SettingsList.h (1)

14-135: LGTM!

The default values are semantically appropriate, and .withObfuscated() is correctly applied only to the OPDS password (KOReader credentials are handled separately in their own file storage).

src/JsonSettingsIO.cpp (2)

52-66: LGTM!

The generic save loop correctly handles both string and value types, with proper obfuscation suffix handling for sensitive fields.


95-108: LGTM!

The obfuscation fallback logic for backward compatibility is well-implemented. The needsResave flag is correctly set only when migrating from plain to obfuscated format, and string copying safely handles buffer limits with proper null termination.

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/JsonSettingsIO.cpp`:
- Around line 109-115: The code reads a raw uint8_t v from doc[info.key] and
only clamps ENUMs; update the load logic around v so VALUE types are clamped to
their configured range (use info.minValue and info.maxValue or equivalent fields
on info) and TOGGLE types are constrained to 0 or 1 before assigning
s.*(info.valuePtr); keep the existing ENUM clamp (clamp(v,
(uint8_t)info.enumValues.size(), info.defaultValue)) and ensure the initial read
uses the intended fallback (don’t rely on bitwise OR) so the value is validated
then assigned to s.*(info.valuePtr) using SettingType, info, and clamp().

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
src/JsonSettingsIO.cpp (1)

90-123: Consider marking needsResave when defaults/clamps are applied.
Right now only obfuscation fallback triggers resave, so missing/out-of-range numeric values are corrected in memory but not persisted. Marking needsResave in those cases would let the file self-heal.

💡 Possible adjustment
-      uint8_t v = doc[info.key] | info.defaultValue;
+      bool corrected = !doc.containsKey(info.key);
+      uint8_t v = doc[info.key] | info.defaultValue;
       if (info.type == SettingType::ENUM) {
-        v = clamp(v, (uint8_t)info.enumValues.size(), info.defaultValue);
+        uint8_t clamped = clamp(v, (uint8_t)info.enumValues.size(), info.defaultValue);
+        corrected = corrected || (clamped != v);
+        v = clamped;
       } else if (info.type == SettingType::TOGGLE) {
-        v = clamp(v, (uint8_t)2, info.defaultValue);
+        uint8_t clamped = clamp(v, (uint8_t)2, info.defaultValue);
+        corrected = corrected || (clamped != v);
+        v = clamped;
       } else if (info.type == SettingType::VALUE) {
-        if (v < info.valueRange.min)
-          v = info.valueRange.min;
-        else if (v > info.valueRange.max)
-          v = info.valueRange.max;
+        if (v < info.valueRange.min) {
+          v = info.valueRange.min;
+          corrected = true;
+        } else if (v > info.valueRange.max) {
+          v = info.valueRange.max;
+          corrected = true;
+        }
       }
+      if (corrected && needsResave) *needsResave = true;
       s.*(info.valuePtr) = v;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/JsonSettingsIO.cpp` around lines 90 - 123, When reading numeric settings
in the loop returned by getSettingsList(), if a setting's value is replaced by
its default or clamped into range you should set *needsResave = true so the
corrected value is persisted; specifically, in the else branch where you compute
uint8_t v = doc[info.key] | info.defaultValue and then adjust v for
SettingType::ENUM (using info.enumValues.size()), SettingType::TOGGLE, or
SettingType::VALUE (using info.valueRange.min/max), detect when the final v
differs from the raw document value (e.g. compare to doc[info.key] or the
default that was used) and if so set *needsResave = true before assigning into s
through the pointer (s.*(info.valuePtr)).
📜 Review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between fe49fd0 and a659b43.

📒 Files selected for processing (1)
  • src/JsonSettingsIO.cpp
🧰 Additional context used
🧬 Code graph analysis (1)
src/JsonSettingsIO.cpp (2)
src/SettingsList.h (3)
  • string (99-102)
  • string (106-109)
  • string (113-116)
lib/Serialization/ObfuscationUtils.cpp (4)
  • obfuscateToBase64 (41-46)
  • obfuscateToBase64 (41-41)
  • deobfuscateFromBase64 (48-74)
  • deobfuscateFromBase64 (48-48)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
  • GitHub Check: build
  • GitHub Check: cppcheck
🔇 Additional comments (3)
src/JsonSettingsIO.cpp (3)

9-15: LGTM — required includes for std::string and SettingsList.


125-136: LGTM — front-button mapping remains explicit and validated.


52-66: String settings binding is inconsistent with numeric settings. In saveSettings(), enum/toggle values use the passed s parameter via s.*(info.valuePtr), but string values use hardcoded SETTINGS pointers via direct info.stringPtr access. While this works because saveSettings() is only called with the global SETTINGS instance, the inconsistent pattern creates fragility: the function signature suggests it can serialize any CrossPointSettings instance, but strings always serialize from the global SETTINGS regardless of the s parameter. Align string binding to the same parameterized pattern as numeric values to avoid future bugs.

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@src/JsonSettingsIO.cpp`:
- Around line 90-123: When reading numeric settings in the loop returned by
getSettingsList(), if a setting's value is replaced by its default or clamped
into range you should set *needsResave = true so the corrected value is
persisted; specifically, in the else branch where you compute uint8_t v =
doc[info.key] | info.defaultValue and then adjust v for SettingType::ENUM (using
info.enumValues.size()), SettingType::TOGGLE, or SettingType::VALUE (using
info.valueRange.min/max), detect when the final v differs from the raw document
value (e.g. compare to doc[info.key] or the default that was used) and if so set
*needsResave = true before assigning into s through the pointer
(s.*(info.valuePtr)).

@jpirnay jpirnay requested review from ngxson and znelson February 24, 2026 13:53
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/JsonSettingsIO.cpp`:
- Around line 95-122: String settings currently default to empty string when the
JSON key is absent, overwriting in-memory defaults; capture the existing string
buffer as the field default before reading JSON and use that as the fallback
value. Specifically, read a std::string fieldDefault =
std::string(info.stringPtr) (or "" if null) at the start of the info.stringPtr
branch, then replace occurrences of doc[info.key] | "" with doc[info.key] |
fieldDefault (and similarly for the obfuscated path using
obfuscation::deobfuscateFromBase64 and needsResave logic), so the
strncpy(info.stringPtr, ...) only overwrites when the JSON provides a value.

ℹ️ Review info

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between a659b43 and c313761.

📒 Files selected for processing (5)
  • src/JsonSettingsIO.cpp
  • src/SettingsList.h
  • src/activities/settings/SettingsActivity.cpp
  • src/activities/settings/SettingsActivity.h
  • src/network/CrossPointWebServer.cpp
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/activities/settings/SettingsActivity.h
📜 Review details
🧰 Additional context used
🧠 Learnings (1)
📚 Learning: 2026-02-23T06:18:08.408Z
Learnt from: raygan
Repo: crosspoint-reader/crosspoint-reader PR: 1116
File: src/activities/settings/WallabagSettingsActivity.cpp:57-75
Timestamp: 2026-02-23T06:18:08.408Z
Learning: When navigating from a settings activity to KeyboardEntryActivity (e.g., KOReaderSettingsActivity, WallabagSettingsActivity), call exitActivity() on the parent before calling enterNewActivity(new KeyboardEntryActivity(...)). The KeyboardEntryActivity should then call exitActivity() via its callbacks to return to the parent. Note: ConfirmationActivity does not require exitActivity() before entering KeyboardEntryActivity. Apply this pattern to similar settings activities in this module.

Applied to files:

  • src/activities/settings/SettingsActivity.cpp
🧬 Code graph analysis (1)
src/JsonSettingsIO.cpp (1)
lib/Serialization/ObfuscationUtils.cpp (4)
  • obfuscateToBase64 (41-46)
  • obfuscateToBase64 (41-41)
  • deobfuscateFromBase64 (48-74)
  • deobfuscateFromBase64 (48-48)
🔇 Additional comments (5)
src/SettingsList.h (1)

14-15: LGTM: shared list + obfuscated OPDS password flag.
Static list avoids per-call rebuilds, and the obfuscation metadata lines up with the new save/load path.

Also applies to: 121-125

src/activities/settings/SettingsActivity.cpp (1)

31-41: LGTM: const iteration and safe copies into category vectors.
This keeps the shared list immutable while building UI-local collections.

src/network/CrossPointWebServer.cpp (1)

1036-1036: LGTM: const reference for settings list in GET/POST handlers.
Avoids needless copies while preserving behavior.

Also applies to: 1132-1136

src/JsonSettingsIO.cpp (2)

8-16: No review needed for include adjustments.


52-69: LGTM: SettingsList-driven serialization + obfuscated string write.
The loop cleanly handles string vs. numeric settings while skipping dynamic entries.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (1)
src/JsonSettingsIO.cpp (1)

90-125: Good job addressing the previous review feedback.

The fieldDefault pattern at lines 96 and 111 correctly preserves struct-initializer defaults when JSON keys are absent, and the per-type clamping (ENUM → default, TOGGLE → default, VALUE → boundary) is a sensible approach.

One minor defensive concern: if a SettingsList entry ever has stringMaxLen == 0, line 108's strncpy(..., info.stringMaxLen - 1) underflows to SIZE_MAX on unsigned types, causing a buffer overrun. Consider guarding:

🛡️ Optional defensive guard
+    if (info.stringMaxLen == 0) continue;
     strncpy(info.stringPtr, val.c_str(), info.stringMaxLen - 1);
     info.stringPtr[info.stringMaxLen - 1] = '\0';
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/JsonSettingsIO.cpp` around lines 90 - 125, The loop writes into
info.stringPtr using strncpy with info.stringMaxLen-1 which underflows when
info.stringMaxLen == 0; before the strncpy call in the stringPtr branch (inside
getSettingsList() iteration, including the obfuscated path that reads
doc[info.key+"_obf"]), add a guard that if info.stringMaxLen == 0 then skip
writing to info.stringPtr (or treat as a zero-length buffer) and avoid using
strncpy, and ensure you still handle needsResave/val logic as before; this
prevents the unsigned underflow and a potential buffer overrun while preserving
the existing default/obfuscation behavior.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/JsonSettingsIO.cpp`:
- Around line 52-66: The saveSettings implementation mixes raw char*
(info.stringPtr) that point into the singleton CrossPointSettings buffers with
member pointers (info.valuePtr), meaning JsonSettingsIO::saveSettings(s) will
serialize strings from the singleton even if s is a different CrossPointSettings
instance; fix this by making the pointer semantics consistent: either change
SettingsList/getSettingsList entries so string fields use member pointers (e.g.,
store a pointer-to-member for string fields and dereference from the passed `s`)
or require and assert that saveSettings only ever receives the singleton (add an
explicit check/assert that &s == &CrossPointSettings::instance() or document the
invariant), and update uses of info.stringPtr and info.valuePtr in
JsonSettingsIO::saveSettings accordingly so both string and numeric fields are
read from the same instance.

---

Nitpick comments:
In `@src/JsonSettingsIO.cpp`:
- Around line 90-125: The loop writes into info.stringPtr using strncpy with
info.stringMaxLen-1 which underflows when info.stringMaxLen == 0; before the
strncpy call in the stringPtr branch (inside getSettingsList() iteration,
including the obfuscated path that reads doc[info.key+"_obf"]), add a guard that
if info.stringMaxLen == 0 then skip writing to info.stringPtr (or treat as a
zero-length buffer) and avoid using strncpy, and ensure you still handle
needsResave/val logic as before; this prevents the unsigned underflow and a
potential buffer overrun while preserving the existing default/obfuscation
behavior.

ℹ️ Review info

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between c313761 and 933b19b.

📒 Files selected for processing (1)
  • src/JsonSettingsIO.cpp
📜 Review details
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
  • GitHub Check: cppcheck
  • GitHub Check: build
🧰 Additional context used
🧬 Code graph analysis (1)
src/JsonSettingsIO.cpp (1)
lib/Serialization/ObfuscationUtils.cpp (4)
  • obfuscateToBase64 (41-46)
  • obfuscateToBase64 (41-41)
  • deobfuscateFromBase64 (48-74)
  • deobfuscateFromBase64 (48-48)
🔇 Additional comments (2)
src/JsonSettingsIO.cpp (2)

9-15: LGTM!

New includes are correct and necessary for the refactored loop-based approach.


127-138: LGTM!

The front-button remap fields are correctly kept as explicit special cases with their own clamping and validation, consistent with the PR objectives.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (2)
src/activities/settings/SettingsActivity.h (1)

107-107: Prefer uintptr_t over size_t for pointer-to-integer arithmetic.

size_t is not guaranteed to be large enough to hold a pointer on all platforms (though it is on most 32-bit embedded targets). uintptr_t from <cstdint> is the standard-defined type for this purpose.

♻️ Suggested fix
-    s.stringOffset = (size_t)ptr - (size_t)&SETTINGS;
+    s.stringOffset = reinterpret_cast<uintptr_t>(ptr) - reinterpret_cast<uintptr_t>(&SETTINGS);

(Also ensure #include <cstdint> is present, or that uintptr_t is available via another header.)

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/activities/settings/SettingsActivity.h` at line 107, The current
pointer-to-integer conversion uses size_t which may not be large enough on all
platforms; change the calculation of s.stringOffset to use uintptr_t by casting
ptr and &SETTINGS to uintptr_t (i.e., s.stringOffset = (uintptr_t)ptr -
(uintptr_t)&SETTINGS) and ensure <cstdint> is included so uintptr_t is
available; update the code referencing s.stringOffset, ptr and SETTINGS
accordingly.
src/JsonSettingsIO.cpp (1)

97-98: Consider using destPtr consistently instead of computing two pointers from the same offset.

Lines 97 and 110 both compute a pointer from info.stringOffset — one const for reading the default, one non-const for writing. This is fine for correctness, but you could compute it once as char* and use it for both purposes.

♻️ Minor simplification
     if (info.stringOffset) {
-      const char* strPtr = (const char*)&s + info.stringOffset;
-      const std::string fieldDefault = strPtr;  // current buffer = struct-initializer default
+      char* fieldPtr = (char*)&s + info.stringOffset;
+      const std::string fieldDefault(fieldPtr);  // current buffer = struct-initializer default
       std::string val;
       ...
-      char* destPtr = (char*)&s + info.stringOffset;
-      strncpy(destPtr, val.c_str(), info.stringMaxLen - 1);
-      destPtr[info.stringMaxLen - 1] = '\0';
+      strncpy(fieldPtr, val.c_str(), info.stringMaxLen - 1);
+      fieldPtr[info.stringMaxLen - 1] = '\0';
     }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/JsonSettingsIO.cpp` around lines 97 - 98, Compute the pointer from
info.stringOffset only once and reuse it for both reading the default and
writing the destination instead of creating two separate pointers; for example,
replace separate uses of "const char* strPtr = (const char*)&s +
info.stringOffset" and the later non-const "destPtr" by a single "char* basePtr
= (char*)&s + info.stringOffset" (or similar name) and then use a const
cast/view when reading the default and the non-const pointer when writing, so
the same offset calculation is not duplicated.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/activities/settings/SettingsActivity.h`:
- Around line 45-46: The field stringOffset in CrossPointSettings is using 0 as
a sentinel which hides a valid offset==0; change stringOffset from size_t to an
explicit optional (e.g., std::optional<size_t> stringOffset) or add a separate
bool (e.g., bool hasString) and leave stringOffset as a raw value, then update
the consumers (the checks in JsonSettingsIO.cpp that currently do
!info.stringOffset on lines that reference stringOffset) to test
has_value()/hasString instead; ensure stringMaxLen handling remains unchanged
and document the invariant if you choose the bool approach.

In `@src/JsonSettingsIO.cpp`:
- Around line 96-112: The code assumes info.stringMaxLen > 0 when
info.stringOffset is non-zero but doesn't guard against a zero max length, which
causes underflow in the strncpy size calculation; add a defensive check right
before computing destPtr/strncpy (in the block that uses info.stringOffset,
info.stringMaxLen, strncpy and destPtr) that when info.stringMaxLen == 0 you
avoid the oversized copy: make a safe fallback (e.g. treat effective maxLen as 1
so only a terminating NUL is written), ensure destPtr[0] = '\0', and mark
needsResave or log the misconfigured setting so the bad SettingInfo entry can be
fixed; keep all changes local to that branch and reference info.stringMaxLen and
destPtr/strncpy locations.

---

Nitpick comments:
In `@src/activities/settings/SettingsActivity.h`:
- Line 107: The current pointer-to-integer conversion uses size_t which may not
be large enough on all platforms; change the calculation of s.stringOffset to
use uintptr_t by casting ptr and &SETTINGS to uintptr_t (i.e., s.stringOffset =
(uintptr_t)ptr - (uintptr_t)&SETTINGS) and ensure <cstdint> is included so
uintptr_t is available; update the code referencing s.stringOffset, ptr and
SETTINGS accordingly.

In `@src/JsonSettingsIO.cpp`:
- Around line 97-98: Compute the pointer from info.stringOffset only once and
reuse it for both reading the default and writing the destination instead of
creating two separate pointers; for example, replace separate uses of "const
char* strPtr = (const char*)&s + info.stringOffset" and the later non-const
"destPtr" by a single "char* basePtr = (char*)&s + info.stringOffset" (or
similar name) and then use a const cast/view when reading the default and the
non-const pointer when writing, so the same offset calculation is not
duplicated.

ℹ️ Review info

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 933b19b and 8bbc4ac.

📒 Files selected for processing (2)
  • src/JsonSettingsIO.cpp
  • src/activities/settings/SettingsActivity.h
📜 Review details
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
  • GitHub Check: build
  • GitHub Check: cppcheck
🔇 Additional comments (4)
src/activities/settings/SettingsActivity.h (1)

54-57: LGTM — clean fluent builder for obfuscation flag.

Simple, idiomatic, and consistent with other builder patterns.

src/JsonSettingsIO.cpp (3)

52-67: Clean generic save loop — good refactoring.

The loop correctly handles all three paths (skip dynamic, string with obfuscation, numeric via member pointer) and the offset-based string access properly uses the passed-in s reference, resolving the prior asymmetry concern.


91-128: Well-structured generic load loop with proper clamping for all numeric types.

The clamping for ENUM, TOGGLE, and VALUE types properly addresses prior review feedback. The fieldDefault pattern correctly preserves struct-initializer defaults when JSON keys are absent.

One minor observation on lines 120–124: VALUE clamping enforces min/max but doesn't snap to valueRange.step boundaries. A value like 7 would pass for a range of 5–40 with step 5. This is likely fine if step is only used for UI increment, but worth noting.


130-140: Front button special-case handling looks correct.

The explicit handling outside the loop with per-field defaults and validateFrontButtonMapping is appropriate given these fields are managed by a separate sub-activity.

@jpirnay
Copy link
Contributor Author

jpirnay commented Feb 27, 2026

Needs rework

@jpirnay jpirnay closed this Feb 27, 2026
@jpirnay jpirnay reopened this Feb 27, 2026
Copy link
Contributor

@znelson znelson left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Tedious stuff, but really helpful in the long run. Thanks for taking it on!

@daveallie daveallie merged commit 04242fa into crosspoint-reader:master Mar 1, 2026
13 checks passed
@jpirnay jpirnay deleted the refactor-json branch March 1, 2026 09:10
laird pushed a commit to laird/crosspoint-claw that referenced this pull request Mar 1, 2026
* **What is the goal of this PR?** Eliminate the 3-file / 4-location
overhead for adding a new setting. Previously, every new setting
required manually editing JsonSettingsIO.cpp in two places (save +
load), duplicating knowledge already present in SettingsList.h. After
this PR, JsonSettingsIO.cpp never needs to be touched again for standard
settings.
* **What changes are included?**
* `SettingInfo` (in `SettingsActivity.h`) gains one new field: `bool
obfuscated` (base64 save/load for passwords), with a fluent builder
method `.withObfuscated()`. The previously proposed
`defaultValue`/`withDefault()` approach was dropped in favour of reading
the struct field's own initializer value as the fallback (see below).
* `SettingsList.h` entries are annotated with `.withObfuscated()` on the
OPDS password entry. The list is now returned as a `static const`
singleton (`const std::vector<SettingInfo>&`), so it is constructed
exactly once. A missing `key`/`category` on the
`statusBarProgressBarThickness` entry was also fixed — it was previously
skipped by the generic save loop, so changes were silently lost on
restart.
* `JsonSettingsIO::saveSettings` and `loadSettings` replace their ~90
lines of manual per-field code with a single generic loop over
`getSettingsList()`. The loop uses `info.key`,
`info.valuePtr`/`info.stringOffset`+`info.stringMaxLen` (for char-array
string fields), `info.enumValues.size()` (for enum clamping), and
`info.obfuscated`.
* **Default values**: instead of a duplicated `defaultValue` field in
`SettingInfo`, `loadSettings` reads `s.*(info.valuePtr)` *before*
overwriting it. Because `CrossPointSettings` is default-constructed
before `loadSettings` is called, this captures each field's struct
initializer value as the JSON-absent fallback. The single source of
truth for defaults is `CrossPointSettings.h`.
* One post-loop special case remains explicitly: the four `frontButton*`
remap fields (managed by the RemapFrontButtons sub-activity, not in
SettingsList) and `validateFrontButtonMapping()`.
* One pre-loop migration guard handles legacy settings files that
predate the status bar refactor: if `statusBarChapterPageCount` is
absent from the JSON, `applyLegacyStatusBarSettings()` is called first
so the generic loop picks up the migrated values as defaults and applies
its normal clamping.
* OPDS password backward-compat migration (plain `opdsPassword` →
obfuscated `opdsPassword_obf`) is preserved inside the generic
obfuscated-string path.

Say we want to add a new `bookmarkStyle` enum setting with options
`DOT`, `LINE`, `NONE` and a default of `DOT`:

1. `src/CrossPointSettings.h` — add enum and member:
```cpp
enum BOOKMARK_STYLE { BOOKMARK_DOT = 0, BOOKMARK_LINE = 1, BOOKMARK_NONE = 2 };
uint8_t bookmarkStyle = BOOKMARK_DOT;
```

2. `lib/I18n/translations/english.yaml` — add display strings:
```yaml
STR_BOOKMARK_STYLE: "Bookmark Style"
STR_BOOKMARK_DOT: "Dot"
STR_BOOKMARK_LINE: "Line"
```
(Other language files will fall back to English if not translated. Run
`gen_i18n.py` to regenerate `I18nKeys.h`.)

3. `src/SettingsList.h` — add one entry in the appropriate category:
```cpp
SettingInfo::Enum(StrId::STR_BOOKMARK_STYLE, &CrossPointSettings::bookmarkStyle,
                  {StrId::STR_BOOKMARK_DOT, StrId::STR_BOOKMARK_LINE, StrId::STR_NONE_OPT},
                  "bookmarkStyle", StrId::STR_CAT_READER),
```

That's it — no default annotation needed anywhere, because
`bookmarkStyle = BOOKMARK_DOT` in the struct already provides the
fallback. The setting will automatically persist to JSON on save, load
with clamping on boot, appear in the device settings UI under the Reader
category, and be exposed via the web API — all with no further changes.

---

While CrossPoint doesn't have restrictions on AI tools in contributing,
please be transparent about their usage as it
helps set the right context for reviewers.

Did you use AI tools to help write this code? _**< PARTIALLY>**_
laird added a commit to laird/crosspoint-claw that referenced this pull request Mar 1, 2026
…sspoint-reader#1086 followup)

The static const initializer_list pattern from upstream crosspoint-reader#1086 causes a
Guru Meditation stack protection fault in loopTask on first settings
access — all 39 SettingInfo temporaries are constructed simultaneously
during static init, overflowing the 8KB loopTask stack.

Reverts to the push_back + reserve(39) pattern from a6a078b while
preserving all additions from crosspoint-reader#1086: fork entries (feedUrl, dangerZone),
.withObfuscated() on opdsPassword and dangerZonePassword, and the
statusBarProgressBarThickness key/category fix.

Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
laird added a commit to laird/crosspoint-claw that referenced this pull request Mar 1, 2026
Cherry-picked upstream fixes and features applied via rescue-1.0.0 worktree:
- fix: properly implement requestUpdateAndWait() (crosspoint-reader#1218)
- fix: Hide unusable button hints in empty directory (crosspoint-reader#1253)
- fix: add missing keyboard metrics to Lyra3CoversTheme (crosspoint-reader#1101)
- fix: remove bookProgressBarHeight from Lyra3CoversTheme
- feat: replace picojpeg with JPEGDEC for JPEG decoding (crosspoint-reader#1136)
- feat: WIFI pill, feed log fix, JPEGDEC version string
- feat: Add git branch to version string (crosspoint-reader#1225)
- fix: navigate directly to QR code after DZ auto-connect
- perf: Removed unused ConfirmationActivity member (crosspoint-reader#1234)
- refactor: Simplify new setting introduction (crosspoint-reader#1086)
- fix: UI fonts, settings stack overflow, PULSR theme name
- fix: convert SettingsList to push_back (prevent stack overflow)

All commits built and verified on device (confirmed 1.1.0-dev+d1e786a).
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.

4 participants