Extract PPBR package path validation

This commit is contained in:
2026-06-04 14:56:29 +02:00
parent 6ab64ccc82
commit 394979e4fc
11 changed files with 444 additions and 32 deletions

View File

@@ -0,0 +1,129 @@
#include "assets/brush_package.h"
#include <cctype>
#include <utility>
namespace pp::assets {
namespace {
[[nodiscard]] std::uint16_t read_u16_le(std::span<const std::byte> bytes, std::size_t offset) noexcept
{
const auto lo = static_cast<std::uint16_t>(std::to_integer<unsigned char>(bytes[offset]));
const auto hi = static_cast<std::uint16_t>(std::to_integer<unsigned char>(bytes[offset + 1U]));
return static_cast<std::uint16_t>(lo | static_cast<std::uint16_t>(hi << 8U));
}
[[nodiscard]] bool is_word_extension(std::string_view value) noexcept
{
if (value.empty()) {
return false;
}
for (const unsigned char ch : value) {
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<PpbrHeader> parse_ppbr_header(std::span<const std::byte> bytes) noexcept
{
if (bytes.size() < ppbr_header_size) {
return pp::foundation::Result<PpbrHeader>::failure(
pp::foundation::Status::out_of_range("PPBR header is truncated"));
}
const std::string_view magic(reinterpret_cast<const char*>(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<PpbrHeader>::failure(status);
}
return pp::foundation::Result<PpbrHeader>::success(PpbrHeader {
.major = major,
.minor = minor,
});
}
pp::foundation::Result<std::string> normalize_ppbr_export_path(std::string_view requested_path)
{
if (requested_path.empty()) {
return pp::foundation::Result<std::string>::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<std::string>::success(std::move(path));
}
pp::foundation::Result<PpbrExportPaths> 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<PpbrExportPaths>::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<PpbrExportPaths>::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<PpbrExportPaths>::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<PpbrExportPaths>::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<PpbrExportPaths>::success(std::move(paths));
}
} // namespace pp::assets

View File

@@ -0,0 +1,53 @@
#pragma once
#include "foundation/result.h"
#include <cstddef>
#include <cstdint>
#include <span>
#include <string>
#include <string_view>
namespace pp::assets {
constexpr std::size_t ppbr_header_size = 8;
constexpr std::uint16_t ppbr_legacy_major_version = 0;
constexpr std::uint16_t ppbr_legacy_minor_version = 1;
enum class PpbrDataDirectoryPolicy {
next_to_package,
override_directory,
};
struct PpbrHeader {
std::uint16_t major = 0;
std::uint16_t minor = 0;
};
struct PpbrExportPaths {
std::string package_path;
std::string directory;
std::string stem;
std::string extension;
std::string data_directory;
bool data_directory_enabled = false;
};
[[nodiscard]] pp::foundation::Status validate_ppbr_header(
std::string_view magic,
std::uint16_t major,
std::uint16_t minor) noexcept;
[[nodiscard]] pp::foundation::Result<PpbrHeader> parse_ppbr_header(
std::span<const std::byte> bytes) noexcept;
[[nodiscard]] pp::foundation::Result<std::string> normalize_ppbr_export_path(
std::string_view requested_path);
[[nodiscard]] pp::foundation::Result<PpbrExportPaths> plan_ppbr_export_paths(
std::string_view requested_path,
std::string_view override_data_directory,
bool export_data,
PpbrDataDirectoryPolicy data_directory_policy);
} // namespace pp::assets

View File

@@ -1,6 +1,7 @@
#include "pch.h"
#include "log.h"
#include "node_panel_brush.h"
#include "assets/brush_package.h"
#include "app_core/brush_ui.h"
#include "legacy_brush_ui_services.h"
#include "asset.h"
@@ -679,26 +680,27 @@ void NodePanelBrushPreset::add_brush(std::shared_ptr<Brush> brush)
bool NodePanelBrushPreset::export_ppbr(const std::string& path_in, const PPBRInfo& info_data)
{
std::string path = path_in;
if (path_in.find(".ppbr") == std::string::npos)
path += ".ppbr";
const auto export_paths = pp::assets::plan_ppbr_export_paths(
path_in,
info_data.dest_path,
info_data.export_data,
#if __OSX__
pp::assets::PpbrDataDirectoryPolicy::override_directory
#else
pp::assets::PpbrDataDirectoryPolicy::next_to_package
#endif
);
if (!export_paths) {
LOG("export_ppbr invalid path: %s", export_paths.status().message);
return false;
}
const auto& path = export_paths.value().package_path;
LOG("export ppbr to: %s", path.c_str());
std::regex r(R"((.*)[\\/]([^\\/]+)\.(\w+)?$)");
std::smatch m;
if (!std::regex_search(path, m, r))
return false;
auto base = m[1].str();
auto name = m[2].str();
auto ext = m[3].str();
const auto& out_path = export_paths.value().data_directory;
#if __OSX__
std::string out_path = info_data.dest_path + "/" + name + "_data";
#else
std::string out_path = base + "/" + name + "_data";
#endif
bool path_created = info_data.export_data && !out_path.empty() ? Asset::create_dir(out_path) : false;
bool path_created = export_paths.value().data_directory_enabled ? Asset::create_dir(out_path) : false;
std::ofstream f(path, std::ios::binary);
if (f.good())
@@ -826,16 +828,12 @@ bool NodePanelBrushPreset::import_ppbr(const std::string& path)
// sanity checks
auto magic = sr.rstring(4);
if (magic != "PPBR")
{
LOG("PPBR tag not found")
return false;
}
auto vmaj = sr.ru16();
auto vmin = sr.ru16();
if (vmaj != 0 && vmin != 1)
const auto header_status = pp::assets::validate_ppbr_header(magic, vmaj, vmin);
if (!header_status.ok())
{
LOG("unrecognised version %d.%d", vmaj, vmin);
LOG("PPBR header rejected: %s (%d.%d)", header_status.message, vmaj, vmin);
return false;
}