diff --git a/docs/modernization/build-inventory.md b/docs/modernization/build-inventory.md index efa37c7..acba6ef 100644 --- a/docs/modernization/build-inventory.md +++ b/docs/modernization/build-inventory.md @@ -97,8 +97,8 @@ Known local toolchain state: - `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. + plus body summary fields, 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 6a219df..eec63cb 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` 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-0013 | Open | Modernization | `pp_assets` and `pano_cli inspect-project` validate the fixed PPI header, thumbnail/body byte layout, and body summary, but do not yet deserialize layer face PNG payloads into document/layer/frame pixel 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, decoded layer face payloads, 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 70249c2..cb237b1 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/project byte-layout 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/body-summary +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. @@ -328,8 +328,8 @@ 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, -`pano_cli inspect-project` reports validated PPI thumbnail/body byte layout, -and +`pano_cli inspect-project` reports validated PPI thumbnail/body byte layout and +body summary, 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` @@ -568,8 +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, including PPI thumbnail/body layout - validation. +- `pp_assets_ppi_header_tests` passed, including PPI thumbnail/body layout and + body summary validation. - `pp_assets_settings_document_tests` passed. - `pp_paint_brush_tests` passed. - `pp_paint_blend_tests` passed. @@ -590,7 +590,7 @@ Results: - `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. + thumbnail/body byte layout and body summary 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 2a62740..8e9abdf 100644 --- a/src/assets/ppi_header.cpp +++ b/src/assets/ppi_header.cpp @@ -2,6 +2,7 @@ #include "foundation/binary_stream.h" +#include #include namespace pp::assets { @@ -13,6 +14,69 @@ namespace { return reader.read_u32_le(); } +[[nodiscard]] pp::foundation::Result read_positive_i32( + pp::foundation::ByteReader& reader, + const char* message) noexcept +{ + const auto value = reader.read_u32_le(); + if (!value) { + return value; + } + + if (value.value() > static_cast(std::numeric_limits::max())) { + return pp::foundation::Result::failure( + pp::foundation::Status::out_of_range(message)); + } + + return value; +} + +[[nodiscard]] pp::foundation::Result read_f32(pp::foundation::ByteReader& reader) noexcept +{ + const auto bits = reader.read_u32_le(); + if (!bits) { + return pp::foundation::Result::failure(bits.status()); + } + + return pp::foundation::Result::success(std::bit_cast(bits.value())); +} + +[[nodiscard]] pp::foundation::Status skip_bytes( + pp::foundation::ByteReader& reader, + std::size_t bytes) noexcept +{ + const auto skipped = reader.read_bytes(bytes); + if (!skipped) { + return skipped.status(); + } + + return pp::foundation::Status::success(); +} + +[[nodiscard]] pp::foundation::Status validate_canvas_size(std::uint32_t width, std::uint32_t height) noexcept +{ + if (width == 0 || height == 0) { + return pp::foundation::Status::invalid_argument("PPI canvas dimensions must be greater than zero"); + } + + if (width > max_ppi_canvas_dimension || height > max_ppi_canvas_dimension) { + return pp::foundation::Status::out_of_range("PPI canvas dimensions exceed the configured limit"); + } + + return pp::foundation::Status::success(); +} + +[[nodiscard]] pp::foundation::Status add_payload_bytes(PpiBodySummary& summary, std::uint32_t bytes) noexcept +{ + const auto next = summary.compressed_face_bytes + static_cast(bytes); + if (next > max_ppi_face_payload_bytes) { + return pp::foundation::Status::out_of_range("PPI compressed face payload exceeds the configured limit"); + } + + summary.compressed_face_bytes = next; + return pp::foundation::Status::success(); +} + } pp::foundation::Result parse_ppi_header(std::span bytes) noexcept @@ -134,4 +198,221 @@ pp::foundation::Result parse_ppi_project_layout(std::span parse_ppi_body_summary( + PpiHeaderInfo header, + std::span body) noexcept +{ + pp::foundation::ByteReader reader(body); + const auto width = read_positive_i32(reader, "PPI canvas width is outside the supported range"); + const auto height = read_positive_i32(reader, "PPI canvas height is outside the supported range"); + const auto layer_count = read_positive_i32(reader, "PPI layer count is outside the supported range"); + if (!width || !height || !layer_count) { + return pp::foundation::Result::failure( + !width ? width.status() : (!height ? height.status() : layer_count.status())); + } + + const auto canvas_status = validate_canvas_size(width.value(), height.value()); + if (!canvas_status.ok()) { + return pp::foundation::Result::failure(canvas_status); + } + + if (layer_count.value() == 0 || layer_count.value() > max_ppi_layer_count) { + return pp::foundation::Result::failure( + pp::foundation::Status::out_of_range("PPI layer count is outside the configured range")); + } + + PpiBodySummary summary { + .width = width.value(), + .height = height.value(), + .layer_count = layer_count.value(), + .declared_frame_count = 1, + }; + + if (header.document_version.minor >= 3U) { + const auto declared_frames = read_positive_i32(reader, "PPI declared frame count is outside the supported range"); + if (!declared_frames) { + return pp::foundation::Result::failure(declared_frames.status()); + } + + if (declared_frames.value() == 0 || declared_frames.value() > max_ppi_frame_count) { + return pp::foundation::Result::failure( + pp::foundation::Status::out_of_range("PPI declared frame count is outside the configured range")); + } + summary.declared_frame_count = declared_frames.value(); + } + + for (std::uint32_t layer_index = 0; layer_index < summary.layer_count; ++layer_index) { + const auto order = read_positive_i32(reader, "PPI layer order is outside the supported range"); + const auto opacity = read_f32(reader); + const auto name_length = read_positive_i32(reader, "PPI layer name length is outside the supported range"); + if (!order || !opacity || !name_length) { + return pp::foundation::Result::failure( + !order ? order.status() : (!opacity ? opacity.status() : name_length.status())); + } + + if (order.value() >= summary.layer_count) { + return pp::foundation::Result::failure( + pp::foundation::Status::out_of_range("PPI layer order is outside the layer list")); + } + + if (opacity.value() < 0.0F || opacity.value() > 1.0F) { + return pp::foundation::Result::failure( + pp::foundation::Status::out_of_range("PPI layer opacity is outside the supported range")); + } + + if (name_length.value() > max_ppi_layer_name_length) { + return pp::foundation::Result::failure( + pp::foundation::Status::out_of_range("PPI layer name exceeds the configured limit")); + } + + const auto name_status = skip_bytes(reader, name_length.value()); + if (!name_status.ok()) { + return pp::foundation::Result::failure(name_status); + } + + if (header.document_version.minor >= 2U) { + const auto blend_mode = read_positive_i32(reader, "PPI layer blend mode is outside the supported range"); + const auto alpha_locked = reader.read_u8(); + const auto visible = reader.read_u8(); + if (!blend_mode || !alpha_locked || !visible) { + return pp::foundation::Result::failure( + !blend_mode ? blend_mode.status() : (!alpha_locked ? alpha_locked.status() : visible.status())); + } + + if (alpha_locked.value() > 1U || visible.value() > 1U) { + return pp::foundation::Result::failure( + pp::foundation::Status::invalid_argument("PPI layer boolean field is invalid")); + } + } + + std::uint32_t layer_frames = 1; + if (header.document_version.minor >= 3U) { + const auto frame_count = read_positive_i32(reader, "PPI layer frame count is outside the supported range"); + if (!frame_count) { + return pp::foundation::Result::failure(frame_count.status()); + } + + if (frame_count.value() == 0 || frame_count.value() > max_ppi_frame_count) { + return pp::foundation::Result::failure( + pp::foundation::Status::out_of_range("PPI layer frame count is outside the configured range")); + } + layer_frames = frame_count.value(); + } + + if (summary.total_layer_frames > max_ppi_frame_count - layer_frames) { + return pp::foundation::Result::failure( + pp::foundation::Status::out_of_range("PPI total frame count exceeds the configured limit")); + } + summary.total_layer_frames += layer_frames; + + for (std::uint32_t frame_index = 0; frame_index < layer_frames; ++frame_index) { + if (header.document_version.minor >= 3U) { + const auto duration = read_positive_i32(reader, "PPI frame duration is outside the supported range"); + if (!duration) { + return pp::foundation::Result::failure(duration.status()); + } + + if (duration.value() == 0) { + return pp::foundation::Result::failure( + pp::foundation::Status::invalid_argument("PPI frame duration must be greater than zero")); + } + } + + for (std::uint32_t face = 0; face < 6U; ++face) { + const auto has_data = read_positive_i32(reader, "PPI face data flag is outside the supported range"); + if (!has_data) { + return pp::foundation::Result::failure(has_data.status()); + } + + if (has_data.value() > 1U) { + return pp::foundation::Result::failure( + pp::foundation::Status::invalid_argument("PPI face data flag is invalid")); + } + + if (has_data.value() == 0U) { + continue; + } + + ++summary.dirty_face_count; + const auto x0 = read_positive_i32(reader, "PPI dirty box coordinate is outside the supported range"); + const auto y0 = read_positive_i32(reader, "PPI dirty box coordinate is outside the supported range"); + const auto x1 = read_positive_i32(reader, "PPI dirty box coordinate is outside the supported range"); + const auto y1 = read_positive_i32(reader, "PPI dirty box coordinate is outside the supported range"); + const auto data_size = read_positive_i32(reader, "PPI compressed face data size is outside the supported range"); + if (!x0 || !y0 || !x1 || !y1 || !data_size) { + return pp::foundation::Result::failure( + !x0 ? x0.status() + : (!y0 ? y0.status() : (!x1 ? x1.status() : (!y1 ? y1.status() : data_size.status())))); + } + + if (x0.value() >= x1.value() || y0.value() >= y1.value() || x1.value() > summary.width + || y1.value() > summary.height) { + return pp::foundation::Result::failure( + pp::foundation::Status::out_of_range("PPI dirty box is outside the canvas")); + } + + if (data_size.value() == 0U) { + return pp::foundation::Result::failure( + pp::foundation::Status::invalid_argument("PPI compressed face payload must not be empty")); + } + + const auto payload_status = add_payload_bytes(summary, data_size.value()); + if (!payload_status.ok()) { + return pp::foundation::Result::failure(payload_status); + } + + const auto skip_status = skip_bytes(reader, data_size.value()); + if (!skip_status.ok()) { + return pp::foundation::Result::failure(skip_status); + } + } + } + } + + if (header.document_version.minor >= 3U && summary.total_layer_frames != summary.declared_frame_count) { + return pp::foundation::Result::failure( + pp::foundation::Status::invalid_argument("PPI declared frame count does not match layer frames")); + } + + if (header.document_version.minor >= 4U) { + const auto info_bytes = read_positive_i32(reader, "PPI info block size is outside the supported range"); + if (!info_bytes) { + return pp::foundation::Result::failure(info_bytes.status()); + } + + summary.info_bytes = info_bytes.value(); + const auto info_status = skip_bytes(reader, summary.info_bytes); + if (!info_status.ok()) { + return pp::foundation::Result::failure(info_status); + } + } + + if (!reader.empty()) { + return pp::foundation::Result::failure( + pp::foundation::Status::invalid_argument("PPI body has trailing bytes")); + } + + return pp::foundation::Result::success(summary); +} + +pp::foundation::Result parse_ppi_project_summary(std::span bytes) noexcept +{ + const auto layout = parse_ppi_project_layout(bytes); + if (!layout) { + return pp::foundation::Result::failure(layout.status()); + } + + const auto body = parse_ppi_body_summary( + layout.value().header, + bytes.subspan(layout.value().body_offset, layout.value().body_bytes)); + if (!body) { + return pp::foundation::Result::failure(body.status()); + } + + return pp::foundation::Result::success(PpiProjectSummary { + .layout = layout.value(), + .body = body.value(), + }); +} + } diff --git a/src/assets/ppi_header.h b/src/assets/ppi_header.h index 039e6e7..3850d39 100644 --- a/src/assets/ppi_header.h +++ b/src/assets/ppi_header.h @@ -9,6 +9,11 @@ namespace pp::assets { constexpr std::size_t ppi_header_size = 40; +constexpr std::uint32_t max_ppi_canvas_dimension = 131072; +constexpr std::uint32_t max_ppi_layer_count = 1024; +constexpr std::uint32_t max_ppi_frame_count = 100000; +constexpr std::size_t max_ppi_layer_name_length = 128; +constexpr std::uint64_t max_ppi_face_payload_bytes = 1024ULL * 1024ULL * 1024ULL; struct PpiVersion { std::uint32_t major = 0; @@ -42,6 +47,22 @@ struct PpiProjectLayout { std::size_t body_bytes = 0; }; +struct PpiBodySummary { + std::uint32_t width = 0; + std::uint32_t height = 0; + std::uint32_t layer_count = 0; + std::uint32_t declared_frame_count = 0; + std::uint32_t total_layer_frames = 0; + std::uint32_t dirty_face_count = 0; + std::uint64_t compressed_face_bytes = 0; + std::uint32_t info_bytes = 0; +}; + +struct PpiProjectSummary { + PpiProjectLayout layout; + PpiBodySummary body; +}; + [[nodiscard]] pp::foundation::Result parse_ppi_header( std::span bytes) noexcept; @@ -50,4 +71,11 @@ struct PpiProjectLayout { [[nodiscard]] pp::foundation::Result parse_ppi_project_layout( std::span bytes) noexcept; +[[nodiscard]] pp::foundation::Result parse_ppi_body_summary( + PpiHeaderInfo header, + std::span body) noexcept; + +[[nodiscard]] pp::foundation::Result parse_ppi_project_summary( + std::span bytes) noexcept; + } diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index c7f4782..1c3ec0d 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -234,7 +234,7 @@ if(TARGET pano_cli) 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\\}") + PASS_REGULAR_EXPRESSION "\"thumbnail\":\\{\"width\":128,\"height\":128,\"components\":4,\"bytes\":65536\\}.*\"body\":\\{\"offset\":65576,\"bytes\":73,\"width\":64,\"height\":32,\"layers\":1,\"frames\":1,\"dirtyFaces\":0,\"compressedBytes\":0,\"infoBytes\":0\\}") add_test(NAME pano_cli_parse_layout_smoke COMMAND pano_cli parse-layout --path "${CMAKE_CURRENT_SOURCE_DIR}/data/layouts/simple-layout.xml") diff --git a/tests/assets/ppi_header_tests.cpp b/tests/assets/ppi_header_tests.cpp index 9ddf861..2faf44a 100644 --- a/tests/assets/ppi_header_tests.cpp +++ b/tests/assets/ppi_header_tests.cpp @@ -2,11 +2,14 @@ #include "test_harness.h" #include +#include #include #include +#include #include using pp::assets::parse_ppi_header; +using pp::assets::parse_ppi_project_summary; using pp::assets::parse_ppi_project_layout; using pp::assets::ppi_header_size; using pp::assets::ppi_thumbnail_byte_size; @@ -22,6 +25,18 @@ void append_u32(std::vector& bytes, std::uint32_t value) bytes.push_back(static_cast((value >> 24U) & 0xffU)); } +void append_f32(std::vector& bytes, float value) +{ + append_u32(bytes, std::bit_cast(value)); +} + +void append_ascii(std::vector& bytes, std::string_view value) +{ + for (const auto ch : value) { + bytes.push_back(static_cast(ch)); + } +} + std::vector valid_header() { std::vector bytes { @@ -42,6 +57,35 @@ std::vector valid_header() return bytes; } +void append_minimal_body(std::vector& bytes) +{ + append_u32(bytes, 64); + append_u32(bytes, 32); + append_u32(bytes, 1); + append_u32(bytes, 1); + append_u32(bytes, 0); + append_f32(bytes, 1.0F); + append_u32(bytes, 3); + append_ascii(bytes, "Ink"); + append_u32(bytes, 0); + bytes.push_back(std::byte { 0 }); + bytes.push_back(std::byte { 1 }); + append_u32(bytes, 1); + append_u32(bytes, 100); + for (std::uint32_t i = 0; i < 6U; ++i) { + append_u32(bytes, 0); + } + append_u32(bytes, 0); +} + +std::vector minimal_project() +{ + auto bytes = valid_header(); + bytes.resize(ppi_header_size + (128U * 128U * 4U), std::byte { 0 }); + append_minimal_body(bytes); + return bytes; +} + void parses_legacy_ppi_header(pp::tests::Harness& h) { const auto bytes = valid_header(); @@ -100,10 +144,7 @@ void rejects_unsupported_document_versions(pp::tests::Harness& h) 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 bytes = minimal_project(); const auto layout = parse_ppi_project_layout(bytes); @@ -111,7 +152,7 @@ void parses_project_layout_with_thumbnail_and_body(pp::tests::Harness& h) 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); + PP_EXPECT(h, layout.value().body_bytes == 73U); } void rejects_project_layout_with_truncated_thumbnail(pp::tests::Harness& h) @@ -125,6 +166,45 @@ void rejects_project_layout_with_truncated_thumbnail(pp::tests::Harness& h) PP_EXPECT(h, layout.status().code == StatusCode::out_of_range); } +void parses_minimal_project_body_summary(pp::tests::Harness& h) +{ + const auto project = minimal_project(); + const auto summary = parse_ppi_project_summary(project); + + PP_EXPECT(h, summary.ok()); + PP_EXPECT(h, summary.value().body.width == 64U); + PP_EXPECT(h, summary.value().body.height == 32U); + PP_EXPECT(h, summary.value().body.layer_count == 1U); + PP_EXPECT(h, summary.value().body.declared_frame_count == 1U); + PP_EXPECT(h, summary.value().body.total_layer_frames == 1U); + PP_EXPECT(h, summary.value().body.dirty_face_count == 0U); + PP_EXPECT(h, summary.value().body.compressed_face_bytes == 0U); + PP_EXPECT(h, summary.value().body.info_bytes == 0U); +} + +void rejects_invalid_project_body_summaries(pp::tests::Harness& h) +{ + auto truncated = minimal_project(); + truncated.pop_back(); + + auto mismatched_frames = minimal_project(); + mismatched_frames[ppi_header_size + (128U * 128U * 4U) + 12U] = std::byte { 2 }; + + auto bad_layer_name = minimal_project(); + bad_layer_name[ppi_header_size + (128U * 128U * 4U) + 24U] = std::byte { 255 }; + + const auto truncated_result = parse_ppi_project_summary(truncated); + const auto mismatched_frames_result = parse_ppi_project_summary(mismatched_frames); + const auto bad_layer_name_result = parse_ppi_project_summary(bad_layer_name); + + PP_EXPECT(h, !truncated_result.ok()); + PP_EXPECT(h, truncated_result.status().code == StatusCode::out_of_range); + PP_EXPECT(h, !mismatched_frames_result.ok()); + PP_EXPECT(h, mismatched_frames_result.status().code == StatusCode::invalid_argument); + PP_EXPECT(h, !bad_layer_name_result.ok()); + PP_EXPECT(h, bad_layer_name_result.status().code == StatusCode::out_of_range); +} + } int main() @@ -135,5 +215,7 @@ int main() 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); + harness.run("parses_minimal_project_body_summary", parses_minimal_project_body_summary); + harness.run("rejects_invalid_project_body_summaries", rejects_invalid_project_body_summaries); return harness.finish(); } diff --git a/tests/data/projects/minimal-project.ppi b/tests/data/projects/minimal-project.ppi index 740b64f..1b813cf 100644 Binary files a/tests/data/projects/minimal-project.ppi and b/tests/data/projects/minimal-project.ppi differ diff --git a/tools/pano_cli/main.cpp b/tools/pano_cli/main.cpp index 22727d3..f5a225a 100644 --- a/tools/pano_cli/main.cpp +++ b/tools/pano_cli/main.cpp @@ -287,25 +287,32 @@ int inspect_project(int argc, char** argv) std::istreambuf_iterator() }; const auto* data = reinterpret_cast(chars.data()); - const auto layout = pp::assets::parse_ppi_project_layout(std::span(data, chars.size())); - if (!layout) { - print_error("inspect-project", layout.status().message); + const auto summary = pp::assets::parse_ppi_project_summary(std::span(data, chars.size())); + if (!summary) { + print_error("inspect-project", summary.status().message); return 2; } std::cout << "{\"ok\":true,\"command\":\"inspect-project\"" - << ",\"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 + << ",\"documentVersion\":\"" << summary.value().layout.header.document_version.major + << "." << summary.value().layout.header.document_version.minor << "\"" + << ",\"softwareVersion\":\"" << summary.value().layout.header.software_version.major + << "." << summary.value().layout.header.software_version.minor + << "." << summary.value().layout.header.software_version.fix + << "." << summary.value().layout.header.software_version.build << "\"" + << ",\"thumbnail\":{\"width\":" << summary.value().layout.header.thumbnail.width + << ",\"height\":" << summary.value().layout.header.thumbnail.height + << ",\"components\":" << summary.value().layout.header.thumbnail.components + << ",\"bytes\":" << summary.value().layout.thumbnail_bytes + << "},\"body\":{\"offset\":" << summary.value().layout.body_offset + << ",\"bytes\":" << summary.value().layout.body_bytes + << ",\"width\":" << summary.value().body.width + << ",\"height\":" << summary.value().body.height + << ",\"layers\":" << summary.value().body.layer_count + << ",\"frames\":" << summary.value().body.declared_frame_count + << ",\"dirtyFaces\":" << summary.value().body.dirty_face_count + << ",\"compressedBytes\":" << summary.value().body.compressed_face_bytes + << ",\"infoBytes\":" << summary.value().body.info_bytes << "}}\n"; return 0; }