A fork of https://github.com/crosspoint-reader/crosspoint-reader
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

refactor: Simplify new setting introduction (#1086)

## 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:
```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.

---

### 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>**_

authored by

jpirnay and committed by
GitHub
04242fa2 2b25f4d1

+103 -110
+73 -88
src/JsonSettingsIO.cpp
··· 6 6 #include <ObfuscationUtils.h> 7 7 8 8 #include <cstring> 9 + #include <string> 9 10 10 11 #include "CrossPointSettings.h" 11 12 #include "CrossPointState.h" 12 13 #include "KOReaderCredentialStore.h" 13 14 #include "RecentBooksStore.h" 15 + #include "SettingsList.h" 14 16 #include "WifiCredentialStore.h" 15 17 16 18 // Convert legacy settings. ··· 96 98 bool JsonSettingsIO::saveSettings(const CrossPointSettings& s, const char* path) { 97 99 JsonDocument doc; 98 100 99 - doc["sleepScreen"] = s.sleepScreen; 100 - doc["sleepScreenCoverMode"] = s.sleepScreenCoverMode; 101 - doc["sleepScreenCoverFilter"] = s.sleepScreenCoverFilter; 102 - doc["statusBar"] = s.statusBar; 103 - doc["extraParagraphSpacing"] = s.extraParagraphSpacing; 104 - doc["textAntiAliasing"] = s.textAntiAliasing; 105 - doc["shortPwrBtn"] = s.shortPwrBtn; 106 - doc["orientation"] = s.orientation; 107 - doc["sideButtonLayout"] = s.sideButtonLayout; 101 + for (const auto& info : getSettingsList()) { 102 + if (!info.key) continue; 103 + // Dynamic entries (KOReader etc.) are stored in their own files — skip. 104 + if (!info.valuePtr && !info.stringOffset) continue; 105 + 106 + if (info.stringOffset) { 107 + const char* strPtr = (const char*)&s + info.stringOffset; 108 + if (info.obfuscated) { 109 + doc[std::string(info.key) + "_obf"] = obfuscation::obfuscateToBase64(strPtr); 110 + } else { 111 + doc[info.key] = strPtr; 112 + } 113 + } else { 114 + doc[info.key] = s.*(info.valuePtr); 115 + } 116 + } 117 + 118 + // Front button remap — managed by RemapFrontButtons sub-activity, not in SettingsList. 108 119 doc["frontButtonBack"] = s.frontButtonBack; 109 120 doc["frontButtonConfirm"] = s.frontButtonConfirm; 110 121 doc["frontButtonLeft"] = s.frontButtonLeft; 111 122 doc["frontButtonRight"] = s.frontButtonRight; 112 - doc["fontFamily"] = s.fontFamily; 113 - doc["fontSize"] = s.fontSize; 114 - doc["lineSpacing"] = s.lineSpacing; 115 - doc["paragraphAlignment"] = s.paragraphAlignment; 116 - doc["sleepTimeout"] = s.sleepTimeout; 117 - doc["refreshFrequency"] = s.refreshFrequency; 118 - doc["screenMargin"] = s.screenMargin; 119 - doc["opdsServerUrl"] = s.opdsServerUrl; 120 - doc["opdsUsername"] = s.opdsUsername; 121 - doc["opdsPassword_obf"] = obfuscation::obfuscateToBase64(s.opdsPassword); 122 - doc["hideBatteryPercentage"] = s.hideBatteryPercentage; 123 - doc["longPressChapterSkip"] = s.longPressChapterSkip; 124 - doc["hyphenationEnabled"] = s.hyphenationEnabled; 125 - doc["uiTheme"] = s.uiTheme; 126 - doc["fadingFix"] = s.fadingFix; 127 - doc["embeddedStyle"] = s.embeddedStyle; 128 - doc["statusBarChapterPageCount"] = s.statusBarChapterPageCount; 129 - doc["statusBarBookProgressPercentage"] = s.statusBarBookProgressPercentage; 130 - doc["statusBarProgressBar"] = s.statusBarProgressBar; 131 - doc["statusBarTitle"] = s.statusBarTitle; 132 - doc["statusBarBattery"] = s.statusBarBattery; 133 - doc["statusBarProgressBarThickness"] = s.statusBarProgressBarThickness; 134 123 135 124 String json; 136 125 serializeJson(doc, json); ··· 146 135 return false; 147 136 } 148 137 149 - using S = CrossPointSettings; 150 138 auto clamp = [](uint8_t val, uint8_t maxVal, uint8_t def) -> uint8_t { return val < maxVal ? val : def; }; 151 139 152 - s.sleepScreen = clamp(doc["sleepScreen"] | (uint8_t)S::DARK, S::SLEEP_SCREEN_MODE_COUNT, S::DARK); 153 - s.sleepScreenCoverMode = 154 - clamp(doc["sleepScreenCoverMode"] | (uint8_t)S::FIT, S::SLEEP_SCREEN_COVER_MODE_COUNT, S::FIT); 155 - s.sleepScreenCoverFilter = 156 - clamp(doc["sleepScreenCoverFilter"] | (uint8_t)S::NO_FILTER, S::SLEEP_SCREEN_COVER_FILTER_COUNT, S::NO_FILTER); 157 - s.statusBar = clamp(doc["statusBar"] | (uint8_t)S::FULL, S::STATUS_BAR_MODE_COUNT, S::FULL); 158 - s.extraParagraphSpacing = doc["extraParagraphSpacing"] | (uint8_t)1; 159 - s.textAntiAliasing = doc["textAntiAliasing"] | (uint8_t)1; 160 - s.shortPwrBtn = clamp(doc["shortPwrBtn"] | (uint8_t)S::IGNORE, S::SHORT_PWRBTN_COUNT, S::IGNORE); 161 - s.orientation = clamp(doc["orientation"] | (uint8_t)S::PORTRAIT, S::ORIENTATION_COUNT, S::PORTRAIT); 162 - s.sideButtonLayout = 163 - clamp(doc["sideButtonLayout"] | (uint8_t)S::PREV_NEXT, S::SIDE_BUTTON_LAYOUT_COUNT, S::PREV_NEXT); 140 + // Legacy migration: if statusBarChapterPageCount is absent this is a pre-refactor settings file. 141 + // Populate s with migrated values now so the generic loop below picks them up as defaults and clamps them. 142 + if (doc["statusBarChapterPageCount"].isNull()) { 143 + applyLegacyStatusBarSettings(s); 144 + } 145 + 146 + for (const auto& info : getSettingsList()) { 147 + if (!info.key) continue; 148 + // Dynamic entries (KOReader etc.) are stored in their own files — skip. 149 + if (!info.valuePtr && !info.stringOffset) continue; 150 + 151 + if (info.stringOffset) { 152 + const char* strPtr = (const char*)&s + info.stringOffset; 153 + const std::string fieldDefault = strPtr; // current buffer = struct-initializer default 154 + std::string val; 155 + if (info.obfuscated) { 156 + bool ok = false; 157 + val = obfuscation::deobfuscateFromBase64(doc[std::string(info.key) + "_obf"] | "", &ok); 158 + if (!ok || val.empty()) { 159 + val = doc[info.key] | fieldDefault; 160 + if (val != fieldDefault && needsResave) *needsResave = true; 161 + } 162 + } else { 163 + val = doc[info.key] | fieldDefault; 164 + } 165 + char* destPtr = (char*)&s + info.stringOffset; 166 + if (info.stringMaxLen == 0) { 167 + LOG_ERR("CPS", "Misconfigured SettingInfo: stringMaxLen is 0 for key '%s'", info.key); 168 + destPtr[0] = '\0'; 169 + if (needsResave) *needsResave = true; 170 + continue; 171 + } 172 + strncpy(destPtr, val.c_str(), info.stringMaxLen - 1); 173 + destPtr[info.stringMaxLen - 1] = '\0'; 174 + } else { 175 + const uint8_t fieldDefault = s.*(info.valuePtr); // struct-initializer default, read before we overwrite it 176 + uint8_t v = doc[info.key] | fieldDefault; 177 + if (info.type == SettingType::ENUM) { 178 + v = clamp(v, (uint8_t)info.enumValues.size(), fieldDefault); 179 + } else if (info.type == SettingType::TOGGLE) { 180 + v = clamp(v, (uint8_t)2, fieldDefault); 181 + } else if (info.type == SettingType::VALUE) { 182 + if (v < info.valueRange.min) 183 + v = info.valueRange.min; 184 + else if (v > info.valueRange.max) 185 + v = info.valueRange.max; 186 + } 187 + s.*(info.valuePtr) = v; 188 + } 189 + } 190 + 191 + // Front button remap — managed by RemapFrontButtons sub-activity, not in SettingsList. 192 + using S = CrossPointSettings; 164 193 s.frontButtonBack = 165 194 clamp(doc["frontButtonBack"] | (uint8_t)S::FRONT_HW_BACK, S::FRONT_BUTTON_HARDWARE_COUNT, S::FRONT_HW_BACK); 166 195 s.frontButtonConfirm = clamp(doc["frontButtonConfirm"] | (uint8_t)S::FRONT_HW_CONFIRM, S::FRONT_BUTTON_HARDWARE_COUNT, ··· 170 199 s.frontButtonRight = 171 200 clamp(doc["frontButtonRight"] | (uint8_t)S::FRONT_HW_RIGHT, S::FRONT_BUTTON_HARDWARE_COUNT, S::FRONT_HW_RIGHT); 172 201 CrossPointSettings::validateFrontButtonMapping(s); 173 - s.fontFamily = clamp(doc["fontFamily"] | (uint8_t)S::BOOKERLY, S::FONT_FAMILY_COUNT, S::BOOKERLY); 174 - s.fontSize = clamp(doc["fontSize"] | (uint8_t)S::MEDIUM, S::FONT_SIZE_COUNT, S::MEDIUM); 175 - s.lineSpacing = clamp(doc["lineSpacing"] | (uint8_t)S::NORMAL, S::LINE_COMPRESSION_COUNT, S::NORMAL); 176 - s.paragraphAlignment = 177 - clamp(doc["paragraphAlignment"] | (uint8_t)S::JUSTIFIED, S::PARAGRAPH_ALIGNMENT_COUNT, S::JUSTIFIED); 178 - s.sleepTimeout = clamp(doc["sleepTimeout"] | (uint8_t)S::SLEEP_10_MIN, S::SLEEP_TIMEOUT_COUNT, S::SLEEP_10_MIN); 179 - s.refreshFrequency = 180 - clamp(doc["refreshFrequency"] | (uint8_t)S::REFRESH_15, S::REFRESH_FREQUENCY_COUNT, S::REFRESH_15); 181 - s.screenMargin = doc["screenMargin"] | (uint8_t)5; 182 - s.hideBatteryPercentage = 183 - clamp(doc["hideBatteryPercentage"] | (uint8_t)S::HIDE_NEVER, S::HIDE_BATTERY_PERCENTAGE_COUNT, S::HIDE_NEVER); 184 - s.longPressChapterSkip = doc["longPressChapterSkip"] | (uint8_t)1; 185 - s.hyphenationEnabled = doc["hyphenationEnabled"] | (uint8_t)0; 186 - s.uiTheme = doc["uiTheme"] | (uint8_t)S::LYRA; 187 - s.fadingFix = doc["fadingFix"] | (uint8_t)0; 188 - s.embeddedStyle = doc["embeddedStyle"] | (uint8_t)1; 189 - 190 - const char* url = doc["opdsServerUrl"] | ""; 191 - strncpy(s.opdsServerUrl, url, sizeof(s.opdsServerUrl) - 1); 192 - s.opdsServerUrl[sizeof(s.opdsServerUrl) - 1] = '\0'; 193 - 194 - const char* user = doc["opdsUsername"] | ""; 195 - strncpy(s.opdsUsername, user, sizeof(s.opdsUsername) - 1); 196 - s.opdsUsername[sizeof(s.opdsUsername) - 1] = '\0'; 197 202 198 - bool passOk = false; 199 - std::string pass = obfuscation::deobfuscateFromBase64(doc["opdsPassword_obf"] | "", &passOk); 200 - if (!passOk || pass.empty()) { 201 - pass = doc["opdsPassword"] | ""; 202 - if (!pass.empty() && needsResave) *needsResave = true; 203 - } 204 - strncpy(s.opdsPassword, pass.c_str(), sizeof(s.opdsPassword) - 1); 205 - s.opdsPassword[sizeof(s.opdsPassword) - 1] = '\0'; 206 203 LOG_DBG("CPS", "Settings loaded from file"); 207 - 208 - if (doc.containsKey("statusBarChapterPageCount")) { 209 - s.statusBarChapterPageCount = doc["statusBarChapterPageCount"]; 210 - s.statusBarBookProgressPercentage = doc["statusBarBookProgressPercentage"]; 211 - s.statusBarProgressBar = doc["statusBarProgressBar"]; 212 - s.statusBarTitle = doc["statusBarTitle"]; 213 - s.statusBarBattery = doc["statusBarBattery"]; 214 - } else { 215 - applyLegacyStatusBarSettings(s); 216 - } 217 - 218 - s.statusBarProgressBarThickness = doc["statusBarProgressBarThickness"] | (uint8_t)S::PROGRESS_BAR_NORMAL; 219 204 220 205 return true; 221 206 }
+7 -5
src/SettingsList.h
··· 11 11 // Shared settings list used by both the device settings UI and the web settings API. 12 12 // Each entry has a key (for JSON API) and category (for grouping). 13 13 // ACTION-type entries and entries without a key are device-only. 14 - inline std::vector<SettingInfo> getSettingsList() { 15 - return { 14 + inline const std::vector<SettingInfo>& getSettingsList() { 15 + static const std::vector<SettingInfo> list = { 16 16 // --- Display --- 17 17 SettingInfo::Enum(StrId::STR_SLEEP_SCREEN, &CrossPointSettings::sleepScreen, 18 18 {StrId::STR_DARK, StrId::STR_LIGHT, StrId::STR_CUSTOM, StrId::STR_COVER, StrId::STR_NONE_OPT, ··· 113 113 SettingInfo::String(StrId::STR_USERNAME, SETTINGS.opdsUsername, sizeof(SETTINGS.opdsUsername), "opdsUsername", 114 114 StrId::STR_OPDS_BROWSER), 115 115 SettingInfo::String(StrId::STR_PASSWORD, SETTINGS.opdsPassword, sizeof(SETTINGS.opdsPassword), "opdsPassword", 116 - StrId::STR_OPDS_BROWSER), 117 - 116 + StrId::STR_OPDS_BROWSER) 117 + .withObfuscated(), 118 118 // --- Status Bar Settings (web-only, uses StatusBarSettingsActivity) --- 119 119 SettingInfo::Toggle(StrId::STR_CHAPTER_PAGE_COUNT, &CrossPointSettings::statusBarChapterPageCount, 120 120 "statusBarChapterPageCount", StrId::STR_CUSTOMISE_STATUS_BAR), ··· 124 124 {StrId::STR_BOOK, StrId::STR_CHAPTER, StrId::STR_HIDE}, "statusBarProgressBar", 125 125 StrId::STR_CUSTOMISE_STATUS_BAR), 126 126 SettingInfo::Enum(StrId::STR_PROGRESS_BAR_THICKNESS, &CrossPointSettings::statusBarProgressBarThickness, 127 - {StrId::STR_PROGRESS_BAR_THIN, StrId::STR_PROGRESS_BAR_MEDIUM, StrId::STR_PROGRESS_BAR_THICK}), 127 + {StrId::STR_PROGRESS_BAR_THIN, StrId::STR_PROGRESS_BAR_MEDIUM, StrId::STR_PROGRESS_BAR_THICK}, 128 + "statusBarProgressBarThickness", StrId::STR_CUSTOMISE_STATUS_BAR), 128 129 SettingInfo::Enum(StrId::STR_TITLE, &CrossPointSettings::statusBarTitle, 129 130 {StrId::STR_BOOK, StrId::STR_CHAPTER, StrId::STR_HIDE}, "statusBarTitle", 130 131 StrId::STR_CUSTOMISE_STATUS_BAR), 131 132 SettingInfo::Toggle(StrId::STR_BATTERY, &CrossPointSettings::statusBarBattery, "statusBarBattery", 132 133 StrId::STR_CUSTOMISE_STATUS_BAR), 133 134 }; 135 + return list; 134 136 }
+5 -5
src/activities/settings/SettingsActivity.cpp
··· 29 29 controlsSettings.clear(); 30 30 systemSettings.clear(); 31 31 32 - for (auto& setting : getSettingsList()) { 32 + for (const auto& setting : getSettingsList()) { 33 33 if (setting.category == StrId::STR_NONE_OPT) continue; 34 34 if (setting.category == StrId::STR_CAT_DISPLAY) { 35 - displaySettings.push_back(std::move(setting)); 35 + displaySettings.push_back(setting); 36 36 } else if (setting.category == StrId::STR_CAT_READER) { 37 - readerSettings.push_back(std::move(setting)); 37 + readerSettings.push_back(setting); 38 38 } else if (setting.category == StrId::STR_CAT_CONTROLS) { 39 - controlsSettings.push_back(std::move(setting)); 39 + controlsSettings.push_back(setting); 40 40 } else if (setting.category == StrId::STR_CAT_SYSTEM) { 41 - systemSettings.push_back(std::move(setting)); 41 + systemSettings.push_back(setting); 42 42 } 43 43 // Web-only categories (KOReader Sync, OPDS Browser) are skipped for device UI 44 44 }
+9 -4
src/activities/settings/SettingsActivity.h
··· 5 5 #include <string> 6 6 #include <vector> 7 7 8 + #include "CrossPointSettings.h" 8 9 #include "activities/Activity.h" 9 10 #include "util/ButtonNavigator.h" 10 - 11 - class CrossPointSettings; 12 11 13 12 enum class SettingType { TOGGLE, ENUM, ACTION, VALUE, STRING }; 14 13 ··· 40 39 41 40 const char* key = nullptr; // JSON API key (nullptr for ACTION types) 42 41 StrId category = StrId::STR_NONE_OPT; // Category for web UI grouping 42 + bool obfuscated = false; // Save/load via base64 obfuscation (passwords) 43 43 44 44 // Direct char[] string fields (for settings stored in CrossPointSettings) 45 - char* stringPtr = nullptr; 45 + size_t stringOffset = 0; 46 46 size_t stringMaxLen = 0; 47 47 48 48 // Dynamic accessors (for settings stored outside CrossPointSettings, e.g. KOReaderCredentialStore) ··· 51 51 std::function<std::string()> stringGetter; 52 52 std::function<void(const std::string&)> stringSetter; 53 53 54 + SettingInfo& withObfuscated() { 55 + obfuscated = true; 56 + return *this; 57 + } 58 + 54 59 static SettingInfo Toggle(StrId nameId, uint8_t CrossPointSettings::* ptr, const char* key = nullptr, 55 60 StrId category = StrId::STR_NONE_OPT) { 56 61 SettingInfo s; ··· 99 104 SettingInfo s; 100 105 s.nameId = nameId; 101 106 s.type = SettingType::STRING; 102 - s.stringPtr = ptr; 107 + s.stringOffset = (size_t)ptr - (size_t)&SETTINGS; 103 108 s.stringMaxLen = maxLen; 104 109 s.key = key; 105 110 s.category = category;
+9 -8
src/network/CrossPointWebServer.cpp
··· 1033 1033 } 1034 1034 1035 1035 void CrossPointWebServer::handleGetSettings() const { 1036 - auto settings = getSettingsList(); 1036 + const auto& settings = getSettingsList(); 1037 1037 1038 1038 server->setContentLength(CONTENT_LENGTH_UNKNOWN); 1039 1039 server->send(200, "application/json", ""); ··· 1087 1087 doc["type"] = "string"; 1088 1088 if (s.stringGetter) { 1089 1089 doc["value"] = s.stringGetter(); 1090 - } else if (s.stringPtr) { 1091 - doc["value"] = s.stringPtr; 1090 + } else if (s.stringOffset > 0) { 1091 + doc["value"] = reinterpret_cast<const char*>(&SETTINGS) + s.stringOffset; 1092 1092 } 1093 1093 break; 1094 1094 } ··· 1129 1129 return; 1130 1130 } 1131 1131 1132 - auto settings = getSettingsList(); 1132 + const auto& settings = getSettingsList(); 1133 1133 int applied = 0; 1134 1134 1135 - for (auto& s : settings) { 1135 + for (const auto& s : settings) { 1136 1136 if (!s.key) continue; 1137 1137 if (!doc[s.key].is<JsonVariant>()) continue; 1138 1138 ··· 1171 1171 const std::string val = doc[s.key].as<std::string>(); 1172 1172 if (s.stringSetter) { 1173 1173 s.stringSetter(val); 1174 - } else if (s.stringPtr && s.stringMaxLen > 0) { 1175 - strncpy(s.stringPtr, val.c_str(), s.stringMaxLen - 1); 1176 - s.stringPtr[s.stringMaxLen - 1] = '\0'; 1174 + } else if (s.stringOffset > 0 && s.stringMaxLen > 0) { 1175 + char* ptr = reinterpret_cast<char*>(&SETTINGS) + s.stringOffset; 1176 + strncpy(ptr, val.c_str(), s.stringMaxLen - 1); 1177 + ptr[s.stringMaxLen - 1] = '\0'; 1177 1178 } 1178 1179 applied++; 1179 1180 break;