settings_upgrade.cpp 9.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268
  1. /**
  2. * Boot-time repair of a settings file written against an older layout version. A member whose
  3. * value form changed cannot be read by the current parser, so the file would be refused and the
  4. * boot would end. Such a member is replaced with the bundled default before the parse.
  5. */
  6. #include "settings_upgrade.h"
  7. #include <array>
  8. #include <cstdint>
  9. #include <cstdio>
  10. #include "settings.h"
  11. namespace sunrise::core::settings::upgrade {
  12. namespace {
  13. /** The layout version member, quoted so a value string cannot match it. */
  14. constexpr std::string_view kVersionMember = "\"version\"";
  15. /** One replaced member, and the layout version that changed it. */
  16. struct ReplacedMember {
  17. /** Quoted member name, so a value string cannot match it. */
  18. std::string_view name;
  19. /** Replaced only while the file is older than this version. */
  20. std::uint32_t version;
  21. };
  22. /**
  23. * Members replaced with the bundled default, each with the version that changed it.
  24. * A member is listed because its value form changed, or because its default changed.
  25. */
  26. constexpr std::array<ReplacedMember, 6> kReplacedMembers{{
  27. {"\"key_bindings\"", 3},
  28. {"\"region_private\"", 5},
  29. {"\"topology\"", 5},
  30. {"\"characters\"", 5},
  31. {"\"profile_items\"", 7},
  32. // Version 8 turned the flat payout list into rows filtered by rarity, gear class and
  33. // masterwork state.
  34. {"\"dismantle_rewards\"", 8},
  35. }};
  36. /** One splice per replaced member, plus the version member itself. */
  37. constexpr std::size_t kSpliceCapacity = kReplacedMembers.size() + 1;
  38. /** Room for the version member and its digits when the file predates versioning. */
  39. constexpr std::size_t kVersionTextCapacity = 32;
  40. /** One region of the document and the text that takes its place. */
  41. struct Splice {
  42. std::size_t start;
  43. std::size_t end;
  44. std::string_view text;
  45. };
  46. /**
  47. * Steps over one JSON string.
  48. * @param text Document text.
  49. * @param position Index of the opening quote.
  50. * @return Index after the closing quote, or the length when it never closes.
  51. */
  52. [[nodiscard]] std::size_t skip_string(std::string_view text, std::size_t position) noexcept {
  53. for (++position; position < text.size(); ++position) {
  54. if (text[position] == '\\') {
  55. ++position;
  56. continue;
  57. }
  58. if (text[position] == '"') {
  59. return position + 1;
  60. }
  61. }
  62. return text.size();
  63. }
  64. /**
  65. * Steps over one JSON value of any type. Strings are skipped whole, so a brace inside one cannot
  66. * close an object.
  67. * @param text Document text.
  68. * @param position Index of the first byte of the value.
  69. * @return Index after the value.
  70. */
  71. [[nodiscard]] std::size_t skip_value(std::string_view text, std::size_t position) noexcept {
  72. if (position >= text.size()) {
  73. return text.size();
  74. }
  75. if (text[position] == '"') {
  76. return skip_string(text, position);
  77. }
  78. if (text[position] != '{' && text[position] != '[') {
  79. std::size_t end = position;
  80. while (end < text.size() && text[end] != ',' && text[end] != '}' && text[end] != ']') {
  81. ++end;
  82. }
  83. // A scalar runs to its separator, so trailing layout belongs to the document, not the
  84. // value.
  85. while (end > position
  86. && (text[end - 1] == ' ' || text[end - 1] == '\t' || text[end - 1] == '\r'
  87. || text[end - 1] == '\n')) {
  88. --end;
  89. }
  90. return end;
  91. }
  92. std::size_t depth = 0;
  93. for (; position < text.size(); ++position) {
  94. const char value = text[position];
  95. if (value == '"') {
  96. position = skip_string(text, position) - 1;
  97. continue;
  98. }
  99. if (value == '{' || value == '[') {
  100. ++depth;
  101. continue;
  102. }
  103. if (value == '}' || value == ']') {
  104. --depth;
  105. if (depth == 0) {
  106. return position + 1;
  107. }
  108. }
  109. }
  110. return text.size();
  111. }
  112. /**
  113. * Finds one member's value inside a document.
  114. * @param text Document text.
  115. * @param member Quoted member name.
  116. * @param start Receives the first byte of the value.
  117. * @param end Receives the byte after the value.
  118. * @return True when the member is present with a value.
  119. */
  120. [[nodiscard]] bool value_span(std::string_view text,
  121. std::string_view member,
  122. std::size_t& start,
  123. std::size_t& end) noexcept {
  124. const std::size_t found = text.find(member);
  125. if (found == std::string_view::npos) {
  126. return false;
  127. }
  128. std::size_t position = found + member.size();
  129. const std::string_view blanks = " \t\r\n";
  130. position = text.find_first_not_of(blanks, position);
  131. if (position == std::string_view::npos || text[position] != ':') {
  132. return false;
  133. }
  134. position = text.find_first_not_of(blanks, position + 1);
  135. if (position == std::string_view::npos) {
  136. return false;
  137. }
  138. start = position;
  139. end = skip_value(text, position);
  140. return end > start;
  141. }
  142. /**
  143. * Reads the layout version a document was written against.
  144. * @param document Document text.
  145. * @return The version, or zero when the member is absent or unreadable.
  146. */
  147. [[nodiscard]] std::uint32_t document_version(std::string_view document) noexcept {
  148. std::size_t start = 0;
  149. std::size_t end = 0;
  150. if (!value_span(document, kVersionMember, start, end)) {
  151. return 0;
  152. }
  153. std::uint32_t version = 0;
  154. for (std::size_t position = start; position < end; ++position) {
  155. const char digit = document[position];
  156. if (digit < '0' || digit > '9') {
  157. return 0;
  158. }
  159. version = version * 10 + static_cast<std::uint32_t>(digit - '0');
  160. }
  161. return version;
  162. }
  163. } // namespace
  164. /** Reports whether the document predates the layout this build reads. */
  165. bool needed(std::string_view document) noexcept {
  166. return document_version(document) < kSettingsVersion;
  167. }
  168. /** Replaces the changed members with the bundled defaults and stamps the current version. */
  169. bool apply(std::string_view document,
  170. std::string_view bundled,
  171. std::span<char> output,
  172. std::size_t& written) noexcept {
  173. written = 0;
  174. std::array<Splice, kSpliceCapacity> splices{};
  175. std::size_t count = 0;
  176. std::array<char, kVersionTextCapacity> versionText{};
  177. std::size_t start = 0;
  178. std::size_t end = 0;
  179. if (value_span(document, kVersionMember, start, end)) {
  180. const int length = std::snprintf(
  181. versionText.data(), versionText.size(), "%u", static_cast<unsigned>(kSettingsVersion));
  182. if (length <= 0) {
  183. return false;
  184. }
  185. splices[count++] = {start, end, {versionText.data(), static_cast<std::size_t>(length)}};
  186. } else {
  187. // A file that predates versioning carries no member to replace, so one is added first.
  188. const std::size_t root = document.find('{');
  189. if (root == std::string_view::npos) {
  190. return false;
  191. }
  192. const int length = std::snprintf(versionText.data(),
  193. versionText.size(),
  194. "\"version\": %u,",
  195. static_cast<unsigned>(kSettingsVersion));
  196. if (length <= 0) {
  197. return false;
  198. }
  199. splices[count++] = {
  200. root + 1, root + 1, {versionText.data(), static_cast<std::size_t>(length)}};
  201. }
  202. const std::uint32_t from = document_version(document);
  203. for (const ReplacedMember& member : kReplacedMembers) {
  204. // A file at or past that version keeps its own value, so a user choice is never lost.
  205. if (from >= member.version) {
  206. continue;
  207. }
  208. std::size_t replacementStart = 0;
  209. std::size_t replacementEnd = 0;
  210. if (!value_span(document, member.name, start, end)
  211. || !value_span(bundled, member.name, replacementStart, replacementEnd)) {
  212. // A member the file never carried needs no replacement.
  213. continue;
  214. }
  215. splices[count++] = {
  216. start, end, bundled.substr(replacementStart, replacementEnd - replacementStart)};
  217. }
  218. for (std::size_t index = 1; index < count; ++index) {
  219. const Splice held = splices[index];
  220. std::size_t position = index;
  221. while (position > 0 && splices[position - 1].start > held.start) {
  222. splices[position] = splices[position - 1];
  223. --position;
  224. }
  225. splices[position] = held;
  226. }
  227. std::size_t read = 0;
  228. for (std::size_t index = 0; index < count; ++index) {
  229. const Splice& splice = splices[index];
  230. const std::size_t copied = splice.start - read;
  231. if (written + copied + splice.text.size() > output.size()) {
  232. return false;
  233. }
  234. document.copy(output.data() + written, copied, read);
  235. written += copied;
  236. splice.text.copy(output.data() + written, splice.text.size());
  237. written += splice.text.size();
  238. read = splice.end;
  239. }
  240. const std::size_t tail = document.size() - read;
  241. if (written + tail > output.size()) {
  242. return false;
  243. }
  244. document.copy(output.data() + written, tail, read);
  245. written += tail;
  246. return true;
  247. }
  248. } // namespace sunrise::core::settings::upgrade