119 if (text.size() > kMaxDocumentBytes) {
120 return absl::ResourceExhaustedError(
"Catalog exceeds 1 MiB");
123 std::vector<std::set<std::string>> object_keys;
124 const auto root = json::parse(
125 text.begin(), text.end(),
126 [&](
int depth, json::parse_event_t event,
json& parsed) {
128 throw std::runtime_error(
"Catalog nesting exceeds limit");
129 if (event == json::parse_event_t::object_start) {
130 object_keys.emplace_back();
131 }
else if (event == json::parse_event_t::key) {
132 if (!object_keys.back().insert(parsed.get<std::string>()).second)
133 throw std::runtime_error(
"Duplicate JSON field");
134 }
else if (event == json::parse_event_t::object_end) {
135 object_keys.pop_back();
139 CheckFields(root, {
"schema_version",
"profile",
"families"});
140 if (ReadInteger(root.at(
"schema_version"), 1, 255) != 1) {
141 return absl::UnimplementedError(
"Unsupported sprite catalog version");
143 if (ReadString(root,
"profile",
true) !=
"oracle") {
144 return absl::UnimplementedError(
"Unsupported sprite catalog profile");
146 const auto&
families = root.at(
"families");
148 return absl::InvalidArgumentError(
"Catalog requires a families array");
151 std::set<std::string> keys;
153 for (
const auto& entry :
families) {
155 entry, {
"key",
"name",
"main_id",
"selector",
"sources",
"variants"});
157 family.
key = ReadString(entry,
"key",
true);
158 family.name = ReadString(entry,
"name",
false);
159 family.main_id = ReadInteger(entry.at(
"main_id"), 0, 0xF2);
160 if (!keys.insert(family.key).second || !ids.insert(family.main_id).second)
161 return absl::InvalidArgumentError(
"Duplicate family key or main ID");
162 const auto& selector = entry.at(
"selector");
163 CheckFields(selector, {
"kind",
"fallback_variant"});
164 if (ReadString(selector,
"kind",
true) !=
"exact_with_fallback") {
165 return absl::UnimplementedError(
"Unsupported sprite selector");
167 family.fallback_variant = ReadString(selector,
"fallback_variant",
true);
168 family.sources = ReadBindings(entry.at(
"sources"));
169 const auto& variants = entry.at(
"variants");
170 if (!variants.is_array() || variants.empty()) {
171 return absl::InvalidArgumentError(
"Family requires variants");
173 std::set<int> subtypes;
174 bool found_fallback =
false;
175 for (
const auto& variant : variants) {
176 CheckFields(variant, {
"key",
"name",
"authored_subtype",
"sources"});
178 parsed.
key = ReadString(variant,
"key",
true);
179 parsed.name = ReadString(variant,
"name",
false);
180 parsed.authored_subtype =
181 ReadInteger(variant.at(
"authored_subtype"), 0, 31);
182 if (!keys.insert(parsed.key).second ||
183 !subtypes.insert(parsed.authored_subtype).second) {
184 return absl::InvalidArgumentError(
"Duplicate variant key or subtype");
186 parsed.sources = ReadBindings(variant.at(
"sources"));
187 found_fallback |= parsed.key == family.fallback_variant;
188 family.variants.push_back(std::move(parsed));
190 if (!found_fallback) {
191 return absl::InvalidArgumentError(
192 "Fallback must reference this family's variant");
194 catalog.
families_.push_back(std::move(family));
197 }
catch (
const std::exception& error) {
198 return absl::InvalidArgumentError(std::string(
"Invalid sprite catalog: ") +
219 int raw_subtype)
const {
222 const auto* family = FindFamily(main_id);
223 if (!family || raw_subtype < 0 || raw_subtype > 255) {
225 "Unknown family or subtype outside byte range; value preserved.";
228 for (
const auto& variant : family->variants) {
229 if (variant.authored_subtype == raw_subtype) {
231 result.
explanation =
"Canonical subtype. Catalog browsing only.";
235 for (
const auto& variant : family->variants) {
236 if (variant.key == family->fallback_variant) {
240 "Legacy fallback; raw subtype preserved, not normalized.";
248 const std::string& source_root,
const std::string& relative_path) {
249 if (source_root.empty()) {
250 return absl::FailedPreconditionError(
251 "Configure sprite_source_root to view source.");
253 if (!SafeRelativePath(relative_path)) {
254 return absl::InvalidArgumentError(
"Invalid source binding");
257 const auto root = std::filesystem::canonical(source_root, ec);
259 return absl::NotFoundError(
"Source root is unavailable: " + source_root);
260 const auto path = std::filesystem::canonical(root / relative_path, ec);
262 return absl::NotFoundError(
"Source file is unavailable: " + relative_path);
263 auto root_it = root.begin();
264 auto path_it = path.begin();
265 for (; root_it != root.end(); ++root_it, ++path_it) {
266 if (path_it == path.end() || *root_it != *path_it) {
267 return absl::PermissionDeniedError(
268 "Source binding escapes configured root");
271 auto text = ReadDocument(path.string());
273 return text.status();
276 document.
path = path.string();
277 std::istringstream stream(*text);
279 while (std::getline(stream, line)) {
280 if (!line.empty() && line.back() ==
'\r')
282 document.
lines.push_back(line);
289 if (!IsIdentifier(binding.
label))
290 return absl::InvalidArgumentError(
"Invalid source label");
293 return result.status();
294 auto document = std::move(*result);
295 for (
size_t i = 0; i < document.lines.size(); ++i) {
296 const auto& line = document.lines[i];
297 auto code = absl::StripAsciiWhitespace(
298 absl::string_view(line).substr(0, line.find(
';')));
299 const auto colon = code.find(
':');
300 if (colon != absl::string_view::npos &&
301 absl::StripAsciiWhitespace(code.substr(0, colon)) == binding.
label) {
302 if (document.label_line) {
303 return absl::FailedPreconditionError(
"Source label is ambiguous: " +
306 document.label_line =
static_cast<int>(i + 1);
309 if (!document.label_line)
310 return absl::NotFoundError(
"Source label not found: " + binding.
label);