121 lines
5.1 KiB
C++
121 lines
5.1 KiB
C++
#include <algorithm>
|
|
#include <cstdint>
|
|
#include <expected>
|
|
#include <filesystem>
|
|
#include <string>
|
|
#include <string_view>
|
|
#include <utility>
|
|
|
|
#include <boost/json.hpp>
|
|
|
|
#include <vamp_zero/errors.hpp>
|
|
#include <vamp_zero/notation.hpp>
|
|
|
|
|
|
namespace {
|
|
|
|
auto string_field(boost::json::object const &object, std::string_view name) -> std::expected<std::string, modulus::error> {
|
|
auto value = object.if_contains(name);
|
|
if (value == nullptr || !value->is_string()) {
|
|
return std::unexpected(vamp_zero::make_error(vamp_zero::error_code::INVALID_ARGUMENT, std::string{name} + " must be a string"));
|
|
}
|
|
return std::string{value->as_string()};
|
|
}
|
|
|
|
auto object_field(boost::json::object const &object, std::string_view name) -> boost::json::object const * {
|
|
auto value = object.if_contains(name);
|
|
return value == nullptr ? nullptr : value->if_object();
|
|
}
|
|
|
|
auto array_field(boost::json::object const &object, std::string_view name) -> boost::json::array const * {
|
|
auto value = object.if_contains(name);
|
|
return value == nullptr ? nullptr : value->if_array();
|
|
}
|
|
|
|
auto integer_equals(boost::json::object const &object, std::string_view name, std::int64_t expected) -> std::bool_t {
|
|
auto value = object.if_contains(name);
|
|
return value != nullptr && ((value->is_int64() && value->as_int64() == expected) ||
|
|
(value->is_uint64() && value->as_uint64() == static_cast<std::uint64_t>(expected)));
|
|
}
|
|
|
|
}
|
|
|
|
auto vamp_zero::parse_notation_document(std::string_view json, std::string source_path) -> std::expected<score_document, modulus::error> {
|
|
auto invalid = [&](std::string message) -> std::expected<score_document, modulus::error> {
|
|
return std::unexpected(vamp_zero::make_error(vamp_zero::error_code::INVALID_ARGUMENT, source_path + ": score bundle: " + std::move(message)));
|
|
};
|
|
auto error = boost::system::error_code{};
|
|
auto value = boost::json::parse(json, error);
|
|
if (error || !value.is_object()) {
|
|
return invalid("expected a JSON object");
|
|
}
|
|
auto const &root = value.as_object();
|
|
auto music = object_field(root, "music");
|
|
auto presentation = object_field(root, "presentation");
|
|
if (music == nullptr || presentation == nullptr) {
|
|
return invalid("music and presentation objects are required; regenerate score assets");
|
|
}
|
|
auto metadata = object_field(*music, "metadata");
|
|
auto staves = array_field(*music, "staves");
|
|
auto pages = array_field(*presentation, "pages");
|
|
if (metadata == nullptr || staves == nullptr || staves->size() != 1 || !staves->front().is_object()) {
|
|
return invalid("music requires metadata and exactly one staff");
|
|
}
|
|
if (!integer_equals(*presentation, "width", 976) || !integer_equals(*presentation, "height", 484) ||
|
|
pages == nullptr || pages->empty() || pages->size() > 10000) {
|
|
return invalid("presentation requires 976x484 pages");
|
|
}
|
|
auto const &staff = staves->front().as_object();
|
|
auto measures = array_field(staff, "measures");
|
|
if (measures == nullptr || measures->empty()) {
|
|
return invalid("music requires measures");
|
|
}
|
|
auto document = score_document{};
|
|
for (auto field : {std::pair{"title", &document.title}, {"composer", &document.composer}, {"key_signature", &document.key_signature}, {"time_signature", &document.time_signature}}) {
|
|
auto text = string_field(*metadata, field.first);
|
|
if (!text) {
|
|
return invalid(std::string{text.error().message()});
|
|
}
|
|
*field.second = std::move(*text);
|
|
}
|
|
auto part = string_field(staff, "name");
|
|
if (!part) {
|
|
return invalid(std::string{part.error().message()});
|
|
}
|
|
document.part = std::move(*part);
|
|
if (staff.if_contains("arrangement_part_type")) {
|
|
auto type = string_field(staff, "arrangement_part_type");
|
|
if (!type) {
|
|
return invalid(std::string{type.error().message()});
|
|
}
|
|
document.arrangement_part_type = std::move(*type);
|
|
}
|
|
for (auto const &item : *pages) {
|
|
if (!item.is_object()) {
|
|
return invalid("page must be an object");
|
|
}
|
|
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 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");
|
|
}
|
|
document.pages.push_back({std::move(*path), std::move(*hash)});
|
|
}
|
|
document.id = std::filesystem::path{source_path}.stem().string();
|
|
document.source_path = std::move(source_path);
|
|
document.source_json = std::string{json};
|
|
document.measure_count = measures->size();
|
|
return document;
|
|
}
|