diff --git a/docs/modernization/build-inventory.md b/docs/modernization/build-inventory.md index 0ac3c83..0f20605 100644 --- a/docs/modernization/build-inventory.md +++ b/docs/modernization/build-inventory.md @@ -92,7 +92,7 @@ Known local toolchain state: `pp_paint_renderer`, `pp_ui_core`, `pano_cli`, and their current headless test binaries, including foundation binary-stream/event/logging/task queue coverage, PNG metadata and - decode, PPI header/layout, settings document, document + decode, PPI header/layout/non-finite opacity rejection, settings document, document snapshot/per-layer-frame/move/duration/face-pixel/PPI export coverage, snapshot-embedded face-payload rejection, paint brush/final-blend/ stroke-alpha-blend/stroke spacing/stroke stress/stroke-script coverage, diff --git a/docs/modernization/roadmap.md b/docs/modernization/roadmap.md index c5667a4..d12e32e 100644 --- a/docs/modernization/roadmap.md +++ b/docs/modernization/roadmap.md @@ -316,7 +316,7 @@ PNG IHDR metadata parsing, PPI header/project byte-layout/body-summary recognition, layer/frame indexing, dirty-face PNG payload metadata validation, asset-level RGBA PNG payload decoding, and a pure typed settings document model, with -corrupt/truncated/unsupported, extreme-dimension, and key/value limit tests. +corrupt/truncated/unsupported, non-finite opacity, 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 final RGBA shader blend modes plus the shader-style stroke-alpha blend modes used by pattern/dual-brush mixing, and deterministic diff --git a/src/assets/ppi_header.cpp b/src/assets/ppi_header.cpp index 01d251f..1270d36 100644 --- a/src/assets/ppi_header.cpp +++ b/src/assets/ppi_header.cpp @@ -5,6 +5,7 @@ #include #include +#include #include #include #include @@ -338,6 +339,11 @@ pp::foundation::Result parse_ppi_body_impl( seen_orders[order.value()] = true; } + if (!std::isfinite(opacity.value())) { + return pp::foundation::Result::failure( + pp::foundation::Status::invalid_argument("PPI layer opacity must be finite")); + } + 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")); @@ -677,6 +683,11 @@ pp::foundation::Result> create_ppi_project(PpiProjectConf pp::foundation::Status::out_of_range("PPI layer name exceeds the configured limit")); } + if (!std::isfinite(layer.metadata.opacity)) { + return pp::foundation::Result>::failure( + pp::foundation::Status::invalid_argument("PPI layer opacity must be finite")); + } + if (layer.metadata.opacity < 0.0F || layer.metadata.opacity > 1.0F) { return pp::foundation::Result>::failure( pp::foundation::Status::out_of_range("PPI layer opacity is outside the supported range")); diff --git a/tests/assets/ppi_header_tests.cpp b/tests/assets/ppi_header_tests.cpp index 954e4a0..1481dbf 100644 --- a/tests/assets/ppi_header_tests.cpp +++ b/tests/assets/ppi_header_tests.cpp @@ -5,6 +5,7 @@ #include #include #include +#include #include #include #include @@ -43,6 +44,19 @@ void append_f32(std::vector& bytes, float value) append_u32(bytes, std::bit_cast(value)); } +void write_u32_at(std::vector& bytes, std::size_t offset, std::uint32_t value) +{ + bytes[offset + 0U] = static_cast(value & 0xffU); + bytes[offset + 1U] = static_cast((value >> 8U) & 0xffU); + bytes[offset + 2U] = static_cast((value >> 16U) & 0xffU); + bytes[offset + 3U] = static_cast((value >> 24U) & 0xffU); +} + +void write_f32_at(std::vector& bytes, std::size_t offset, float value) +{ + write_u32_at(bytes, offset, std::bit_cast(value)); +} + void append_ascii(std::vector& bytes, std::string_view value) { for (const auto ch : value) { @@ -375,9 +389,16 @@ void rejects_invalid_project_body_summaries(pp::tests::Harness& h) auto bad_layer_name = minimal_project(); bad_layer_name[ppi_header_size + (128U * 128U * 4U) + 24U] = std::byte { 255 }; + auto non_finite_opacity = minimal_project(); + write_f32_at( + non_finite_opacity, + ppi_header_size + (128U * 128U * 4U) + 20U, + std::numeric_limits::quiet_NaN()); + 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); + const auto non_finite_opacity_result = parse_ppi_project_summary(non_finite_opacity); PP_EXPECT(h, !truncated_result.ok()); PP_EXPECT(h, truncated_result.status().code == StatusCode::out_of_range); @@ -385,6 +406,8 @@ void rejects_invalid_project_body_summaries(pp::tests::Harness& h) 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); + PP_EXPECT(h, !non_finite_opacity_result.ok()); + PP_EXPECT(h, non_finite_opacity_result.status().code == StatusCode::invalid_argument); } void creates_minimal_project_for_roundtrip_load(pp::tests::Harness& h) @@ -743,6 +766,18 @@ void rejects_invalid_minimal_project_writer_inputs(pp::tests::Harness& h) .frame_duration_ms = 100, .dirty_faces = {}, }); + const auto non_finite_opacity = create_minimal_ppi_project(pp::assets::PpiMinimalProjectConfig { + .width = 128, + .height = 128, + .layer_name = "Ink", + .layer_metadata = pp::assets::PpiLayerMetadataConfig { + .opacity = std::numeric_limits::quiet_NaN(), + }, + .layer_count = 1, + .frame_count = 1, + .frame_duration_ms = 100, + .dirty_faces = {}, + }); const auto bad_blend_mode = create_minimal_ppi_project(pp::assets::PpiMinimalProjectConfig { .width = 128, .height = 128, @@ -798,6 +833,8 @@ void rejects_invalid_minimal_project_writer_inputs(pp::tests::Harness& h) PP_EXPECT(h, no_layers.status().code == StatusCode::out_of_range); PP_EXPECT(h, !bad_opacity.ok()); PP_EXPECT(h, bad_opacity.status().code == StatusCode::out_of_range); + PP_EXPECT(h, !non_finite_opacity.ok()); + PP_EXPECT(h, non_finite_opacity.status().code == StatusCode::invalid_argument); PP_EXPECT(h, !bad_blend_mode.ok()); PP_EXPECT(h, bad_blend_mode.status().code == StatusCode::out_of_range); PP_EXPECT(h, !duplicate_dirty_face.ok());