#include "vdm/meta/veloxpart.hpp" #include #include #include #include #include #include #include "vdm/util/crc32.hpp" #include "vtest.hpp" using namespace vdm; using namespace vdm::meta; namespace { VeloxPart sample_full() { VeloxPart vp; vp.version = kVersion; vp.total_size = 5'000'000'000ull; vp.downloaded = 1'234'567; vp.urls = {"https://origin.example/big.iso", "https://cdn.example/big.iso?sig=abc", "https://mirror1.example/big.iso"}; vp.etag = "\"deadbeef-1234\""; vp.last_modified = "Wed, 01 Jan 2025 00:00:00 GMT"; vp.content_type = "application/octet-stream"; for (int i = 0; i < 8; ++i) { SegmentRecord s; s.start = static_cast(i) * 625'000'000ull; s.end = s.start + 625'000'000ull - 1; // inclusive s.completed = (i < 2) ? s.end - s.start + 1 : 100'000ull * (i + 1); vp.segments.push_back(s); } vp.flags = kFlagHasShaState; vp.sha256_state = {std::byte{1}, std::byte{2}, std::byte{3}, std::byte{0xAA}, std::byte{0xFF}}; return vp; } VeloxPart sample_minimal() { VeloxPart vp; vp.total_size = 0; // chunked / unknown vp.urls = {"http://x/y"}; SegmentRecord s; s.start = 0; s.end = 0; // 1-byte resource s.completed = 0; vp.segments.push_back(s); return vp; } // Re-CRC a mutated body (everything except the final u32). std::vector refresh_crc(std::vector image) { std::uint32_t c = crc32(ConstByteSpan(image.data(), image.size() - 4)); for (int i = 0; i < 4; ++i) image[image.size() - 4 + i] = static_cast((c >> (8 * i)) & 0xFF); return image; } struct TempPath { std::string path; TempPath() { const char *d = std::getenv("TMPDIR"); path = (d ? d : "/tmp"); path += "/vdm_vp_test_XXXXXX"; int fd = ::mkstemp(path.data()); if (fd >= 0) { ::close(fd); ::unlink(path.c_str()); } } ~TempPath() { ::unlink(path.c_str()); ::unlink((path + ".tmp").c_str()); } }; } // namespace VT_TEST(crc32_known_vector) { const char *s = "123456789"; VT_CHECK_EQ(crc32(ConstByteSpan(reinterpret_cast(s), 9)), 0xCBF43926u); VT_CHECK_EQ(crc32(ConstByteSpan{}), 0u); } VT_TEST(vp_roundtrip_full) { VeloxPart in = sample_full(); auto image = serialize_veloxpart(in); auto out = parse_veloxpart(ConstByteSpan(image.data(), image.size())); VT_REQUIRE(out.has_value()); VT_CHECK(out.value() == in); VT_CHECK_EQ(out.value().effective_url(), std::string_view("https://cdn.example/big.iso?sig=abc")); VT_CHECK_EQ(out.value().segments.size(), 8u); } VT_TEST(vp_roundtrip_minimal) { VeloxPart in = sample_minimal(); auto image = serialize_veloxpart(in); auto out = parse_veloxpart(ConstByteSpan(image.data(), image.size())); VT_REQUIRE(out.has_value()); VT_CHECK(out.value() == in); VT_CHECK(out.value().sha256_state.empty()); } VT_TEST(vp_serialize_is_deterministic) { VeloxPart in = sample_full(); auto a = serialize_veloxpart(in); auto b = serialize_veloxpart(in); VT_CHECK(a == b); } VT_TEST(vp_file_roundtrip) { TempPath tp; VeloxPart in = sample_full(); VT_REQUIRE(write_veloxpart_file(tp.path, in, /*fsync=*/true).has_value()); auto out = read_veloxpart_file(tp.path); VT_REQUIRE(out.has_value()); VT_CHECK(out.value() == in); // the temp file must be gone after the atomic rename VT_CHECK_EQ(::access((tp.path + ".tmp").c_str(), F_OK), -1); } VT_TEST(vp_read_missing_file_errors) { auto out = read_veloxpart_file("/vdm_no_such_dir_zz/x.veloxpart.meta"); VT_REQUIRE(!out.has_value()); VT_CHECK_EQ(out.error().code, Error::path_rejected); } // --- truncation / corruption table ------------------------------------------------- VT_TEST(vp_reject_empty_and_tiny) { VT_CHECK_EQ(parse_veloxpart(ConstByteSpan{}).error().code, Error::meta_corrupt); std::array three{}; VT_CHECK_EQ(parse_veloxpart(ConstByteSpan(three.data(), 3)).error().code, Error::meta_corrupt); std::array almost{}; VT_CHECK_EQ(parse_veloxpart(ConstByteSpan(almost.data(), almost.size())).error().code, Error::meta_corrupt); } VT_TEST(vp_reject_bad_magic) { auto image = serialize_veloxpart(sample_minimal()); image[1] = std::byte{'X'}; // "VDMP" -> "VXMP" image = refresh_crc(std::move(image)); // fix CRC so we're testing the magic check auto r = parse_veloxpart(ConstByteSpan(image.data(), image.size())); VT_REQUIRE(!r.has_value()); VT_CHECK_EQ(r.error().code, Error::meta_corrupt); } VT_TEST(vp_reject_crc_mismatch) { auto image = serialize_veloxpart(sample_minimal()); image[20] ^= std::byte{0x40}; // flip a payload bit, do NOT refresh the CRC auto r = parse_veloxpart(ConstByteSpan(image.data(), image.size())); VT_REQUIRE(!r.has_value()); VT_CHECK_EQ(r.error().code, Error::meta_corrupt); // flipping the CRC field itself is also a mismatch auto image2 = serialize_veloxpart(sample_minimal()); image2.back() ^= std::byte{0xFF}; VT_CHECK_EQ(parse_veloxpart(ConstByteSpan(image2.data(), image2.size())).error().code, Error::meta_corrupt); } VT_TEST(vp_future_version_is_unsupported) { auto image = serialize_veloxpart(sample_minimal()); image[4] = std::byte{99}; // version u16 low byte image[5] = std::byte{0}; image = refresh_crc(std::move(image)); auto r = parse_veloxpart(ConstByteSpan(image.data(), image.size())); VT_REQUIRE(!r.has_value()); VT_CHECK_EQ(r.error().code, Error::meta_version_unsupported); } VT_TEST(vp_reject_truncation_at_every_stage) { auto full = serialize_veloxpart(sample_full()); // Chop the image at many lengths; each must be meta_corrupt, never a crash. for (std::size_t len = full.size() - 1; len >= 1; len = (len > 8 ? len - 7 : len - 1)) { auto r = parse_veloxpart(ConstByteSpan(full.data(), len)); VT_CHECK(!r.has_value()); if (r.has_value()) break; VT_CHECK_EQ(r.error().code, Error::meta_corrupt); if (len == 1) break; } } VT_TEST(vp_reject_hostile_url_count) { auto image = serialize_veloxpart(sample_minimal()); // url_count u32 sits right after magic(4)+ver(2)+flags(2)+total(8)+downloaded(8) = 24 for (int i = 0; i < 4; ++i) image[24 + i] = std::byte{0xFF}; image = refresh_crc(std::move(image)); auto r = parse_veloxpart(ConstByteSpan(image.data(), image.size())); VT_REQUIRE(!r.has_value()); VT_CHECK_EQ(r.error().code, Error::meta_corrupt); } VT_TEST(vp_reject_hostile_lp_string_length) { // The case AGENT-CORE singles out: a length prefix that reaches past the buffer. // Build a minimal image, then overwrite url[0]'s length prefix with a huge value. auto image = serialize_veloxpart(sample_minimal()); // layout up to url[0] length: magic4 ver2 flags2 total8 downloaded8 url_count4 = 28 for (int i = 0; i < 4; ++i) image[28 + i] = std::byte{0xFF}; // url[0] len = 4 GiB - 1 image = refresh_crc(std::move(image)); auto r = parse_veloxpart(ConstByteSpan(image.data(), image.size())); VT_REQUIRE(!r.has_value()); VT_CHECK_EQ(r.error().code, Error::meta_corrupt); } VT_TEST(vp_reject_hostile_segment_count) { auto image = serialize_veloxpart(sample_minimal()); // find the segment_count: it's u32 right before the (24-byte) segment records and the // trailing crc. minimal sample has 1 segment and no sha state, so: // segment_count is at size - 4(crc) - 24(one segment) - 4 = size - 32 std::size_t sc = image.size() - 32; for (int i = 0; i < 4; ++i) image[sc + i] = std::byte{0xFF}; image = refresh_crc(std::move(image)); auto r = parse_veloxpart(ConstByteSpan(image.data(), image.size())); VT_REQUIRE(!r.has_value()); VT_CHECK_EQ(r.error().code, Error::meta_corrupt); } VT_TEST(vp_reject_trailing_bytes) { auto image = serialize_veloxpart(sample_minimal()); image.push_back(std::byte{0}); image.push_back(std::byte{0}); image = refresh_crc(std::move(image)); // CRC now covers the padding too auto r = parse_veloxpart(ConstByteSpan(image.data(), image.size())); VT_REQUIRE(!r.has_value()); VT_CHECK_EQ(r.error().code, Error::meta_corrupt); } VT_TEST(vp_reject_segment_completed_over_length) { VeloxPart in = sample_minimal(); in.segments[0].start = 0; in.segments[0].end = 99; // length 100 in.segments[0].completed = 500; // impossible auto image = serialize_veloxpart(in); auto r = parse_veloxpart(ConstByteSpan(image.data(), image.size())); VT_REQUIRE(!r.has_value()); VT_CHECK_EQ(r.error().code, Error::meta_corrupt); }