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

@@ -76,6 +76,16 @@ add_test(NAME pp_assets_image_format_tests COMMAND pp_assets_image_format_tests)
set_tests_properties(pp_assets_image_format_tests PROPERTIES
LABELS "assets;desktop-fast")
add_executable(pp_assets_brush_package_tests
assets/brush_package_tests.cpp)
target_link_libraries(pp_assets_brush_package_tests PRIVATE
pp_assets
pp_test_harness)
add_test(NAME pp_assets_brush_package_tests COMMAND pp_assets_brush_package_tests)
set_tests_properties(pp_assets_brush_package_tests PROPERTIES
LABELS "assets;paint;desktop-fast;fuzz")
add_executable(pp_assets_image_metadata_tests
assets/image_metadata_tests.cpp)
target_link_libraries(pp_assets_image_metadata_tests PRIVATE
@@ -942,7 +952,7 @@ if(TARGET pano_cli)
--header-image)
set_tests_properties(pano_cli_plan_brush_package_export_smoke PROPERTIES
LABELS "app;paint;assets;integration;desktop-fast"
PASS_REGULAR_EXPRESSION "\"command\":\"plan-brush-package-export\".*\"path\":\"D:/Paint/clouds.ppbr\".*\"author\":\"Artist\".*\"destPath\":\"D:/Paint/BrushPreviews\".*\"exportData\":true.*\"hasHeaderImage\":true.*\"dispatches\":1")
PASS_REGULAR_EXPRESSION "\"command\":\"plan-brush-package-export\".*\"path\":\"D:/Paint/clouds.ppbr\".*\"author\":\"Artist\".*\"destPath\":\"D:/Paint/BrushPreviews\".*\"exportData\":true.*\"hasHeaderImage\":true.*\"paths\":\\{\"package\":\"D:/Paint/clouds.ppbr\".*\"dataDirectory\":\"D:/Paint/BrushPreviews/clouds_data\".*\"dataDirectoryEnabled\":true.*\"dispatches\":1")
add_test(NAME pano_cli_plan_brush_package_export_rejects_empty_path
COMMAND "${CMAKE_COMMAND}"
@@ -952,6 +962,15 @@ if(TARGET pano_cli)
set_tests_properties(pano_cli_plan_brush_package_export_rejects_empty_path PROPERTIES
LABELS "app;paint;assets;integration;desktop-fast;fuzz")
add_test(NAME pano_cli_plan_brush_package_export_rejects_path_without_directory
COMMAND "${CMAKE_COMMAND}"
-DPANO_CLI=$<TARGET_FILE:pano_cli>
-DEXPECT_NO_DIRECTORY=ON
"-DEXPECTED_OUTPUT=PPBR export path must include a directory and file name"
-P "${CMAKE_CURRENT_SOURCE_DIR}/cmake/expect_pano_cli_plan_brush_package_export_failure.cmake")
set_tests_properties(pano_cli_plan_brush_package_export_rejects_path_without_directory PROPERTIES
LABELS "app;paint;assets;integration;desktop-fast;fuzz")
add_test(NAME pano_cli_plan_brush_package_export_dest_without_data_smoke
COMMAND pano_cli plan-brush-package-export
--path D:/Paint/clouds.ppbr
@@ -959,7 +978,7 @@ if(TARGET pano_cli)
--no-export-data)
set_tests_properties(pano_cli_plan_brush_package_export_dest_without_data_smoke PROPERTIES
LABELS "app;paint;assets;integration;desktop-fast;fuzz"
PASS_REGULAR_EXPRESSION "\"command\":\"plan-brush-package-export\".*\"destPath\":\"D:/Paint/BrushPreviews\".*\"exportData\":false.*\"dispatches\":1")
PASS_REGULAR_EXPRESSION "\"command\":\"plan-brush-package-export\".*\"destPath\":\"D:/Paint/BrushPreviews\".*\"exportData\":false.*\"dataDirectory\":\"D:/Paint/BrushPreviews/clouds_data\".*\"dataDirectoryEnabled\":false.*\"dispatches\":1")
add_test(NAME pano_cli_plan_tools_menu_shortcuts_smoke
COMMAND pano_cli plan-tools-menu --command shortcuts)

View File

