57 if (text.size() > kMaxBytes)
58 return absl::ResourceExhaustedError(
"Palette catalog exceeds 1 MiB");
60 const auto document = json::parse(text);
61 Fields(document, {
"schema_version",
"game_id",
"entries"});
62 if (Integer(document.at(
"schema_version"), 1) != 1)
63 throw std::runtime_error(
"Palette catalog requires schema_version 1");
64 Text(document,
"game_id");
65 const auto&
entries = document.at(
"entries");
67 throw std::runtime_error(
"Expected at most 4096 palette entries");
69 std::set<std::string> keys;
70 std::set<std::pair<std::string, int>> identities;
71 for (
const auto& item :
entries) {
72 Fields(item, {
"key",
"name",
"group",
"index",
"world",
"use",
73 "description",
"provenance",
"ownership",
"usages"});
75 entry.
key = Text(item,
"key");
76 entry.name = Text(item,
"name");
77 entry.group = Text(item,
"group");
78 entry.index = Integer(item.at(
"index"), 255);
80 if (!group || entry.index >= group->palette_count)
81 throw std::runtime_error(
"Unknown palette group or index");
82 entry.world = Text(item,
"world");
83 entry.use = Text(item,
"use");
84 entry.description = Text(item,
"description");
85 entry.provenance = Text(item,
"provenance");
86 entry.ownership = Text(item,
"ownership");
87 if (!OneOf(entry.world, {
"light_world",
"dark_world",
"underworld",
88 "special",
"shared"}) ||
89 !ValidUse(entry.use) || !OneOf(entry.ownership, {
"rom",
"asm"}))
90 throw std::runtime_error(
"Unsupported world, use or ownership");
91 if (!keys.insert(entry.key).second ||
92 !identities.insert({entry.group, entry.index}).second)
93 throw std::runtime_error(
"Duplicate palette key or group/index");
94 const auto& usages = item.at(
"usages");
95 if (!usages.is_array() || usages.size() > 256)
96 throw std::runtime_error(
"Expected at most 256 contextual usages");
97 for (
const auto& usage : usages) {
98 Fields(usage, {
"name",
"context",
"use",
"obj_row",
"sprite_sheets",
101 parsed.
name = Text(usage,
"name");
102 parsed.context = Text(usage,
"context");
103 parsed.use = Text(usage,
"use");
104 if (!ValidUse(parsed.use))
105 throw std::runtime_error(
"Unsupported usage category");
106 parsed.obj_row = Integer(usage.at(
"obj_row"), 7);
107 parsed.source = Text(usage,
"source");
108 const auto& sheets = usage.at(
"sprite_sheets");
109 if (!sheets.is_array() || sheets.size() > 8)
110 throw std::runtime_error(
"Expected at most 8 sprite sheet values");
111 for (
const auto& sheet : sheets)
112 parsed.sprite_sheets.push_back(Integer(sheet, 0x6B));
113 entry.usages.push_back(std::move(parsed));
115 result.
entries_.push_back(std::move(entry));
118 }
catch (
const std::exception& error) {
119 return absl::InvalidArgumentError(std::string(
"Invalid palette catalog: ") +
125 const std::string& project_file,
const std::string& catalog_file) {
126 namespace fs = std::filesystem;
127 const fs::path configured(catalog_file);
130 if (project_file.empty() || catalog_file.empty() ||
131 catalog_file.find(
'\0') != std::string::npos ||
132 (!configured.is_absolute() &&
133 (catalog_file.find(
'\\') != std::string::npos ||
134 catalog_file.find(
':') != std::string::npos)))
135 return absl::InvalidArgumentError(
136 "Catalog requires a project-relative path");
137 for (
const auto& part : configured)
139 return absl::InvalidArgumentError(
"Catalog path contains traversal");
141 const auto root = fs::canonical(fs::path(project_file).parent_path(), ec);
143 return absl::NotFoundError(
"Project directory is unavailable");
144 const auto path = fs::canonical(root / configured, ec);
145 if (ec || !fs::is_regular_file(path, ec))
146 return absl::NotFoundError(
"Palette catalog is unavailable");
147 const auto mismatch =
148 std::mismatch(root.begin(), root.end(), path.begin(), path.end());
149 if (mismatch.first != root.end())
150 return absl::InvalidArgumentError(
"Catalog path escapes the project");
155 const std::string& project_file,
const std::string& catalog_file) {
158 return path.status();
159 std::ifstream stream(*path, std::ios::binary);
161 return absl::NotFoundError(
"Cannot read palette catalog");
162 std::string text(kMaxBytes + 1,
'\0');
163 stream.read(text.data(), text.size());
164 text.resize(
static_cast<size_t>(stream.gcount()));
166 return absl::DataLossError(
"Cannot read palette catalog");
171 std::string_view query, std::string_view world,
172 std::string_view use)
const {
173 std::vector<const PaletteCatalogEntry*> result;
174 const auto needle = Lower(query);
175 for (
const auto& entry :
entries_) {
176 if (!world.empty() && entry.world != world)
178 if (!use.empty() && entry.use != use &&
179 std::none_of(entry.usages.begin(), entry.usages.end(),
180 [&](
const auto& usage) { return usage.use == use; }))
182 std::string text = entry.key +
" " + entry.name +
" " + entry.group +
" " +
183 std::to_string(entry.index) +
" " + entry.description +
184 " " + entry.provenance;
185 for (
const auto& usage : entry.usages)
186 text +=
" " + usage.name +
" " + usage.context +
" " + usage.source;
187 if (needle.empty() || Lower(text).find(needle) != std::string::npos)
188 result.push_back(&entry);