146 lines
6.7 KiB
C++
146 lines
6.7 KiB
C++
#include "workspace.hpp"
|
|
|
|
#include "encoding.hpp"
|
|
|
|
#include <chrono>
|
|
#include <fstream>
|
|
#include <random>
|
|
#include <regex>
|
|
#include <stdexcept>
|
|
|
|
namespace connector {
|
|
namespace {
|
|
|
|
constexpr std::size_t kMaxJson = 64 * 1024 * 1024;
|
|
constexpr std::size_t kMaxImage = 32 * 1024 * 1024;
|
|
constexpr std::size_t kMaxImages = 128 * 1024 * 1024;
|
|
|
|
std::string required_string(const Json& object, const std::string& key) {
|
|
const Json* value = object.find(key);
|
|
if (!value || !value->is_string()) throw std::runtime_error("连接器请求缺少字段:" + key);
|
|
return value->string();
|
|
}
|
|
|
|
std::string safe_relative_path(const std::string& value, const char* label) {
|
|
std::string normalized = value;
|
|
for (char& character : normalized) if (character == '\\') character = '/';
|
|
while (normalized.rfind("./", 0) == 0) normalized.erase(0, 2);
|
|
while (!normalized.empty() && normalized.back() == '/') normalized.pop_back();
|
|
if (normalized.empty()) throw std::runtime_error(std::string(label) + "路径无效");
|
|
const std::filesystem::path path = std::filesystem::u8path(normalized).lexically_normal();
|
|
if (path.is_absolute() || path.has_root_name()) throw std::runtime_error(std::string(label) + "必须使用相对路径");
|
|
for (const auto& part : path) if (part == "..") throw std::runtime_error(std::string(label) + "路径不能超出工程目录");
|
|
return path.generic_u8string();
|
|
}
|
|
|
|
std::filesystem::path make_temporary_directory() {
|
|
std::random_device device;
|
|
std::mt19937_64 generator(device());
|
|
for (int attempt = 0; attempt < 32; ++attempt) {
|
|
const auto candidate = std::filesystem::temp_directory_path() /
|
|
("spine-particle-" + std::to_string(std::chrono::steady_clock::now().time_since_epoch().count()) + "-" + std::to_string(generator()));
|
|
std::error_code error;
|
|
if (std::filesystem::create_directory(candidate, error)) return candidate;
|
|
}
|
|
throw std::runtime_error("无法创建连接器临时目录");
|
|
}
|
|
|
|
void write_binary(const std::filesystem::path& path, const std::vector<std::uint8_t>& data) {
|
|
std::filesystem::create_directories(path.parent_path());
|
|
std::ofstream output(path, std::ios::binary | std::ios::trunc);
|
|
if (!output) throw std::runtime_error("无法写入临时文件");
|
|
output.write(reinterpret_cast<const char*>(data.data()), static_cast<std::streamsize>(data.size()));
|
|
}
|
|
|
|
} // namespace
|
|
|
|
std::string safe_project_name(const std::string& value) {
|
|
std::string result = trim(value.empty() ? "SpineParticle" : value);
|
|
const std::string forbidden = "\\/:*?\"<>|";
|
|
for (char& character : result) {
|
|
if (static_cast<unsigned char>(character) < 0x20 || forbidden.find(character) != std::string::npos) character = '_';
|
|
}
|
|
while (!result.empty() && result.front() == '.') result.erase(result.begin());
|
|
return result.empty() ? "SpineParticle" : result;
|
|
}
|
|
|
|
std::string normalize_spine_version(const std::string& value) {
|
|
static const std::regex pattern(R"(^(3\.8|4\.[0-3])(?:\.\d+)?$)");
|
|
std::smatch match;
|
|
const std::string cleaned = trim(value);
|
|
if (!std::regex_match(cleaned, match, pattern)) throw std::runtime_error("不支持的 Spine 版本:" + value);
|
|
return match[1].str();
|
|
}
|
|
|
|
ConversionWorkspace::ConversionWorkspace(const Json& payload) {
|
|
try {
|
|
const Json* schema = payload.find("schemaVersion");
|
|
if (!schema || !schema->is_number() || schema->number() != 1) throw std::runtime_error("不支持的连接器请求格式");
|
|
const std::string skeleton_text = required_string(payload, "skeletonJson");
|
|
if (skeleton_text.size() > kMaxJson) throw std::runtime_error("Spine JSON 不能超过 64MB");
|
|
Json skeleton;
|
|
try { skeleton = Json::parse(skeleton_text); }
|
|
catch (...) { throw std::runtime_error("Spine JSON 格式无效"); }
|
|
const Json* skeleton_info = skeleton.find("skeleton");
|
|
const Json* version_value = skeleton_info ? skeleton_info->find("spine") : nullptr;
|
|
if (!version_value || !version_value->is_string()) throw std::runtime_error("JSON 中缺少 Spine 版本");
|
|
spine_version = normalize_spine_version(version_value->string());
|
|
const Json* requested = payload.find("spineVersion");
|
|
if (requested && requested->is_string() && normalize_spine_version(requested->string()) != spine_version) {
|
|
throw std::runtime_error("请求版本与 JSON 版本不一致");
|
|
}
|
|
std::string images_directory = "images";
|
|
if (skeleton_info) {
|
|
if (const Json* images = skeleton_info->find("images"); images && images->is_string() && !images->string().empty()) images_directory = images->string();
|
|
}
|
|
images_directory = safe_relative_path(images_directory, "Spine 图片");
|
|
project_name = safe_project_name(required_string(payload, "projectName"));
|
|
root = make_temporary_directory();
|
|
input_path = root / std::filesystem::u8path(project_name + ".json");
|
|
output_path = root / std::filesystem::u8path(project_name + ".spine");
|
|
if (spine_version == "3.8") {
|
|
Json* mutable_info = skeleton.find("skeleton");
|
|
if (mutable_info) (*mutable_info)["images"] = Json(images_directory);
|
|
}
|
|
{
|
|
std::ofstream output(input_path, std::ios::binary | std::ios::trunc);
|
|
if (!output) throw std::runtime_error("无法写入 Spine JSON 临时文件");
|
|
output << (spine_version == "3.8" ? skeleton.dump(2) + "\n" : skeleton_text);
|
|
}
|
|
std::filesystem::create_directories(root / std::filesystem::u8path(images_directory));
|
|
std::size_t total = 0;
|
|
if (const Json* images = payload.find("images"); images && images->is_array()) {
|
|
for (const Json& image : images->array()) {
|
|
const std::string relative = safe_relative_path(required_string(image, "path"), "图片");
|
|
std::vector<std::uint8_t> data = decode_base64(required_string(image, "dataBase64"));
|
|
if (data.size() > kMaxImage) throw std::runtime_error("单张图片不能超过 32MB");
|
|
total += data.size();
|
|
if (total > kMaxImages) throw std::runtime_error("图片总大小不能超过 128MB");
|
|
write_binary(root / std::filesystem::u8path(images_directory) / std::filesystem::u8path(relative), data);
|
|
archive_images.push_back({images_directory + "/" + relative, std::move(data)});
|
|
}
|
|
}
|
|
} catch (...) {
|
|
if (!root.empty()) {
|
|
std::error_code error;
|
|
std::filesystem::remove_all(root, error);
|
|
}
|
|
throw;
|
|
}
|
|
}
|
|
|
|
ConversionWorkspace::~ConversionWorkspace() {
|
|
if (!root.empty()) {
|
|
std::error_code error;
|
|
std::filesystem::remove_all(root, error);
|
|
}
|
|
}
|
|
|
|
std::vector<std::uint8_t> ConversionWorkspace::read_output() const {
|
|
std::ifstream input(output_path, std::ios::binary);
|
|
if (!input) throw std::runtime_error("Spine 未生成有效的工程文件");
|
|
return {std::istreambuf_iterator<char>(input), std::istreambuf_iterator<char>()};
|
|
}
|
|
|
|
} // namespace connector
|