Reject duplicate document snapshot payloads

This commit is contained in:
2026-06-02 17:45:29 +02:00
parent f6780d183c
commit 53fc5f9a57
4 changed files with 72 additions and 2 deletions

View File

@@ -96,7 +96,7 @@ Known local toolchain state:
including foundation binary-stream/event/logging/task queue coverage, PNG metadata and 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 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/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, stroke-alpha-blend/stroke spacing/stroke stress/stroke-script coverage,
renderer shader descriptor and OpenGL capability coverage, UI renderer shader descriptor and OpenGL capability coverage, UI
color parsing, and layout XML parse coverage. color parsing, and layout XML parse coverage.

View File

@@ -330,7 +330,8 @@ construction, per-layer frame metadata, layer metadata operations, frame
move/duration queries, renderer-free RGBA8 cube-face payload storage, move/duration queries, renderer-free RGBA8 cube-face payload storage,
renderer-free alpha8 selection-mask storage, PPI image import/export, and renderer-free alpha8 selection-mask storage, PPI image import/export, and
layer/frame/undo-redo history invariant tests. Snapshot construction validates 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 `pp_renderer_api` has started with renderer-neutral
texture/readback descriptors and validation tests. `pp_paint_renderer` has texture/readback descriptors and validation tests. `pp_paint_renderer` has
started with deterministic CPU layer compositing over renderer extents using started with deterministic CPU layer compositing over renderer extents using

View File

@@ -1,6 +1,7 @@
#include "document/document.h" #include "document/document.h"
#include <algorithm> #include <algorithm>
#include <array>
#include <cmath> #include <cmath>
#include <limits> #include <limits>
#include <string> #include <string>
@@ -238,11 +239,18 @@ namespace {
std::uint32_t document_height) noexcept std::uint32_t document_height) noexcept
{ {
for (const auto& frame : frames) { for (const auto& frame : frames) {
std::array<bool, cube_face_count> seen_faces {};
for (const auto& pixels : frame.face_pixels) { for (const auto& pixels : frame.face_pixels) {
const auto pixels_status = validate_face_pixels(pixels, document_width, document_height); const auto pixels_status = validate_face_pixels(pixels, document_width, document_height);
if (!pixels_status.ok()) { if (!pixels_status.ok()) {
return pixels_status; 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> CanvasDocument::create_from_snapshot(Docu
document.frames_.push_back(frame_config); document.frames_.push_back(frame_config);
} }
std::array<bool, cube_face_count> seen_selection_masks {};
for (const auto& mask : config.selection_masks) { for (const auto& mask : config.selection_masks) {
const auto mask_status = validate_selection_mask(mask, document.width_, document.height_); const auto mask_status = validate_selection_mask(mask, document.width_, document.height_);
if (!mask_status.ok()) { if (!mask_status.ok()) {
return pp::foundation::Result<CanvasDocument>::failure(mask_status); return pp::foundation::Result<CanvasDocument>::failure(mask_status);
} }
if (seen_selection_masks[mask.face_index]) {
return pp::foundation::Result<CanvasDocument>::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); document.selection_masks_.push_back(mask);
} }

View File

@@ -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[] { const DocumentLayerConfig bad_byte_count_layers[] {
{ {
.name = "Ink", .name = "Ink",
@@ -356,6 +379,12 @@ void rejects_invalid_snapshot_face_pixels(pp::tests::Harness& h)
.frames = outside_frames, .frames = outside_frames,
}, },
}; };
const DocumentLayerConfig duplicate_face_layers[] {
{
.name = "Ink",
.frames = duplicate_face_frames,
},
};
const DocumentLayerConfig layers[] { const DocumentLayerConfig layers[] {
{ {
.name = "Ink", .name = "Ink",
@@ -384,6 +413,13 @@ void rejects_invalid_snapshot_face_pixels(pp::tests::Harness& h)
.frames = bad_byte_count_frames, .frames = bad_byte_count_frames,
.selection_masks = {}, .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.ok());
PP_EXPECT(h, bad_layer_payload.status().code == StatusCode::invalid_argument); 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, outside_layer_payload.status().code == StatusCode::out_of_range);
PP_EXPECT(h, !bad_root_payload.ok()); PP_EXPECT(h, !bad_root_payload.ok());
PP_EXPECT(h, bad_root_payload.status().code == StatusCode::invalid_argument); 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) 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 } }); 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 missing_clear = document.clear_selection_mask(2);
const auto bad_clear_face = document.clear_selection_mask(6); 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.ok());
PP_EXPECT(h, bad_face.code == StatusCode::out_of_range); 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, missing_clear.code == StatusCode::out_of_range);
PP_EXPECT(h, !bad_clear_face.ok()); PP_EXPECT(h, !bad_clear_face.ok());
PP_EXPECT(h, bad_clear_face.code == StatusCode::out_of_range); 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); PP_EXPECT(h, document.selection_mask_payload_count() == 0U);
} }