From 2da247f0fbd89e19290ed0777f870fcd953501c8 Mon Sep 17 00:00:00 2001 From: omigamedev Date: Mon, 1 Jun 2026 12:38:21 +0200 Subject: [PATCH] Validate PPI project layout --- docs/modernization/build-inventory.md | 9 ++-- docs/modernization/debt.md | 2 +- docs/modernization/roadmap.md | 15 ++++-- src/assets/ppi_header.cpp | 64 ++++++++++++++++++++++++ src/assets/ppi_header.h | 13 +++++ tests/CMakeLists.txt | 6 +++ tests/assets/ppi_header_tests.cpp | 31 ++++++++++++ tests/data/projects/minimal-project.ppi | Bin 0 -> 65583 bytes tools/pano_cli/main.cpp | 27 +++++----- 9 files changed, 146 insertions(+), 21 deletions(-) create mode 100644 tests/data/projects/minimal-project.ppi diff --git a/docs/modernization/build-inventory.md b/docs/modernization/build-inventory.md index fc5f518..efa37c7 100644 --- a/docs/modernization/build-inventory.md +++ b/docs/modernization/build-inventory.md @@ -91,11 +91,14 @@ Known local toolchain state: `pp_paint`, `pp_document`, `pp_renderer_api`, `pp_paint_renderer`, `pp_ui_core`, `pano_cli`, and their current headless test binaries, including foundation event/logging/task queue coverage, PNG metadata, PPI - header, settings document, document frame move/duration coverage, paint - brush/stroke/stroke-script coverage, renderer shader descriptor coverage, UI - color parsing, and layout XML parse coverage. + header/layout, settings document, document frame move/duration coverage, + paint brush/stroke/stroke-script coverage, renderer shader descriptor + coverage, UI color parsing, and layout XML parse coverage. - `pano_cli inspect-image` reports PNG IHDR metadata as JSON and is covered by `pano_cli_inspect_png_metadata_smoke` with a tiny IHDR fixture. +- `pano_cli inspect-project` reports validated PPI thumbnail/body byte layout + and is covered by `pano_cli_inspect_project_layout_smoke` with a minimal PPI + fixture. - `pano_cli create-document` supports `--frames` and `--frame-duration-ms` and is covered by `pano_cli_create_animation_document_smoke`. - `pano_cli simulate-stroke` exposes the pure stroke sampler for scripted diff --git a/docs/modernization/debt.md b/docs/modernization/debt.md index 9b0ce1a..6a219df 100644 --- a/docs/modernization/debt.md +++ b/docs/modernization/debt.md @@ -31,7 +31,7 @@ agent or engineer to remove them without reconstructing context from chat. | DEBT-0010 | Open | Modernization | `pp_document` is a pure layer/frame/document/undo-history model but is not yet wired to legacy `Canvas`, PPI load/save, selection masks, or legacy action commands | Keep extraction incremental while preserving app behavior | `ctest --preset desktop-fast --build-config Debug`; `pano_cli create-document --width 64 --height 32 --layers 2` | Legacy document behavior is represented by `pp_document` tests and the app consumes it through a boundary/facade | | DEBT-0011 | Open | Modernization | `package-smoke` validates the Windows CMake app artifact only, not AppX/APK/Apple/WebGL package outputs | Platform package targets are not migrated to root CMake yet | `powershell -ExecutionPolicy Bypass -File scripts\automation\package-smoke.ps1 -Preset windows-msvc-default -Configuration Debug` | Package-smoke covers Windows AppX, Android APK variants, Apple bundles, and WebGL output where local toolchains are present | | DEBT-0012 | Open | Modernization | `pp_ui_core` uses vcpkg tinyxml2 on `windows-msvc-vcpkg-headless`, but retains `pp_vendor_tinyxml2` for default and unproven platform presets | Mobile/AppX/Apple triplets and app packaging still need validation before removing the vendored fallback | `ctest --preset desktop-fast-vcpkg --build-config Debug`; `ctest --preset desktop-fast --build-config Debug`; `powershell -ExecutionPolicy Bypass -File scripts\automation\platform-build.ps1 -Presets android-arm64` | All supported presets consume vcpkg tinyxml2 or document a permanent vendored exception | -| DEBT-0013 | Open | Modernization | `pp_assets` and `pano_cli inspect-project` recognize only the fixed PPI header, not thumbnail bytes or the project body | Full PPI parsing requires staged extraction of legacy `Canvas` serialization and image/layer payload handling | `ctest --preset desktop-fast --build-config Debug`; `pp_assets_ppi_header_tests` | Full PPI load/save fixtures cover thumbnail, layers, frames, metadata, corrupt payloads, and round-trip compatibility | +| DEBT-0013 | Open | Modernization | `pp_assets` and `pano_cli inspect-project` validate the fixed PPI header and thumbnail/body byte layout, but do not yet deserialize the project body into document/layer/frame data | Full PPI parsing requires staged extraction of legacy `Canvas` serialization and image/layer payload handling | `ctest --preset desktop-fast --build-config Debug`; `pp_assets_ppi_header_tests`; `pano_cli_inspect_project_layout_smoke` | Full PPI load/save fixtures cover thumbnail, layers, frames, metadata, corrupt payloads, and round-trip compatibility | | DEBT-0014 | Open | Modernization | `windows-clangcl-asan` now configures as a headless Ninja/clang-cl preset and uses the release MSVC runtime required by ASan, but local builds still fail because installed clang-cl 18.1.8 is paired with VS 2026-preview STL headers that require Clang 20 or newer | Sanitizer validation should be local and repeatable, but this machine's compiler/header pairing is incompatible | `cmake --fresh --preset windows-clangcl-asan`; `cmake --build --preset windows-clangcl-asan --target pp_foundation` | Install/use Clang 20+ with the VS 2026 STL, or point the preset at a compatible VS 2022 toolchain, then make `platform-build.ps1 -Presets windows-clangcl-asan` pass for the headless matrix | ## Closed Debt diff --git a/docs/modernization/roadmap.md b/docs/modernization/roadmap.md index a42defe..70249c2 100644 --- a/docs/modernization/roadmap.md +++ b/docs/modernization/roadmap.md @@ -312,9 +312,9 @@ input. A synchronous event dispatcher, structured logging facade, bounded FIFO task queue, and deterministic `TraceRecorder` now record component/name/thread/frame/stroke metadata with filtering, capacity, and invalid-end tests. `pp_assets` has started with PNG/JPEG signature detection, -PNG IHDR metadata parsing, PPI header recognition, and a pure typed settings -document model, with corrupt/truncated/unsupported, extreme-dimension, and -key/value limit tests. +PNG IHDR metadata parsing, PPI header/project byte-layout recognition, and a +pure typed settings document model, with corrupt/truncated/unsupported, +extreme-dimension, and key/value limit tests. `pp_paint` has started with pure brush parameter validation/stamp evaluation, CPU reference math for the five current shader blend modes, and deterministic stroke spacing/interpolation plus a pure text stroke-script parser. @@ -327,7 +327,9 @@ started with deterministic CPU layer compositing over renderer extents using the paint blend reference. `pp_ui_core` has started with XML-layout-facing length parsing, color parsing, tinyxml-backed layout XML parsing, and invalid input tests. -`pano_cli inspect-image` exposes PNG IHDR metadata as JSON, and +`pano_cli inspect-image` exposes PNG IHDR metadata as JSON, +`pano_cli inspect-project` reports validated PPI thumbnail/body byte layout, +and `pano_cli create-document` can create simple animation documents with explicit frame count/duration, and `pano_cli simulate-stroke` exercises the pure stroke sampler for scripted-stroke automation. `pano_cli simulate-stroke-script` @@ -566,7 +568,8 @@ Results: - `pp_foundation_trace_tests` passed. - `pp_assets_image_format_tests` passed. - `pp_assets_image_metadata_tests` passed. -- `pp_assets_ppi_header_tests` passed. +- `pp_assets_ppi_header_tests` passed, including PPI thumbnail/body layout + validation. - `pp_assets_settings_document_tests` passed. - `pp_paint_brush_tests` passed. - `pp_paint_blend_tests` passed. @@ -586,6 +589,8 @@ Results: test. - `pano_cli_inspect_png_metadata_smoke` passed and reports PNG metadata JSON for the tiny IHDR fixture. +- `pano_cli_inspect_project_layout_smoke` passed and reports PPI + thumbnail/body byte layout JSON. - `pano_cli_parse_layout_smoke` passed. - `pano_cli_simulate_stroke_smoke` passed and reports deterministic stroke sample counts/distances. diff --git a/src/assets/ppi_header.cpp b/src/assets/ppi_header.cpp index 5245c3f..2a62740 100644 --- a/src/assets/ppi_header.cpp +++ b/src/assets/ppi_header.cpp @@ -2,6 +2,8 @@ #include "foundation/binary_stream.h" +#include + namespace pp::assets { namespace { @@ -70,4 +72,66 @@ pp::foundation::Result parse_ppi_header(std::span::success(info); } +pp::foundation::Result ppi_thumbnail_byte_size(PpiThumbnailInfo thumbnail) noexcept +{ + if (thumbnail.width == 0 || thumbnail.height == 0 || thumbnail.components == 0) { + return pp::foundation::Result::failure( + pp::foundation::Status::invalid_argument("PPI thumbnail descriptor is invalid")); + } + + const auto width = static_cast(thumbnail.width); + const auto height = static_cast(thumbnail.height); + const auto components = static_cast(thumbnail.components); + if (width > std::numeric_limits::max() / height) { + return pp::foundation::Result::failure( + pp::foundation::Status::out_of_range("PPI thumbnail byte size overflows")); + } + + const auto pixels = width * height; + if (pixels > std::numeric_limits::max() / components) { + return pp::foundation::Result::failure( + pp::foundation::Status::out_of_range("PPI thumbnail byte size overflows")); + } + + const auto bytes = pixels * components; + if (bytes > static_cast(std::numeric_limits::max())) { + return pp::foundation::Result::failure( + pp::foundation::Status::out_of_range("PPI thumbnail byte size exceeds addressable memory")); + } + + return pp::foundation::Result::success(static_cast(bytes)); +} + +pp::foundation::Result parse_ppi_project_layout(std::span bytes) noexcept +{ + const auto header = parse_ppi_header(bytes); + if (!header) { + return pp::foundation::Result::failure(header.status()); + } + + const auto thumbnail_bytes = ppi_thumbnail_byte_size(header.value().thumbnail); + if (!thumbnail_bytes) { + return pp::foundation::Result::failure(thumbnail_bytes.status()); + } + + if (thumbnail_bytes.value() > std::numeric_limits::max() - ppi_header_size) { + return pp::foundation::Result::failure( + pp::foundation::Status::out_of_range("PPI thumbnail byte size overflows")); + } + + const auto body_offset = ppi_header_size + thumbnail_bytes.value(); + if (bytes.size() < body_offset) { + return pp::foundation::Result::failure( + pp::foundation::Status::out_of_range("PPI thumbnail payload is truncated")); + } + + return pp::foundation::Result::success(PpiProjectLayout { + .header = header.value(), + .thumbnail_offset = ppi_header_size, + .thumbnail_bytes = thumbnail_bytes.value(), + .body_offset = body_offset, + .body_bytes = bytes.size() - body_offset, + }); +} + } diff --git a/src/assets/ppi_header.h b/src/assets/ppi_header.h index 086196c..039e6e7 100644 --- a/src/assets/ppi_header.h +++ b/src/assets/ppi_header.h @@ -34,7 +34,20 @@ struct PpiHeaderInfo { PpiThumbnailInfo thumbnail; }; +struct PpiProjectLayout { + PpiHeaderInfo header; + std::size_t thumbnail_offset = 0; + std::size_t thumbnail_bytes = 0; + std::size_t body_offset = 0; + std::size_t body_bytes = 0; +}; + [[nodiscard]] pp::foundation::Result parse_ppi_header( std::span bytes) noexcept; +[[nodiscard]] pp::foundation::Result ppi_thumbnail_byte_size(PpiThumbnailInfo thumbnail) noexcept; + +[[nodiscard]] pp::foundation::Result parse_ppi_project_layout( + std::span bytes) noexcept; + } diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 4eac920..c7f4782 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -230,6 +230,12 @@ if(TARGET pano_cli) LABELS "assets;integration;desktop-fast" PASS_REGULAR_EXPRESSION "\"format\":\"png\".*\"width\":320.*\"height\":240.*\"components\":4.*\"colorType\":\"rgba\"") + add_test(NAME pano_cli_inspect_project_layout_smoke + COMMAND pano_cli inspect-project --path "${CMAKE_CURRENT_SOURCE_DIR}/data/projects/minimal-project.ppi") + set_tests_properties(pano_cli_inspect_project_layout_smoke PROPERTIES + LABELS "assets;integration;desktop-fast" + PASS_REGULAR_EXPRESSION "\"thumbnail\":\\{\"width\":128,\"height\":128,\"components\":4,\"bytes\":65536\\}.*\"body\":\\{\"offset\":65576,\"bytes\":7\\}") + add_test(NAME pano_cli_parse_layout_smoke COMMAND pano_cli parse-layout --path "${CMAKE_CURRENT_SOURCE_DIR}/data/layouts/simple-layout.xml") set_tests_properties(pano_cli_parse_layout_smoke PROPERTIES diff --git a/tests/assets/ppi_header_tests.cpp b/tests/assets/ppi_header_tests.cpp index b74af91..9ddf861 100644 --- a/tests/assets/ppi_header_tests.cpp +++ b/tests/assets/ppi_header_tests.cpp @@ -7,7 +7,9 @@ #include using pp::assets::parse_ppi_header; +using pp::assets::parse_ppi_project_layout; using pp::assets::ppi_header_size; +using pp::assets::ppi_thumbnail_byte_size; using pp::foundation::StatusCode; namespace { @@ -96,6 +98,33 @@ void rejects_unsupported_document_versions(pp::tests::Harness& h) PP_EXPECT(h, minor_result.status().code == StatusCode::invalid_argument); } +void parses_project_layout_with_thumbnail_and_body(pp::tests::Harness& h) +{ + auto bytes = valid_header(); + const auto thumbnail_size = ppi_thumbnail_byte_size(parse_ppi_header(bytes).value().thumbnail); + PP_EXPECT(h, thumbnail_size.ok()); + bytes.resize(ppi_header_size + thumbnail_size.value() + 7U, std::byte { 0xaa }); + + const auto layout = parse_ppi_project_layout(bytes); + + PP_EXPECT(h, layout.ok()); + PP_EXPECT(h, layout.value().thumbnail_offset == ppi_header_size); + PP_EXPECT(h, layout.value().thumbnail_bytes == 128U * 128U * 4U); + PP_EXPECT(h, layout.value().body_offset == ppi_header_size + (128U * 128U * 4U)); + PP_EXPECT(h, layout.value().body_bytes == 7U); +} + +void rejects_project_layout_with_truncated_thumbnail(pp::tests::Harness& h) +{ + auto bytes = valid_header(); + bytes.resize(ppi_header_size + (128U * 128U * 4U) - 1U, std::byte { 0 }); + + const auto layout = parse_ppi_project_layout(bytes); + + PP_EXPECT(h, !layout.ok()); + PP_EXPECT(h, layout.status().code == StatusCode::out_of_range); +} + } int main() @@ -104,5 +133,7 @@ int main() harness.run("parses_legacy_ppi_header", parses_legacy_ppi_header); harness.run("rejects_truncated_invalid_magic_and_bad_thumbnail", rejects_truncated_invalid_magic_and_bad_thumbnail); harness.run("rejects_unsupported_document_versions", rejects_unsupported_document_versions); + harness.run("parses_project_layout_with_thumbnail_and_body", parses_project_layout_with_thumbnail_and_body); + harness.run("rejects_project_layout_with_truncated_thumbnail", rejects_project_layout_with_truncated_thumbnail); return harness.finish(); } diff --git a/tests/data/projects/minimal-project.ppi b/tests/data/projects/minimal-project.ppi new file mode 100644 index 0000000000000000000000000000000000000000..740b64fe21e94277ac074279437ad7b241d73040 GIT binary patch literal 65583 zcmeIuu?>Jg2nA50RyUThxk}pG#?j@BPT?bgPoR65J!Wmsj94ny_i$Ct{(iM80t5&U zAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C7 z2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N z0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+ z009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBly zK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF e5FkK+009C72oNAZfB*pk1PBoLxj^QA9;4l2mIKoO literal 0 HcmV?d00001 diff --git a/tools/pano_cli/main.cpp b/tools/pano_cli/main.cpp index ce7dd95..22727d3 100644 --- a/tools/pano_cli/main.cpp +++ b/tools/pano_cli/main.cpp @@ -287,22 +287,25 @@ int inspect_project(int argc, char** argv) std::istreambuf_iterator() }; const auto* data = reinterpret_cast(chars.data()); - const auto header = pp::assets::parse_ppi_header(std::span(data, chars.size())); - if (!header) { - print_error("inspect-project", header.status().message); + const auto layout = pp::assets::parse_ppi_project_layout(std::span(data, chars.size())); + if (!layout) { + print_error("inspect-project", layout.status().message); return 2; } std::cout << "{\"ok\":true,\"command\":\"inspect-project\"" - << ",\"documentVersion\":\"" << header.value().document_version.major - << "." << header.value().document_version.minor << "\"" - << ",\"softwareVersion\":\"" << header.value().software_version.major - << "." << header.value().software_version.minor - << "." << header.value().software_version.fix - << "." << header.value().software_version.build << "\"" - << ",\"thumbnail\":{\"width\":" << header.value().thumbnail.width - << ",\"height\":" << header.value().thumbnail.height - << ",\"components\":" << header.value().thumbnail.components + << ",\"documentVersion\":\"" << layout.value().header.document_version.major + << "." << layout.value().header.document_version.minor << "\"" + << ",\"softwareVersion\":\"" << layout.value().header.software_version.major + << "." << layout.value().header.software_version.minor + << "." << layout.value().header.software_version.fix + << "." << layout.value().header.software_version.build << "\"" + << ",\"thumbnail\":{\"width\":" << layout.value().header.thumbnail.width + << ",\"height\":" << layout.value().header.thumbnail.height + << ",\"components\":" << layout.value().header.thumbnail.components + << ",\"bytes\":" << layout.value().thumbnail_bytes + << "},\"body\":{\"offset\":" << layout.value().body_offset + << ",\"bytes\":" << layout.value().body_bytes << "}}\n"; return 0; }