yaze 0.3.2
Link to the Past ROM Editor
 
Loading...
Searching...
No Matches
catalog_palette_preview.cc
Go to the documentation of this file.
2
3#include <algorithm>
4#include <cctype>
5#include <filesystem>
6#include <fstream>
7#include <map>
8#include <stdexcept>
9#include <utility>
10
11#include "absl/status/status.h"
13#include "nlohmann/json.hpp"
14#include "util/macro.h"
15
16namespace yaze::editor {
17namespace {
18constexpr size_t kJsonBytes = 8 * 1024 * 1024;
19constexpr size_t kSourceBytes = 16 * 1024 * 1024;
20constexpr size_t kTotalSourceBytes = 128 * 1024 * 1024;
21constexpr size_t kMaxSources = 64;
22constexpr size_t kMaxRows = 4096;
23
24absl::StatusOr<std::string> ReadBoundedFile(const std::filesystem::path& path,
25 size_t limit) {
26 std::error_code error;
27 if (!std::filesystem::is_regular_file(path, error) || error) {
28 return absl::InvalidArgumentError("Not a readable palette/source file: " +
29 path.string());
30 }
31 const auto size = std::filesystem::file_size(path, error);
32 if (error || size > limit) {
33 return absl::InvalidArgumentError(
34 "Palette/source file exceeds byte limit: " + path.string());
35 }
36 std::ifstream input(path, std::ios::binary);
37 std::string bytes(static_cast<size_t>(size), '\0');
38 if (!input || !input.read(bytes.data(), static_cast<std::streamsize>(size)) ||
39 input.peek() != std::char_traits<char>::eof()) {
40 return absl::DataLossError("Cannot read complete palette/source file: " +
41 path.string());
42 }
43 return bytes;
44}
45
46bool ValidText(const std::string& text, size_t limit) {
47 return !text.empty() && text.size() <= limit &&
48 text.find('\0') == std::string::npos;
49}
50} // namespace
51
52absl::StatusOr<std::vector<CatalogPreviewPalette>> LoadCatalogPreviewPalettes(
53 const std::string& catalog_filename) {
54 try {
55 const auto palette_file =
56 std::filesystem::absolute(catalog_filename).parent_path() / "palettes" /
57 "rom-palettes.json";
58 std::error_code error;
59 const bool exists = std::filesystem::exists(palette_file, error);
60 if (error) {
61 return absl::InvalidArgumentError("Cannot inspect palette file: " +
62 error.message());
63 }
64 if (!exists)
65 return std::vector<CatalogPreviewPalette>{};
66 ASSIGN_OR_RETURN(auto bytes, ReadBoundedFile(palette_file, kJsonBytes));
67 const auto document = nlohmann::json::parse(
68 bytes, [](int depth, nlohmann::json::parse_event_t, nlohmann::json&) {
69 if (depth > 32)
70 throw std::invalid_argument("Palette JSON exceeds depth limit");
71 return true;
72 });
73 if (!document.is_object() ||
74 !document.at("schema_version").is_number_integer() ||
75 document.at("schema_version") != 1 ||
76 !document.at("sources").is_array() ||
77 document.at("sources").size() > kMaxSources) {
78 return absl::InvalidArgumentError(
79 "Expected bounded schema_version 1 palette sources");
80 }
81 std::vector<CatalogPreviewPalette> palettes;
82 std::map<std::filesystem::path, std::string> hashes;
83 size_t total_bytes = 0;
84 for (const auto& source : document.at("sources")) {
85 const auto name = source.at("path").get<std::string>();
86 const auto title = source.at("title").get<std::string>();
87 auto expected = source.at("sha256").get<std::string>();
88 const auto& rows = source.at("rows");
89 if (!ValidText(name, 4096) || !ValidText(title, 1024) ||
90 !rows.is_array() || rows.size() > kMaxRows - palettes.size() ||
91 expected.size() != 64 ||
92 !std::all_of(expected.begin(), expected.end(),
93 [](unsigned char c) { return std::isxdigit(c); })) {
94 return absl::InvalidArgumentError(
95 "Invalid palette source metadata/count/hash");
96 }
97 std::transform(expected.begin(), expected.end(), expected.begin(),
98 [](unsigned char c) { return std::tolower(c); });
99 const auto path = (palette_file.parent_path() / name).lexically_normal();
100 if (!hashes.contains(path)) {
101 ASSIGN_OR_RETURN(auto rom, ReadBoundedFile(path, kSourceBytes));
102 if (rom.size() > kTotalSourceBytes - total_bytes) {
103 return absl::InvalidArgumentError(
104 "Palette source byte budget exceeded");
105 }
106 total_bytes += rom.size();
107 hashes.emplace(path, core::ComputeSourceArtifactSha256(rom));
108 }
109 if (hashes.at(path) != expected) {
110 return absl::DataLossError("Palette source ROM SHA256 mismatch: " +
111 path.string());
112 }
113 for (const auto& row : rows) {
114 const auto label = row.at("label").get<std::string>();
115 if (row.contains("bgr5558") && row.contains("bgr555") &&
116 row.at("bgr5558") != row.at("bgr555")) {
117 return absl::InvalidArgumentError("Conflicting palette word fields");
118 }
119 const auto& words =
120 row.contains("bgr5558") ? row.at("bgr5558") : row.at("bgr555");
121 if (!ValidText(label, 1024) || !words.is_array() || words.size() != 8) {
122 return absl::InvalidArgumentError(
123 "Palette row requires label and eight BGR555 words");
124 }
125 CatalogPreviewPalette palette{title + " / " + label, path.string(), {}};
126 for (size_t i = 0; i < 8; ++i) {
127 if (!words[i].is_number_integer() || words[i] < 0 ||
128 words[i] > 0x7FFF) {
129 return absl::InvalidArgumentError(
130 "Palette word must be an integer in 0..32767");
131 }
132 const auto word = words[i].get<uint16_t>();
133 palette.colors[i] = {static_cast<uint8_t>((word & 31) * 8),
134 static_cast<uint8_t>(((word >> 5) & 31) * 8),
135 static_cast<uint8_t>(((word >> 10) & 31) * 8)};
136 }
137 palettes.push_back(std::move(palette));
138 }
139 }
140 return palettes;
141 } catch (const nlohmann::json::exception& error) {
142 return absl::InvalidArgumentError(std::string("Invalid palette JSON: ") +
143 error.what());
144 } catch (const std::filesystem::filesystem_error& error) {
145 return absl::InvalidArgumentError(error.what());
146 } catch (const std::invalid_argument& error) {
147 return absl::InvalidArgumentError(error.what());
148 }
149}
150} // namespace yaze::editor
auto size() const
Definition rom.h:212
#define ASSIGN_OR_RETURN(type_variable_name, expression)
Definition macro.h:62
std::string ComputeSourceArtifactSha256(std::string_view content)
absl::StatusOr< std::string > ReadBoundedFile(const std::filesystem::path &path, size_t limit)
Editors are the view controllers for the application.
absl::StatusOr< std::vector< CatalogPreviewPalette > > LoadCatalogPreviewPalettes(const std::string &catalog_filename)