#include #include #include #include #include #include #include #include #include #include namespace { auto string_field(boost::json::object const &object, std::string_view name) -> std::expected { 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(expected))); } } auto vamp_zero::parse_notation_document(std::string_view json, std::string source_path) -> std::expected { auto invalid = [&](std::string message) -> std::expected { 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; }