#include "workspace.hpp" #include "encoding.hpp" #include #include #include #include #include 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& 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(data.data()), static_cast(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(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 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 ConversionWorkspace::read_output() const { std::ifstream input(output_path, std::ios::binary); if (!input) throw std::runtime_error("Spine 未生成有效的工程文件"); return {std::istreambuf_iterator(input), std::istreambuf_iterator()}; } } // namespace connector