#include #include #include #include #include #include #include #include #include #include #include #include #include #include #include namespace { struct artwork_fixture { std::filesystem::path root{std::filesystem::temp_directory_path() / ("vamp-title-test-" + std::to_string(std::chrono::steady_clock::now().time_since_epoch().count()))}; std::string bytes = std::string(vamp_protocol::title_image_size, static_cast(0xa3)); std::string identifier{}; artwork_fixture() { bytes.replace(0U, 8U, "VPTI\x28\x01\x40\x00", 8U); auto digest = std::array{}; SHA256(reinterpret_cast(bytes.data()), bytes.size(), digest.data()); for (auto byte : digest) { identifier.push_back("0123456789abcdef"[byte >> 4U]); identifier.push_back("0123456789abcdef"[byte & 15U]); } std::filesystem::create_directories(root / "pico/titles"); write(bytes); } ~artwork_fixture() { auto error = std::error_code{}; std::filesystem::remove_all(root, error); } auto path() const -> std::filesystem::path { return root / "pico/titles" / (identifier + ".mask"); } auto write(std::string const &value) -> void { auto stream = std::ofstream{path(), std::ios::binary}; stream.write(value.data(), static_cast(value.size())); } auto config() const -> vamp_zero::config { auto value = vamp_zero::config{}; value.runtime.assets_path = root.string(); return value; } auto state() const -> vamp_zero::app_state { auto catalog = std::make_shared(); catalog->parts.push_back({.id = "part", .title = "Title", .part = "Acoustic", .total_pages = 10000U, .title_image = identifier}); auto value = vamp_zero::app_state{}; value.catalog = catalog; value.mode = vamp_zero::app_mode::PERFORMANCE_SCORE; value.active_part_id = "part"; value.page_index = 9999U; return value; } }; auto call(ligo::service_router const &router, std::string method, std::string route, boost::json::value payload = {}) -> std::expected { auto io = boost::asio::io_context{}; auto future = boost::asio::co_spawn(io, router.call_response({.method = std::move(method), .route = std::move(route)}, std::move(payload)), boost::asio::use_future); io.run(); return future.get(); } } BOOST_FIXTURE_TEST_CASE(control_title_route_serves_registered_binary_and_remote_identity, artwork_fixture) { auto control = vamp_zero::control_state{config(), state()}; auto router = vamp_zero::make_router(control); auto status = call(router, "GET", vamp_protocol::remote_display_path); BOOST_REQUIRE(status); auto remote = vamp_pico::remote_display_state{}; BOOST_REQUIRE(vamp_pico::parse_remote_display_body(status->body, remote)); BOOST_TEST(std::string{remote.title_image.data()} == identifier); BOOST_TEST(remote.page == 10000U); BOOST_TEST(remote.pages == 10000U); auto response = call(router, "POST", vamp_protocol::remote_title_path, boost::json::object{{"id", identifier}}); BOOST_REQUIRE(response); BOOST_TEST(response->content_type == "application/octet-stream"); BOOST_TEST(response->body == bytes); auto validated = vamp_zero::validated_title_path(root.string(), identifier); BOOST_REQUIRE(validated); BOOST_TEST(*validated == path().string()); BOOST_CHECK(!call(router, "GET", vamp_protocol::remote_title_path, boost::json::object{{"id", identifier}})); } BOOST_FIXTURE_TEST_CASE(control_title_route_rejects_arbitrary_unregistered_and_corrupt_assets, artwork_fixture) { auto control = vamp_zero::control_state{config(), state()}; auto router = vamp_zero::make_router(control); for (auto const &id : {std::string{"../title"}, std::string(64U, 'f'), std::string(64U, 'A'), identifier + "a"}) { BOOST_CHECK(!call(router, "POST", vamp_protocol::remote_title_path, boost::json::object{{"id", id}})); } BOOST_CHECK(!call(router, "POST", vamp_protocol::remote_title_path, boost::json::object{{"id", identifier}, {"path", path().string()}})); BOOST_CHECK(!call(router, "POST", vamp_protocol::remote_title_path, boost::json::object{{"id", 1}})); BOOST_CHECK(!call(router, "POST", vamp_protocol::remote_title_path)); auto empty = vamp_zero::control_state{config(), {}}; BOOST_CHECK(!call(vamp_zero::make_router(empty), "POST", vamp_protocol::remote_title_path, boost::json::object{{"id", identifier}})); for (auto const &corrupt : {bytes.substr(1U), bytes + "x", std::string(bytes.size(), 'x')}) { write(corrupt); BOOST_CHECK(!vamp_zero::validated_title_path(root.string(), identifier)); BOOST_CHECK(!call(router, "POST", vamp_protocol::remote_title_path, boost::json::object{{"id", identifier}})); } auto modified = bytes; modified.back() ^= 1; write(modified); BOOST_CHECK(!vamp_zero::validated_title_path(root.string(), identifier)); BOOST_CHECK(!call(router, "POST", vamp_protocol::remote_title_path, boost::json::object{{"id", identifier}})); } BOOST_FIXTURE_TEST_CASE(control_title_validation_rejects_symlink_outside_title_directory, artwork_fixture) { auto other = root / "other.mask"; std::filesystem::rename(path(), other); std::filesystem::create_symlink(other, path()); BOOST_CHECK(!vamp_zero::validated_title_path(root.string(), identifier)); } BOOST_FIXTURE_TEST_CASE(control_has_no_browser_remote_or_catalog_dump, artwork_fixture) { auto control = vamp_zero::control_state{config(), {}}; auto router = vamp_zero::make_router(control); BOOST_CHECK(!call(router, "GET", "/")); BOOST_CHECK(!call(router, "GET", "/api/status")); BOOST_CHECK(!call(router, "GET", "/assets/PetalumaScript.otf")); BOOST_CHECK(!call(router, "GET", "/assets/other.otf")); BOOST_REQUIRE(call(router, "GET", "/api/health")); BOOST_REQUIRE(call(router, "GET", vamp_protocol::remote_display_path)); }