+110
-31
@@ -1,5 +1,6 @@
|
||||
#include <algorithm>
|
||||
#include <cstdint>
|
||||
#include <cmath>
|
||||
#include <expected>
|
||||
#include <filesystem>
|
||||
#include <string>
|
||||
@@ -141,42 +142,120 @@ auto vamp_zero::parse_notation_document(std::string_view json, std::string sourc
|
||||
} catch (...) {
|
||||
return invalid("missing performance timing; regenerate score assets");
|
||||
}
|
||||
auto cursor = std::size_t{};
|
||||
for (auto const &item : *pages) {
|
||||
if (!item.is_object()) {
|
||||
return invalid("page must be an object");
|
||||
try {
|
||||
for (auto const &time : times) {
|
||||
document.measure_boundaries.push_back({numerator(time).convert_to<std::uint64_t>(), denominator(time).convert_to<std::uint64_t>()});
|
||||
}
|
||||
auto path = string_field(item.as_object(), "path");
|
||||
auto hash = string_field(item.as_object(), "sha256");
|
||||
if (!path || !hash) {
|
||||
return invalid("page requires path and sha256 strings");
|
||||
auto const *playback = array_field(*presentation, "playback_pages");
|
||||
if (!playback || playback->empty() || playback->size() > 10000) {
|
||||
return invalid("missing playback pages; regenerate score assets");
|
||||
}
|
||||
auto relative = std::filesystem::path{*path};
|
||||
if (path->empty() || path->find('\0') != std::string::npos || path->find('\\') != std::string::npos ||
|
||||
relative.is_absolute() || relative.extension() != ".png" ||
|
||||
std::any_of(relative.begin(), relative.end(), [](auto const &component) { return component == ".." || component == "."; })) {
|
||||
return invalid("page paths must be safe relative PNG paths");
|
||||
}
|
||||
if (hash->size() != 64 || !std::all_of(hash->begin(), hash->end(), [](std::char_t ch) {
|
||||
return (ch >= '0' && ch <= '9') || (ch >= 'a' && ch <= 'f');
|
||||
})) {
|
||||
return invalid("page sha256 must be 64 lowercase hexadecimal digits");
|
||||
}
|
||||
try {
|
||||
auto first = item.at("first_measure").to_number<std::size_t>();
|
||||
auto count = item.at("measure_count").to_number<std::size_t>();
|
||||
auto start = vamp_zero::parse_beat(item.at("start_beat")), end = vamp_zero::parse_beat(item.at("end_beat"));
|
||||
if (first != cursor + 1 || count == 0 || count > measures->size() - cursor || !start || !end || rational(*start) != times[cursor] || rational(*end) != times[cursor + count]) {
|
||||
auto parse_page = [&](boost::json::value const &item) -> score_page {
|
||||
if (!item.is_object()) {
|
||||
throw std::invalid_argument("page must be an object");
|
||||
}
|
||||
auto path = string_field(item.as_object(), "path");
|
||||
auto hash = string_field(item.as_object(), "sha256");
|
||||
if (!path || !hash) {
|
||||
throw std::invalid_argument("page requires path and sha256 strings");
|
||||
}
|
||||
auto relative = std::filesystem::path{*path};
|
||||
if (path->empty() || path->find('\0') != std::string::npos || path->find('\\') != std::string::npos ||
|
||||
relative.is_absolute() || relative.extension() != ".png" ||
|
||||
std::any_of(relative.begin(), relative.end(), [](auto const &component) { return component == ".." || component == "."; })) {
|
||||
throw std::invalid_argument("page paths must be safe relative PNG paths");
|
||||
}
|
||||
if (hash->size() != 64 || !std::all_of(hash->begin(), hash->end(), [](std::char_t ch) {
|
||||
return (ch >= '0' && ch <= '9') || (ch >= 'a' && ch <= 'f');
|
||||
})) {
|
||||
throw std::invalid_argument("page sha256 must be 64 lowercase hexadecimal digits");
|
||||
}
|
||||
auto parse_range = [&](boost::json::value const &value, auto &range) {
|
||||
range.first_measure = value.at("first_measure").to_number<std::size_t>();
|
||||
range.measure_count = value.at("measure_count").to_number<std::size_t>();
|
||||
auto a = range.first_measure, count = range.measure_count;
|
||||
auto start = parse_beat(value.at("start_beat")), end = parse_beat(value.at("end_beat"));
|
||||
if (!a || a > measures->size() || !count || count > measures->size() - (a - 1) || !start || !end || rational(*start) != times[a - 1] || rational(*end) != times[a + count - 1]) {
|
||||
throw std::invalid_argument("invalid page/system timing or coverage");
|
||||
}
|
||||
range.start_beat = *start;
|
||||
range.end_beat = *end;
|
||||
};
|
||||
auto result = score_page{};
|
||||
result.path = std::move(*path);
|
||||
result.sha256 = std::move(*hash);
|
||||
parse_range(item, result);
|
||||
auto const &systems = item.at("systems").as_array();
|
||||
if (systems.empty() || systems.size() > 2) {
|
||||
throw std::invalid_argument("invalid page systems");
|
||||
}
|
||||
auto cursor = result.first_measure;
|
||||
for (auto const &value : systems) {
|
||||
auto system = score_page::system{};
|
||||
parse_range(value, system);
|
||||
if (system.first_measure != cursor) {
|
||||
throw std::invalid_argument("system gap or overlap");
|
||||
}
|
||||
auto const &labels = value.at("labels").as_array();
|
||||
if (labels.size() != system.measure_count) {
|
||||
throw std::invalid_argument("invalid measure label coverage");
|
||||
}
|
||||
for (auto const &label : labels) {
|
||||
auto parsed = score_page::measure_label{};
|
||||
parsed.measure = label.at("measure").to_number<std::size_t>();
|
||||
auto const &bounds = label.at("bounds").as_array();
|
||||
if (parsed.measure != cursor++ || bounds.size() != 4) {
|
||||
throw std::invalid_argument("invalid measure label");
|
||||
}
|
||||
for (std::size_t k = 0; k < 4; ++k) {
|
||||
parsed.bounds[k] = bounds[k].to_number<double>();
|
||||
if (!std::isfinite(parsed.bounds[k])) {
|
||||
throw std::invalid_argument("non-finite label bounds");
|
||||
}
|
||||
}
|
||||
auto const &r = parsed.bounds;
|
||||
if (!(4 <= r[0] && r[0] < r[2] && r[2] <= 972 && 4 <= r[1] && r[1] < r[3] && r[3] <= 480)) {
|
||||
throw std::invalid_argument("invalid label bounds");
|
||||
}
|
||||
system.labels.push_back(parsed);
|
||||
}
|
||||
result.systems.push_back(std::move(system));
|
||||
}
|
||||
if (cursor != result.first_measure + result.measure_count) {
|
||||
throw std::invalid_argument("systems disagree with page coverage");
|
||||
}
|
||||
return result;
|
||||
};
|
||||
auto cursor = std::size_t{1};
|
||||
auto canonical = std::vector<score_page::system>{};
|
||||
for (auto const &item : *pages) {
|
||||
auto page = parse_page(item);
|
||||
if (page.first_measure != cursor || (page.systems.size() != 2 && &item != &pages->back())) {
|
||||
return invalid("invalid page measure coverage");
|
||||
}
|
||||
document.pages.push_back({std::move(*path), std::move(*hash), first, count, *start, *end});
|
||||
cursor += count;
|
||||
} catch (...) {
|
||||
return invalid("missing page timing; regenerate score assets");
|
||||
cursor += page.measure_count;
|
||||
canonical.insert(canonical.end(), page.systems.begin(), page.systems.end());
|
||||
document.pages.push_back(std::move(page));
|
||||
}
|
||||
}
|
||||
if (cursor != measures->size()) {
|
||||
return invalid("incomplete page measure coverage");
|
||||
if (cursor != measures->size() + 1 || playback->size() != canonical.size()) {
|
||||
return invalid("incomplete normal or playback coverage");
|
||||
}
|
||||
for (std::size_t i = 0; i < playback->size(); ++i) {
|
||||
auto page = parse_page((*playback)[i]);
|
||||
if (page.systems.size() != std::min(std::size_t{2}, canonical.size() - i)) {
|
||||
return invalid("invalid playback system pairing");
|
||||
}
|
||||
for (std::size_t j = 0; j < page.systems.size(); ++j) {
|
||||
auto const &a = canonical[i + j];
|
||||
auto const &b = page.systems[j];
|
||||
if (a.first_measure != b.first_measure || a.measure_count != b.measure_count) {
|
||||
return invalid("playback changes system boundaries");
|
||||
}
|
||||
}
|
||||
document.playback_pages.push_back(std::move(page));
|
||||
}
|
||||
} catch (std::exception const &error) {
|
||||
return invalid(std::string{"invalid playback presentation: "} + error.what() + "; regenerate score assets");
|
||||
}
|
||||
document.id = std::filesystem::path{source_path}.stem().string();
|
||||
document.source_path = std::move(source_path);
|
||||
|
||||
Reference in New Issue
Block a user