settings_upgrade.cpp 9.0 KB

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