Reject non-finite PPI layer opacity

This commit is contained in:
2026-06-02 17:37:32 +02:00
parent 9759abde44
commit 52da64fc96
4 changed files with 50 additions and 2 deletions

View File

@@ -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,

View File

@@ -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

View File

@@ -5,6 +5,7 @@
#include <array>
#include <bit>
#include <cmath>
#include <limits>
#include <string>
#include <string_view>
@@ -338,6 +339,11 @@ pp::foundation::Result<PpiBodySummary> parse_ppi_body_impl(
seen_orders[order.value()] = true;
}
if (!std::isfinite(opacity.value())) {
return pp::foundation::Result<PpiBodySummary>::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<PpiBodySummary>::failure(
pp::foundation::Status::out_of_range("PPI layer opacity is outside the supported range"));
@@ -677,6 +683,11 @@ pp::foundation::Result<std::vector<std::byte>> 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<std::vector<std::byte>>::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<std::vector<std::byte>>::failure(
pp::foundation::Status::out_of_range("PPI layer opacity is outside the supported range"));

View File

@@ -5,6 +5,7 @@
#include <bit>
#include <cstddef>
#include <cstdint>
#include <limits>
#include <span>
#include <string_view>
#include <vector>
@@ -43,6 +44,19 @@ void append_f32(std::vector<std::byte>& bytes, float value)
append_u32(bytes, std::bit_cast<std::uint32_t>(value));
}
void write_u32_at(std::vector<std::byte>& bytes, std::size_t offset, std::uint32_t value)
{
bytes[offset + 0U] = static_cast<std::byte>(value & 0xffU);
bytes[offset + 1U] = static_cast<std::byte>((value >> 8U) & 0xffU);
bytes[offset + 2U] = static_cast<std::byte>((value >> 16U) & 0xffU);
bytes[offset + 3U] = static_cast<std::byte>((value >> 24U) & 0xffU);
}
void write_f32_at(std::vector<std::byte>& bytes, std::size_t offset, float value)
{
write_u32_at(bytes, offset, std::bit_cast<std::uint32_t>(value));
}
void append_ascii(std::vector<std::byte>& 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<float>::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<float>::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());