#include "scan/SpineDataVersionDetector.hpp" #include #include #include namespace dts { namespace { constexpr std::size_t maxJsonProbeBytes = 1024 * 1024; constexpr std::size_t maxBinaryStringBytes = 4096; std::optional readVarint(std::istream& input) { std::uint32_t value = 0; for (int shift = 0; shift < 35; shift += 7) { const int byte = input.get(); if (byte == std::char_traits::eof()) { return std::nullopt; } value |= static_cast(byte & 0x7f) << shift; if ((byte & 0x80) == 0) { return value; } } return std::nullopt; } std::optional readBinaryString(std::istream& input) { const auto encodedLength = readVarint(input); if (!encodedLength || *encodedLength == 0) { return std::nullopt; } const std::size_t length = *encodedLength - 1; if (length > maxBinaryStringBytes) { return std::nullopt; } std::string value(length, '\0'); if (length > 0 && !input.read(value.data(), static_cast(length))) { return std::nullopt; } return value; } VersionDetectionResult finish(SkeletonDataFormat format, const SpineVersion& version) { VersionDetectionResult result; result.format = format; result.version = version; result.status = version.isSupported() ? VersionDetectionStatus::Detected : VersionDetectionStatus::UnsupportedVersion; result.message = version.isSupported() ? "Spine data version detected." : "Spine data version is outside the supported range."; return result; } VersionDetectionResult inspectJson(std::ifstream& input) { std::string text(maxJsonProbeBytes, '\0'); input.read(text.data(), static_cast(text.size())); text.resize(static_cast(input.gcount())); static const std::regex bones(R"("bones"\s*:)"); static const std::regex version( R"regex("spine"\s*:\s*"(\d+\.\d+(?:\.\d+)?(?:-[^"]+)?)")regex"); std::smatch match; if (!std::regex_search(text, bones) || !std::regex_search(text, match, version)) { return {SkeletonDataFormat::Json, VersionDetectionStatus::InvalidData, std::nullopt, "JSON does not contain the minimum Spine skeleton schema."}; } const auto parsed = SpineVersion::parse(match[1].str()); if (!parsed) { return {SkeletonDataFormat::Json, VersionDetectionStatus::InvalidData, std::nullopt, "Spine JSON version is invalid."}; } return finish(SkeletonDataFormat::Json, *parsed); } VersionDetectionResult inspectBinary(std::ifstream& input) { const auto hash = readBinaryString(input); const auto versionText = readBinaryString(input); if (hash && versionText) { if (const auto version = SpineVersion::parse(*versionText)) return finish(SkeletonDataFormat::Binary, *version); } input.clear(); input.seekg(0); std::string header(256, '\0'); input.read(header.data(), static_cast(header.size())); header.resize(static_cast(input.gcount())); static const std::regex embeddedVersion( R"regex((3\.8(?:\.\d+)?(?:-[A-Za-z0-9.-]+)?|4\.[0-3](?:\.\d+)?(?:-[A-Za-z0-9.-]+)?))regex"); std::smatch match; if (std::regex_search(header, match, embeddedVersion)) { if (const auto version = SpineVersion::parse(match[1].str())) return finish(SkeletonDataFormat::Binary, *version); } return {SkeletonDataFormat::Binary, VersionDetectionStatus::InvalidData, std::nullopt, "SKEL header does not contain a valid Spine version."}; } } // namespace VersionDetectionResult SpineDataVersionDetector::inspect(const std::filesystem::path& path) const { std::ifstream input(path, std::ios::binary); if (!input) { return {SkeletonDataFormat::Unknown, VersionDetectionStatus::FileMissing, std::nullopt, "Skeleton data file could not be opened."}; } char first = '\0'; while (input.get(first)) { if (first != ' ' && first != '\t' && first != '\r' && first != '\n') { break; } } input.clear(); input.seekg(0); return first == '{' ? inspectJson(input) : inspectBinary(input); } std::string toString(SkeletonDataFormat format) { switch (format) { case SkeletonDataFormat::Json: return "json"; case SkeletonDataFormat::Binary: return "skel"; case SkeletonDataFormat::Unknown: return "unknown"; } return "unknown"; } std::string toString(VersionDetectionStatus status) { switch (status) { case VersionDetectionStatus::Detected: return "detected"; case VersionDetectionStatus::FileMissing: return "file-missing"; case VersionDetectionStatus::InvalidData: return "invalid-data"; case VersionDetectionStatus::UnsupportedVersion: return "unsupported-version"; } return "unknown"; } } // namespace dts