Files
ReSkate/Launcher/Test/thunderstore_tests.cpp
T
2026-10-02 14:40:41 -05:00

142 lines
8.0 KiB
C++

// Thunderstore listing: parsing, gzip chunks, versions and the folder a package installs into.
#include "Launcher/thunderstore.h"
#include <miniz.h>
#include <iostream>
#include <string>
using namespace dingosdk;
namespace {
int failures = 0;
void check(bool condition, const char* what) {
if (condition) return;
std::cerr << "FAIL: " << what << '\n';
++failures;
}
template<class F> std::string error_of(F&& run) {
try { run(); } catch (const std::exception& failure) { return failure.what(); }
return {};
}
// A gzip member holding `text` in one stored (uncompressed) deflate block.
std::string gzip(std::string_view text, bool with_name = false) {
std::string out{'\x1f', '\x8b', '\x08', static_cast<char>(with_name ? 0x08 : 0), 0, 0, 0, 0, 0, '\xff'};
if (with_name) out += std::string("listing.json") + '\0';
const auto length = static_cast<unsigned>(text.size());
out += '\x01';
out += static_cast<char>(length & 0xff);
out += static_cast<char>(length >> 8 & 0xff);
out += static_cast<char>(~length & 0xff);
out += static_cast<char>(~length >> 8 & 0xff);
out += text;
const auto crc = mz_crc32(MZ_CRC32_INIT, reinterpret_cast<const unsigned char*>(text.data()), text.size());
for (int i = 0; i < 4; ++i) out += static_cast<char>(crc >> (8 * i) & 0xff);
for (int i = 0; i < 4; ++i) out += static_cast<char>(length >> (8 * i) & 0xff);
return out;
}
constexpr char listing[] = R"([
{"name": "Desert_Springs", "full_name": "Zee-Desert_Springs", "owner": "Zee",
"package_url": "https://thunderstore.io/c/reskate/p/Zee/Desert_Springs/",
"date_created": "2026-10-01T10:00:00Z", "date_updated": "2026-10-02T09:00:00Z",
"rating_score": 4, "is_pinned": false, "is_deprecated": false, "has_nsfw_content": false,
"categories": ["Mods", "Maps"],
"versions": [
{"name": "Desert_Springs", "full_name": "Zee-Desert_Springs-1.9.0", "description": "Old",
"icon": "https://gcdn.thunderstore.io/live/repository/icons/Zee-Desert_Springs-1.9.0.png",
"version_number": "1.9.0", "dependencies": [], "downloads": 10,
"download_url": "https://thunderstore.io/package/download/Zee/Desert_Springs/1.9.0/",
"date_created": "2026-10-01T10:00:00Z", "website_url": "", "is_active": true, "file_size": 700000000},
{"name": "Desert_Springs", "full_name": "Zee-Desert_Springs-1.10.0", "description": "A desert park.",
"icon": "https://gcdn.thunderstore.io/live/repository/icons/Zee-Desert_Springs-1.10.0.png",
"version_number": "1.10.0", "dependencies": [], "downloads": 5,
"download_url": "https://thunderstore.io/package/download/Zee/Desert_Springs/1.10.0/",
"date_created": "2026-10-02T09:00:00Z", "website_url": "https://example.com", "is_active": true,
"file_size": 5368709120},
{"name": "Desert_Springs", "full_name": "Zee-Desert_Springs-2.0.0", "description": "Pulled",
"version_number": "2.0.0", "download_url": "https://thunderstore.io/package/download/Zee/Desert_Springs/2.0.0/",
"is_active": false}
]},
{"name": "Bad Name", "full_name": "Zee-Bad Name", "owner": "Zee", "versions": []},
{"name": "No_Versions", "full_name": "Zee-No_Versions", "owner": "Zee", "versions": []},
{"name": "Plain", "full_name": "Other-Plain", "owner": "Other",
"versions": [{"version_number": "1.0.0", "download_url": "http://insecure.example/plain.zip"}]},
"not a package",
{"name": "Low_Cam", "full_name": "SunJay-Low_Cam", "owner": "SunJay", "is_deprecated": true,
"versions": [{"version_number": "0.1.0", "description": "Low camera",
"download_url": "https://thunderstore.io/package/download/SunJay/Low_Cam/0.1.0/",
"icon": "http://not-https/icon.png", "downloads": 3}]}
])";
} // namespace
int main() {
// ------------------------------------------------ listing
const auto packages = thunderstore::parse_listing(listing);
check(packages.size() == 2, "Malformed rows, packages without versions and insecure downloads are skipped");
const auto& desert = packages.at(0);
check(desert.full_name == "Zee-Desert_Springs" && desert.owner == "Zee" && desert.title() == "Desert Springs",
"Names are read and titles show underscores as spaces");
check(desert.versions.size() == 2 && desert.latest().number == "1.10.0",
"Inactive versions are dropped and the newest version comes first");
check(desert.latest().file_size == 5368709120ull, "Sizes over 4 GB survive");
check(desert.downloads == 15 && desert.in_category("Maps") && !desert.in_category("Audio"),
"Downloads add up across versions and categories are listed");
const auto& cam = packages.at(1);
check(cam.deprecated && cam.latest().icon.empty(), "Flags are read and non-HTTPS icons are dropped");
check(!error_of([] { thunderstore::parse_listing(R"({"error": "No cache available"})"); }).empty(),
"A listing that is not an array is an error");
check(thunderstore::parse_listing("[]").empty(), "An empty community lists nothing");
// ------------------------------------------------ index and gzip
const auto chunk = gzip(listing, true);
check(thunderstore::gunzip(chunk, 1 << 20) == listing, "A gzip chunk (with a file name) unpacks");
check(thunderstore::gunzip("[]", 16) == "[]", "Plain JSON passes through");
check(!error_of([&] { thunderstore::gunzip(chunk, 100); }).empty(), "Output past the limit is refused");
auto damaged = chunk;
damaged[damaged.size() - 9] ^= 0x20;
check(!error_of([&] { thunderstore::gunzip(damaged, 1 << 20); }).empty(), "A damaged chunk fails its CRC check");
check(!error_of([&] { thunderstore::gunzip(chunk.substr(0, 12), 1 << 20); }).empty(), "A truncated chunk is refused");
const auto index = thunderstore::parse_index(thunderstore::gunzip(gzip(
R"(["https://ccdn.thunderstore.io/live/blob-storage/sha256/abc.blob"])"), 1024));
check(index.size() == 1 && index[0] == L"https://ccdn.thunderstore.io/live/blob-storage/sha256/abc.blob",
"The index lists chunk URLs");
check(!error_of([] { thunderstore::parse_index(R"(["http://insecure/blob"])"); }).empty(),
"Non-HTTPS chunks are refused");
// ------------------------------------------------ versions
check(thunderstore::compare_versions("1.10.0", "1.9.0") > 0, "Version parts compare as numbers");
check(thunderstore::compare_versions("1.0.0", "1.0") == 0, "Missing parts count as zero");
check(thunderstore::compare_versions("01.2.3", "1.2.3") == 0, "Leading zeros do not matter");
check(thunderstore::compare_versions("2.0.0", "10.0.0") < 0, "A shorter number is smaller");
check(thunderstore::compare_versions("", "0.0.1") < 0, "No version is older than any version");
// ------------------------------------------------ installed copies
check(thunderstore::folder_for("Zee-Desert_Springs") == "Zee-Desert_Springs", "Packages install as Namespace-Name");
check(thunderstore::folder_for(std::string(100, 'a')).size() == 64, "Folder names keep to the Mods name limit");
thunderstore::Installed installed{{"Zee-Desert_Springs", "1.9.0"}, {"SunJay-Low_Cam", "0.1.0"}};
check(thunderstore::update_available(desert, installed), "An older installed copy has an update");
check(!thunderstore::update_available(cam, installed), "The same version has none");
installed["Zee-Desert_Springs"] = "";
check(!thunderstore::update_available(desert, installed), "An installed copy without a version is left alone");
installed.erase("Zee-Desert_Springs");
check(!thunderstore::update_available(desert, installed), "A package that is not installed has no update");
// ------------------------------------------------ community
check(thunderstore::listing_index_url("reskate") == L"https://thunderstore.io/c/reskate/api/v1/package-listing-index/",
"The listing index URL");
check(thunderstore::community_page("reskate") == L"https://thunderstore.io/c/reskate/", "The community page URL");
if (failures) {
std::cerr << failures << " Thunderstore check(s) failed\n";
return 1;
}
std::cout << "Thunderstore checks passed.\n";
return 0;
}