#include "assets/brush_package.h" #include #include namespace pp::assets { namespace { [[nodiscard]] std::uint16_t read_u16_le(std::span bytes, std::size_t offset) noexcept { const auto lo = static_cast(std::to_integer(bytes[offset])); const auto hi = static_cast(std::to_integer(bytes[offset + 1U])); return static_cast(lo | static_cast(hi << 8U)); } [[nodiscard]] bool is_word_extension(std::string_view value) noexcept { if (value.empty()) { return false; } for (const char raw : value) { const auto ch = static_cast(raw); if (std::isalnum(ch) == 0 && ch != '_') { return false; } } return true; } } // namespace pp::foundation::Status validate_ppbr_header( std::string_view magic, std::uint16_t major, std::uint16_t minor) noexcept { if (magic != "PPBR") { return pp::foundation::Status::invalid_argument("PPBR header magic is invalid"); } // DEBT-0049: preserve legacy version acceptance until PPBR compatibility fixtures exist. if (major != ppbr_legacy_major_version && minor != ppbr_legacy_minor_version) { return pp::foundation::Status::invalid_argument("PPBR version is unsupported"); } return pp::foundation::Status::success(); } pp::foundation::Result parse_ppbr_header(std::span bytes) noexcept { if (bytes.size() < ppbr_header_size) { return pp::foundation::Result::failure( pp::foundation::Status::out_of_range("PPBR header is truncated")); } const std::string_view magic(reinterpret_cast(bytes.data()), 4U); const auto major = read_u16_le(bytes, 4U); const auto minor = read_u16_le(bytes, 6U); const auto status = validate_ppbr_header(magic, major, minor); if (!status.ok()) { return pp::foundation::Result::failure(status); } return pp::foundation::Result::success(PpbrHeader { .major = major, .minor = minor, }); } pp::foundation::Result normalize_ppbr_export_path(std::string_view requested_path) { if (requested_path.empty()) { return pp::foundation::Result::failure( pp::foundation::Status::invalid_argument("PPBR export path must not be empty")); } std::string path(requested_path); if (requested_path.find(".ppbr") == std::string_view::npos) { path += ".ppbr"; } return pp::foundation::Result::success(std::move(path)); } pp::foundation::Result plan_ppbr_export_paths( std::string_view requested_path, std::string_view override_data_directory, bool export_data, PpbrDataDirectoryPolicy data_directory_policy) { const auto normalized = normalize_ppbr_export_path(requested_path); if (!normalized) { return pp::foundation::Result::failure(normalized.status()); } const auto slash = normalized.value().find_last_of("/\\"); if (slash == std::string::npos || slash + 1U >= normalized.value().size()) { return pp::foundation::Result::failure( pp::foundation::Status::invalid_argument("PPBR export path must include a directory and file name")); } const auto dot = normalized.value().find_last_of('.'); if (dot == std::string::npos || dot <= slash + 1U || dot + 1U >= normalized.value().size()) { return pp::foundation::Result::failure( pp::foundation::Status::invalid_argument("PPBR export path must include a file extension")); } PpbrExportPaths paths; paths.package_path = normalized.value(); paths.directory = normalized.value().substr(0, slash); paths.stem = normalized.value().substr(slash + 1U, dot - slash - 1U); paths.extension = normalized.value().substr(dot + 1U); if (!is_word_extension(paths.extension)) { return pp::foundation::Result::failure( pp::foundation::Status::invalid_argument("PPBR export path extension contains unsupported characters")); } if (data_directory_policy == PpbrDataDirectoryPolicy::override_directory) { paths.data_directory = std::string(override_data_directory) + "/" + paths.stem + "_data"; } else { paths.data_directory = paths.directory + "/" + paths.stem + "_data"; } paths.data_directory_enabled = export_data && !paths.data_directory.empty(); return pp::foundation::Result::success(std::move(paths)); } pp::foundation::Result plan_brush_package_image_target_paths( std::string_view data_path, BrushPackageImageKind kind, std::string_view image_name, std::string_view image_extension) { if (data_path.empty()) { return pp::foundation::Result::failure( pp::foundation::Status::invalid_argument("brush package data path must not be empty")); } if (image_name.empty()) { return pp::foundation::Result::failure( pp::foundation::Status::invalid_argument("brush package image name must not be empty")); } if (!is_word_extension(image_extension)) { return pp::foundation::Result::failure( pp::foundation::Status::invalid_argument("brush package image extension contains unsupported characters")); } const auto directory = kind == BrushPackageImageKind::brush_tip ? "brushes" : "patterns"; const std::string base_path = std::string(data_path) + "/" + directory + "/" + std::string(image_name) + "." + std::string(image_extension); return pp::foundation::Result::success(BrushPackageImageTargetPaths { .image_path = base_path, .thumbnail_path = std::string(data_path) + "/" + directory + "/thumbs/" + std::string(image_name) + "." + std::string(image_extension), }); } } // namespace pp::assets