From 53fc5f9a57bd1a38cc1be1e9ff553fca260fa0e6 Mon Sep 17 00:00:00 2001 From: omigamedev Date: Tue, 2 Jun 2026 17:45:29 +0200 Subject: [PATCH] Reject duplicate document snapshot payloads --- docs/modernization/build-inventory.md | 2 +- docs/modernization/roadmap.md | 3 +- src/document/document.cpp | 16 ++++++++ tests/document/document_tests.cpp | 53 +++++++++++++++++++++++++++ 4 files changed, 72 insertions(+), 2 deletions(-) diff --git a/docs/modernization/build-inventory.md b/docs/modernization/build-inventory.md index 98fd7ae..22f52e0 100644 --- a/docs/modernization/build-inventory.md +++ b/docs/modernization/build-inventory.md @@ -96,7 +96,7 @@ Known local toolchain state: including foundation binary-stream/event/logging/task queue coverage, PNG metadata and decode, PPI header/layout/non-finite opacity and blend-mode rejection, settings document, document snapshot/per-layer-frame/move/duration/face-pixel/PPI export coverage, - snapshot-embedded face-payload rejection, paint brush/final-blend/ + snapshot-embedded duplicate/invalid face-payload and selection-mask rejection, paint brush/final-blend/ stroke-alpha-blend/stroke spacing/stroke stress/stroke-script coverage, renderer shader descriptor and OpenGL capability coverage, UI color parsing, and layout XML parse coverage. diff --git a/docs/modernization/roadmap.md b/docs/modernization/roadmap.md index 434257e..43cd782 100644 --- a/docs/modernization/roadmap.md +++ b/docs/modernization/roadmap.md @@ -330,7 +330,8 @@ construction, per-layer frame metadata, layer metadata operations, frame move/duration queries, renderer-free RGBA8 cube-face payload storage, renderer-free alpha8 selection-mask storage, PPI image import/export, and layer/frame/undo-redo history invariant tests. Snapshot construction validates -embedded face-pixel payload bounds and byte counts. +embedded face-pixel payload bounds, byte counts, duplicate face payloads, and +duplicate selection masks. `pp_renderer_api` has started with renderer-neutral texture/readback descriptors and validation tests. `pp_paint_renderer` has started with deterministic CPU layer compositing over renderer extents using diff --git a/src/document/document.cpp b/src/document/document.cpp index 802ddc9..167dca4 100644 --- a/src/document/document.cpp +++ b/src/document/document.cpp @@ -1,6 +1,7 @@ #include "document/document.h" #include +#include #include #include #include @@ -238,11 +239,18 @@ namespace { std::uint32_t document_height) noexcept { for (const auto& frame : frames) { + std::array seen_faces {}; for (const auto& pixels : frame.face_pixels) { const auto pixels_status = validate_face_pixels(pixels, document_width, document_height); if (!pixels_status.ok()) { return pixels_status; } + + if (seen_faces[pixels.face_index]) { + return pp::foundation::Status::invalid_argument( + "snapshot contains duplicate face pixel payloads for a cube face"); + } + seen_faces[pixels.face_index] = true; } } @@ -366,11 +374,19 @@ pp::foundation::Result CanvasDocument::create_from_snapshot(Docu document.frames_.push_back(frame_config); } + std::array seen_selection_masks {}; for (const auto& mask : config.selection_masks) { const auto mask_status = validate_selection_mask(mask, document.width_, document.height_); if (!mask_status.ok()) { return pp::foundation::Result::failure(mask_status); } + + if (seen_selection_masks[mask.face_index]) { + return pp::foundation::Result::failure( + pp::foundation::Status::invalid_argument( + "snapshot contains duplicate selection masks for a cube face")); + } + seen_selection_masks[mask.face_index] = true; document.selection_masks_.push_back(mask); } diff --git a/tests/document/document_tests.cpp b/tests/document/document_tests.cpp index 1837ba2..5d4e94a 100644 --- a/tests/document/document_tests.cpp +++ b/tests/document/document_tests.cpp @@ -344,6 +344,29 @@ void rejects_invalid_snapshot_face_pixels(pp::tests::Harness& h) }, }, }; + const AnimationFrame duplicate_face_frames[] { + { + .duration_ms = 100, + .face_pixels = { + LayerFacePixels { + .face_index = 0, + .x = 0, + .y = 0, + .width = 1, + .height = 1, + .rgba8 = { 1, 2, 3, 4 }, + }, + LayerFacePixels { + .face_index = 0, + .x = 1, + .y = 0, + .width = 1, + .height = 1, + .rgba8 = { 5, 6, 7, 8 }, + }, + }, + }, + }; const DocumentLayerConfig bad_byte_count_layers[] { { .name = "Ink", @@ -356,6 +379,12 @@ void rejects_invalid_snapshot_face_pixels(pp::tests::Harness& h) .frames = outside_frames, }, }; + const DocumentLayerConfig duplicate_face_layers[] { + { + .name = "Ink", + .frames = duplicate_face_frames, + }, + }; const DocumentLayerConfig layers[] { { .name = "Ink", @@ -384,6 +413,13 @@ void rejects_invalid_snapshot_face_pixels(pp::tests::Harness& h) .frames = bad_byte_count_frames, .selection_masks = {}, }); + const auto duplicate_face_payload = CanvasDocument::create_from_snapshot(DocumentSnapshotConfig { + .width = 64, + .height = 64, + .layers = duplicate_face_layers, + .frames = frames, + .selection_masks = {}, + }); PP_EXPECT(h, !bad_layer_payload.ok()); PP_EXPECT(h, bad_layer_payload.status().code == StatusCode::invalid_argument); @@ -391,6 +427,8 @@ void rejects_invalid_snapshot_face_pixels(pp::tests::Harness& h) PP_EXPECT(h, outside_layer_payload.status().code == StatusCode::out_of_range); PP_EXPECT(h, !bad_root_payload.ok()); PP_EXPECT(h, bad_root_payload.status().code == StatusCode::invalid_argument); + PP_EXPECT(h, !duplicate_face_payload.ok()); + PP_EXPECT(h, duplicate_face_payload.status().code == StatusCode::invalid_argument); } void manages_animation_frames_and_duration(pp::tests::Harness& h) @@ -649,6 +687,19 @@ void rejects_invalid_selection_masks(pp::tests::Harness& h) SelectionMask { .face_index = 0, .x = 0, .y = 0, .width = 2, .height = 1, .alpha8 = { 1 } }); const auto missing_clear = document.clear_selection_mask(2); const auto bad_clear_face = document.clear_selection_mask(6); + const DocumentLayerConfig layers[] { { .name = "Ink", .frames = {} } }; + const AnimationFrame frames[] { { .duration_ms = 100, .face_pixels = {} } }; + const SelectionMask duplicate_masks[] { + { .face_index = 2, .x = 0, .y = 0, .width = 1, .height = 1, .alpha8 = { 1 } }, + { .face_index = 2, .x = 1, .y = 0, .width = 1, .height = 1, .alpha8 = { 2 } }, + }; + const auto duplicate_snapshot_masks = CanvasDocument::create_from_snapshot(DocumentSnapshotConfig { + .width = 64, + .height = 32, + .layers = layers, + .frames = frames, + .selection_masks = duplicate_masks, + }); PP_EXPECT(h, !bad_face.ok()); PP_EXPECT(h, bad_face.code == StatusCode::out_of_range); @@ -662,6 +713,8 @@ void rejects_invalid_selection_masks(pp::tests::Harness& h) PP_EXPECT(h, missing_clear.code == StatusCode::out_of_range); PP_EXPECT(h, !bad_clear_face.ok()); PP_EXPECT(h, bad_clear_face.code == StatusCode::out_of_range); + PP_EXPECT(h, !duplicate_snapshot_masks.ok()); + PP_EXPECT(h, duplicate_snapshot_masks.status().code == StatusCode::invalid_argument); PP_EXPECT(h, document.selection_mask_payload_count() == 0U); }