53 const std::string& catalog_filename) {
55 const auto palette_file =
56 std::filesystem::absolute(catalog_filename).parent_path() /
"palettes" /
58 std::error_code error;
59 const bool exists = std::filesystem::exists(palette_file, error);
61 return absl::InvalidArgumentError(
"Cannot inspect palette file: " +
65 return std::vector<CatalogPreviewPalette>{};
67 const auto document = nlohmann::json::parse(
68 bytes, [](
int depth, nlohmann::json::parse_event_t, nlohmann::json&) {
70 throw std::invalid_argument(
"Palette JSON exceeds depth limit");
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");
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");
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)) {
102 if (rom.
size() > kTotalSourceBytes - total_bytes) {
103 return absl::InvalidArgumentError(
104 "Palette source byte budget exceeded");
106 total_bytes += rom.
size();
109 if (hashes.at(path) != expected) {
110 return absl::DataLossError(
"Palette source ROM SHA256 mismatch: " +
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");
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");
126 for (
size_t i = 0; i < 8; ++i) {
127 if (!words[i].is_number_integer() || words[i] < 0 ||
129 return absl::InvalidArgumentError(
130 "Palette word must be an integer in 0..32767");
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)};
137 palettes.push_back(std::move(palette));
141 }
catch (
const nlohmann::json::exception& error) {
142 return absl::InvalidArgumentError(std::string(
"Invalid palette JSON: ") +
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());