142 lines
4.9 KiB
C++
142 lines
4.9 KiB
C++
#include "scan/SpineDataVersionDetector.hpp"
|
|
|
|
#include <cstdint>
|
|
#include <fstream>
|
|
#include <regex>
|
|
|
|
namespace dts {
|
|
namespace {
|
|
|
|
constexpr std::size_t maxJsonProbeBytes = 1024 * 1024;
|
|
constexpr std::size_t maxBinaryStringBytes = 4096;
|
|
|
|
std::optional<std::uint32_t> 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<char>::eof()) {
|
|
return std::nullopt;
|
|
}
|
|
value |= static_cast<std::uint32_t>(byte & 0x7f) << shift;
|
|
if ((byte & 0x80) == 0) {
|
|
return value;
|
|
}
|
|
}
|
|
return std::nullopt;
|
|
}
|
|
|
|
std::optional<std::string> 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<std::streamsize>(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<std::streamsize>(text.size()));
|
|
text.resize(static_cast<std::size_t>(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<std::streamsize>(header.size()));
|
|
header.resize(static_cast<std::size_t>(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
|