#include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include namespace { constexpr auto application_id = std::int64_t{0x56414D50}; constexpr auto catalogue_schema_version = std::int64_t{2}; auto connection_config(vamp_zero::database_config const &config) -> noto::connection_config { auto result = noto::file_connection_config(config.path, "product-vamp"); result.mode = noto::open_mode::READ_ONLY; result.busy_timeout = std::chrono::seconds{5}; result.pragmas = {{.name = "query_only", .value = true}}; return result; } auto text_value(noto::sqlite_value const &value) -> std::string { auto text = std::get_if(&value.value); return text == nullptr ? std::string{} : *text; } auto close_catalogue_query(noto::connection &connection, noto::statement &statement, std::expected status) -> boost::asio::awaitable> { auto finalized = co_await statement.async_finalize(); if (!finalized) { modulus::log_error("catalogue query finalization failed: " + finalized.error().describe()); if (status) { status = std::unexpected(finalized.error()); } } auto closed = co_await connection.async_close(); if (!closed) { modulus::log_error("catalogue connection close failed: " + closed.error().describe()); if (status) { status = std::unexpected(closed.error()); } } co_return status; } auto resolve_source_path(std::string const &assets_path, std::string const &source) -> std::expected { auto fail = [] { return std::unexpected(vamp_zero::make_error(vamp_zero::error_code::PERSISTENCE, "unsafe catalogue asset path; rebuild or reinstall Product Vamp")); }; auto relative = std::filesystem::path{source}; if (source.empty() || source.find('\0') != std::string::npos || source.find('\\') != std::string::npos || relative.is_absolute() || relative.extension() != ".json" || std::any_of(relative.begin(), relative.end(), [](auto const &part) { return part.empty() || part == "." || part == ".."; })) { return fail(); } auto error = std::error_code{}; auto root = std::filesystem::weakly_canonical(assets_path, error); if (error) { return fail(); } auto path = std::filesystem::weakly_canonical(root / relative, error); if (error) { return fail(); } auto resolved = path.lexically_relative(root); if (resolved.empty() || *resolved.begin() == "..") { return fail(); } return path.string(); } auto count_value(noto::sqlite_value const &value) -> std::size_t { auto count = std::get_if(&value.value); return count == nullptr || *count <= 0 ? 0U : static_cast(*count); } } auto vamp_zero::async_validate_database(boost::asio::any_io_executor executor, vamp_zero::database_config config, std::bool_t integrity) -> boost::asio::awaitable> { auto error = std::error_code{}; if (!std::filesystem::is_regular_file(config.path, error) || error) { co_return std::unexpected(vamp_zero::make_error(vamp_zero::error_code::PERSISTENCE, "catalogue is missing or unreadable: " + config.path + "; rebuild or reinstall Product Vamp")); } auto connection = noto::connection{executor, connection_config(config)}; auto connected = co_await connection.async_connect(); if (!connected) { co_return std::unexpected(vamp_zero::make_error(vamp_zero::error_code::PERSISTENCE, "cannot open catalogue: " + config.path + "; rebuild or reinstall Product Vamp: " + connected.error().describe())); } auto application = co_await connection.async_application_id(); auto version = co_await connection.async_user_version(); auto status = std::expected{}; if (!application || !version || *application != application_id || *version != catalogue_schema_version) { status = std::unexpected(vamp_zero::make_error(vamp_zero::error_code::PERSISTENCE, "invalid or incompatible catalogue: " + config.path + "; rebuild or reinstall Product Vamp")); } if (status && integrity) { auto checked = co_await connection.async_execute("PRAGMA quick_check(1)"); if (!checked || checked->rows.size() != 1U || checked->rows[0].size() != 1U || text_value(checked->rows[0][0]) != "ok") { status = std::unexpected(vamp_zero::make_error(vamp_zero::error_code::PERSISTENCE, "corrupt catalogue: " + config.path + "; rebuild or reinstall Product Vamp")); } } auto closed = co_await connection.async_close(); if (!status) { co_return std::unexpected(status.error()); } co_return closed; } auto vamp_zero::async_load_performance_library(boost::asio::any_io_executor executor, vamp_zero::database_config config, std::string assets_path) -> boost::asio::awaitable> { auto connection = noto::connection{executor, connection_config(config)}; auto connected = co_await connection.async_connect(); if (!connected) { co_return std::unexpected(connected.error()); } auto selected = co_await connection.async_prepare("SELECT id, title, composer, part, source_path, measure_count, key_signature, total_pages FROM " "songs ORDER BY title COLLATE NOCASE, part COLLATE NOCASE"); if (!selected) { auto close = co_await connection.async_close(); if (!close) { modulus::log_error("database close failed after load error: " + close.error().describe()); } co_return std::unexpected(selected.error()); } auto parts = std::vector{}; auto status = std::expected{}; for (;;) { auto next = co_await selected->async_step(); if (!next) { status = std::unexpected(next.error()); break; } if (!*next) { break; } auto const &row = **next; if (row.size() != 8U || count_value(row[5]) == 0U || count_value(row[7]) == 0U) { status = std::unexpected(vamp_zero::make_error(vamp_zero::error_code::PERSISTENCE, "invalid performance metadata")); break; } auto source = resolve_source_path(assets_path, text_value(row[4])); if (!source) { status = std::unexpected(source.error()); break; } parts.push_back({ .id = text_value(row[0]), .title = text_value(row[1]), .composer = text_value(row[2]), .key_signature = text_value(row[6]), .part = text_value(row[3]), .source_path = std::move(*source), .measure_count = count_value(row[5]), .total_pages = count_value(row[7]), }); } auto closed = co_await close_catalogue_query(connection, *selected, std::move(status)); if (!closed) { co_return std::unexpected(closed.error()); } co_return vamp_zero::group_library_parts(std::move(parts)); } auto vamp_zero::async_load_practice_library(boost::asio::any_io_executor executor, vamp_zero::database_config config, std::string assets_path) -> boost::asio::awaitable> { auto connection = noto::connection{executor, connection_config(config)}; auto connected = co_await connection.async_connect(); if (!connected) { co_return std::unexpected(connected.error()); } auto selected = co_await connection.async_prepare("SELECT id, title, composer, key_signature, source_path, measure_count, total_pages, authored_key_signature, exercise_id " "FROM practice_exercises ORDER BY exercise_id, id"); if (!selected) { auto close = co_await connection.async_close(); if (!close) { modulus::log_error("database close failed after practice load error: " + close.error().describe()); } co_return std::unexpected(selected.error()); } auto exercises = std::vector{}; auto status = std::expected{}; for (;;) { auto next = co_await selected->async_step(); if (!next) { status = std::unexpected(next.error()); break; } if (!*next) { break; } auto const &row = **next; if (row.size() != 9U || count_value(row[5]) == 0U || count_value(row[6]) == 0U || text_value(row[8]).empty() || text_value(row[3]).empty()) { status = std::unexpected(vamp_zero::make_error(vamp_zero::error_code::PERSISTENCE, "invalid practice metadata")); break; } auto source = resolve_source_path(assets_path, text_value(row[4])); if (!source) { status = std::unexpected(source.error()); break; } auto variant = vamp_zero::practice_exercise::score_variant{ .id = text_value(row[0]), .key_signature = text_value(row[3]), .source_path = std::move(*source), .measure_count = count_value(row[5]), .total_pages = count_value(row[6]), }; exercises.push_back({ .id = text_value(row[8]), .title = text_value(row[1]), .composer = text_value(row[2]), .authored_key_signature = text_value(row[7]), .variants = {std::move(variant)}, }); } auto closed = co_await close_catalogue_query(connection, *selected, std::move(status)); if (!closed) { co_return std::unexpected(closed.error()); } co_return vamp_zero::group_practice_exercises(std::move(exercises)); } auto vamp_zero::async_load_score(boost::asio::any_io_executor executor, vamp_zero::database_config config, vamp_zero::score_reference reference, std::string assets_path) -> boost::asio::awaitable> { // Unwind inline database completions between sequential loads of many scores. co_await boost::asio::post(executor, boost::asio::use_awaitable); auto connection = noto::connection{executor, connection_config(config)}; auto connected = co_await connection.async_connect(); if (!connected) { co_return std::unexpected(connected.error()); } auto table = reference.category == vamp_zero::score_category::PERFORMANCE ? "songs" : "practice_exercises"; auto selected = co_await connection.async_prepare("SELECT source_json, source_path FROM " + std::string{table} + " WHERE id = ?1"); if (!selected) { static_cast(co_await connection.async_close()); co_return std::unexpected(selected.error()); } auto result = std::expected{std::unexpected(vamp_zero::make_error(vamp_zero::error_code::NOT_FOUND, "score not found: " + reference.id))}; auto params = std::vector{noto::sqlite_value{reference.id}}; auto bound = co_await selected->async_reset(std::move(params)); if (!bound) { result = std::unexpected(bound.error()); } else { auto row = co_await selected->async_step(); if (!row) { result = std::unexpected(row.error()); } else if (*row) { auto source = resolve_source_path(assets_path, text_value((**row)[1])); result = source ? vamp_zero::parse_score_document(text_value((**row)[0]), std::move(*source)) : std::expected{std::unexpected(source.error())}; if (result) { result->id = reference.id; } } } auto status = std::expected{}; if (!result) { status = std::unexpected(result.error()); } auto closed = co_await close_catalogue_query(connection, *selected, std::move(status)); if (!closed) { co_return std::unexpected(closed.error()); } co_return result; }