@@ -0,0 +1,155 @@
#include "assets/brush_package.h"
#include "test_harness.h"
#include <array>
#include <cstddef>
#include <string>
#include <string_view>
namespace {
std::array<std::byte, pp::assets::ppbr_header_size> ppbr_header(std::uint16_t major, std::uint16_t minor)
{
return {
std::byte { 'P' },
std::byte { 'P' },
std::byte { 'B' },
std::byte { 'R' },
static_cast<std::byte>(major & 0xffU),
static_cast<std::byte>((major >> 8U) & 0xffU),
static_cast<std::byte>(minor & 0xffU),
static_cast<std::byte>((minor >> 8U) & 0xffU),
};
}
void parses_ppbr_header_and_legacy_version_tolerance(pp::tests::Harness& harness)
{
const auto canonical_bytes = ppbr_header(0, 1);
const auto canonical = pp::assets::parse_ppbr_header(canonical_bytes);
PP_EXPECT(harness, canonical);
PP_EXPECT(harness, canonical.value().major == 0U);
PP_EXPECT(harness, canonical.value().minor == 1U);
const auto minor_tolerated_bytes = ppbr_header(0, 2);
const auto major_tolerated_bytes = ppbr_header(1, 1);
const auto rejected_bytes = ppbr_header(1, 2);
const auto legacy_minor_tolerated = pp::assets::parse_ppbr_header(minor_tolerated_bytes);
const auto legacy_major_tolerated = pp::assets::parse_ppbr_header(major_tolerated_bytes);
const auto rejected = pp::assets::parse_ppbr_header(rejected_bytes);
PP_EXPECT(harness, legacy_minor_tolerated);
PP_EXPECT(harness, legacy_major_tolerated);
PP_EXPECT(harness, !rejected);
}
void rejects_truncated_and_bad_magic_headers(pp::tests::Harness& harness)
{
const std::array<std::byte, 4> truncated {
std::byte { 'P' },
std::byte { 'P' },
std::byte { 'B' },
std::byte { 'R' },
};
auto bad_magic = ppbr_header(0, 1);
bad_magic[2] = std::byte { 'X' };
const auto truncated_result = pp::assets::parse_ppbr_header(truncated);
const auto magic_result = pp::assets::parse_ppbr_header(bad_magic);
PP_EXPECT(harness, !truncated_result);
PP_EXPECT(harness, truncated_result.status().code == pp::foundation::StatusCode::out_of_range);
PP_EXPECT(harness, !magic_result);
PP_EXPECT(harness, magic_result.status().code == pp::foundation::StatusCode::invalid_argument);
}
void plans_export_package_and_data_paths(pp::tests::Harness& harness)
{
const auto regular = pp::assets::plan_ppbr_export_paths(
"D:/Paint/clouds",
"",
true,
pp::assets::PpbrDataDirectoryPolicy::next_to_package);
PP_EXPECT(harness, regular);
if (regular) {
PP_EXPECT(harness, regular.value().package_path == "D:/Paint/clouds.ppbr");
PP_EXPECT(harness, regular.value().directory == "D:/Paint");
PP_EXPECT(harness, regular.value().stem == "clouds");
PP_EXPECT(harness, regular.value().extension == "ppbr");
PP_EXPECT(harness, regular.value().data_directory == "D:/Paint/clouds_data");
PP_EXPECT(harness, regular.value().data_directory_enabled);
}
const auto override = pp::assets::plan_ppbr_export_paths(
"/brushes/clouds.ppbr",
"/Users/artist/Exports",
true,
pp::assets::PpbrDataDirectoryPolicy::override_directory);
PP_EXPECT(harness, override);
if (override) {
PP_EXPECT(harness, override.value().data_directory == "/Users/artist/Exports/clouds_data");
PP_EXPECT(harness, override.value().data_directory_enabled);
}
const auto no_data = pp::assets::plan_ppbr_export_paths(
"D:/Paint/clouds.ppbr",
"",
false,
pp::assets::PpbrDataDirectoryPolicy::next_to_package);
PP_EXPECT(harness, no_data);
if (no_data) {
PP_EXPECT(harness, !no_data.value().data_directory_enabled);
}
}
void preserves_legacy_extension_containment_rule(pp::tests::Harness& harness)
{
const auto path = pp::assets::normalize_ppbr_export_path("D:/Paint/clouds.ppbr.tmp");
PP_EXPECT(harness, path);
PP_EXPECT(harness, path.value() == "D:/Paint/clouds.ppbr.tmp");
}
void rejects_export_paths_that_legacy_regex_could_not_match(pp::tests::Harness& harness)
{
PP_EXPECT(
harness,
!pp::assets::plan_ppbr_export_paths(
"",
"",
true,
pp::assets::PpbrDataDirectoryPolicy::next_to_package));
PP_EXPECT(
harness,
!pp::assets::plan_ppbr_export_paths(
"clouds",
"",
true,
pp::assets::PpbrDataDirectoryPolicy::next_to_package));
PP_EXPECT(
harness,
!pp::assets::plan_ppbr_export_paths(
"D:/Paint/.ppbr",
"",
true,
pp::assets::PpbrDataDirectoryPolicy::next_to_package));
PP_EXPECT(
harness,
!pp::assets::plan_ppbr_export_paths(
"D:/Paint/clouds.ppbr!",
"",
true,
pp::assets::PpbrDataDirectoryPolicy::next_to_package));
}
} // namespace
int main()
{
pp::tests::Harness harness;
harness.run("parses PPBR header and legacy version tolerance", parses_ppbr_header_and_legacy_version_tolerance);
harness.run("rejects truncated and bad magic headers", rejects_truncated_and_bad_magic_headers);
harness.run("plans export package and data paths", plans_export_package_and_data_paths);
harness.run("preserves legacy extension containment rule", preserves_legacy_extension_containment_rule);
harness.run("rejects export paths that legacy regex could not match", rejects_export_paths_that_legacy_regex_could_not_match);
return harness.finish();
}

View File

@@ -6,11 +6,24 @@ if(NOT DEFINED EXPECTED_OUTPUT)
message(FATAL_ERROR "EXPECTED_OUTPUT must be set")
endif()
execute_process(
COMMAND "${PANO_CLI}" plan-brush-package-export
RESULT_VARIABLE result
OUTPUT_VARIABLE output
ERROR_VARIABLE error)
if(NOT DEFINED EXPECT_NO_DIRECTORY)
set(EXPECT_NO_DIRECTORY OFF)
endif()
if(EXPECT_NO_DIRECTORY)
execute_process(
COMMAND "${PANO_CLI}" plan-brush-package-export
--path clouds
RESULT_VARIABLE result
OUTPUT_VARIABLE output
ERROR_VARIABLE error)
else()
execute_process(
COMMAND "${PANO_CLI}" plan-brush-package-export
RESULT_VARIABLE result
OUTPUT_VARIABLE output
ERROR_VARIABLE error)
endif()
if(result EQUAL 0)
message(FATAL_ERROR "Expected pano_cli plan-brush-package-export to fail, but it exited 0")