Files
SpineParticlesWeb/connector/native/workspace.cpp
T
2026-09-05 23:35:45 +08:00

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