#include "assets/ppi_header.h" #include "test_harness.h" #include #include #include #include #include #include using pp::assets::parse_ppi_header; using pp::assets::create_minimal_ppi_project; using pp::assets::decode_ppi_project_images; using pp::assets::parse_ppi_project_index; 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; using pp::foundation::StatusCode; namespace { void append_u32(std::vector& bytes, std::uint32_t value) { bytes.push_back(static_cast(value & 0xffU)); bytes.push_back(static_cast((value >> 8U) & 0xffU)); bytes.push_back(static_cast((value >> 16U) & 0xffU)); bytes.push_back(static_cast((value >> 24U) & 0xffU)); } void append_u32_be(std::vector& bytes, std::uint32_t value) { bytes.push_back(static_cast((value >> 24U) & 0xffU)); bytes.push_back(static_cast((value >> 16U) & 0xffU)); bytes.push_back(static_cast((value >> 8U) & 0xffU)); bytes.push_back(static_cast(value & 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 { std::byte { 'P' }, std::byte { 'P' }, std::byte { 'I' }, std::byte { 0 }, }; append_u32(bytes, 0); append_u32(bytes, 4); append_u32(bytes, 0); append_u32(bytes, 2); append_u32(bytes, 3); append_u32(bytes, 1024); append_u32(bytes, 128); append_u32(bytes, 128); append_u32(bytes, 4); 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 png_ihdr_payload( std::uint32_t width, std::uint32_t height, std::uint8_t bit_depth = 8U, std::uint8_t color_type = 6U) { std::vector bytes { std::byte { 0x89 }, std::byte { 0x50 }, std::byte { 0x4e }, std::byte { 0x47 }, std::byte { 0x0d }, std::byte { 0x0a }, std::byte { 0x1a }, std::byte { 0x0a }, }; append_u32_be(bytes, 13); bytes.push_back(std::byte { 'I' }); bytes.push_back(std::byte { 'H' }); bytes.push_back(std::byte { 'D' }); bytes.push_back(std::byte { 'R' }); append_u32_be(bytes, width); append_u32_be(bytes, height); bytes.push_back(static_cast(bit_depth)); bytes.push_back(static_cast(color_type)); bytes.push_back(std::byte { 0 }); bytes.push_back(std::byte { 0 }); bytes.push_back(std::byte { 0 }); append_u32_be(bytes, 0); return bytes; } std::vector transparent_png_1x1() { return { std::byte { 0x89 }, std::byte { 0x50 }, std::byte { 0x4e }, std::byte { 0x47 }, std::byte { 0x0d }, std::byte { 0x0a }, std::byte { 0x1a }, std::byte { 0x0a }, std::byte { 0x00 }, std::byte { 0x00 }, std::byte { 0x00 }, std::byte { 0x0d }, std::byte { 0x49 }, std::byte { 0x48 }, std::byte { 0x44 }, std::byte { 0x52 }, std::byte { 0x00 }, std::byte { 0x00 }, std::byte { 0x00 }, std::byte { 0x01 }, std::byte { 0x00 }, std::byte { 0x00 }, std::byte { 0x00 }, std::byte { 0x01 }, std::byte { 0x08 }, std::byte { 0x06 }, std::byte { 0x00 }, std::byte { 0x00 }, std::byte { 0x00 }, std::byte { 0x1f }, std::byte { 0x15 }, std::byte { 0xc4 }, std::byte { 0x89 }, std::byte { 0x00 }, std::byte { 0x00 }, std::byte { 0x00 }, std::byte { 0x0b }, std::byte { 0x49 }, std::byte { 0x44 }, std::byte { 0x41 }, std::byte { 0x54 }, std::byte { 0x78 }, std::byte { 0x9c }, std::byte { 0x63 }, std::byte { 0x60 }, std::byte { 0x00 }, std::byte { 0x02 }, std::byte { 0x00 }, std::byte { 0x00 }, std::byte { 0x05 }, std::byte { 0x00 }, std::byte { 0x01 }, std::byte { 0x7a }, std::byte { 0x5e }, std::byte { 0xab }, std::byte { 0x3f }, std::byte { 0x00 }, std::byte { 0x00 }, std::byte { 0x00 }, std::byte { 0x00 }, std::byte { 0x49 }, std::byte { 0x45 }, std::byte { 0x4e }, std::byte { 0x44 }, std::byte { 0xae }, std::byte { 0x42 }, std::byte { 0x60 }, std::byte { 0x82 }, }; } 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; } std::vector project_with_single_face_payload( std::vector payload, std::uint32_t dirty_width = 8, std::uint32_t dirty_height = 4) { auto bytes = valid_header(); bytes.resize(ppi_header_size + (128U * 128U * 4U), std::byte { 0 }); 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); append_u32(bytes, 1); append_u32(bytes, 2); append_u32(bytes, 3); append_u32(bytes, 2 + dirty_width); append_u32(bytes, 3 + dirty_height); append_u32(bytes, static_cast(payload.size())); bytes.insert(bytes.end(), payload.begin(), payload.end()); for (std::uint32_t i = 1; i < 6U; ++i) { append_u32(bytes, 0); } append_u32(bytes, 0); return bytes; } void parses_legacy_ppi_header(pp::tests::Harness& h) { const auto bytes = valid_header(); const auto header = parse_ppi_header(bytes); PP_EXPECT(h, bytes.size() == ppi_header_size); PP_EXPECT(h, header.ok()); PP_EXPECT(h, header.value().document_version.major == 0U); PP_EXPECT(h, header.value().document_version.minor == 4U); PP_EXPECT(h, header.value().software_version.fix == 3U); PP_EXPECT(h, header.value().software_version.build == 1024U); PP_EXPECT(h, header.value().thumbnail.width == 128U); PP_EXPECT(h, header.value().thumbnail.height == 128U); PP_EXPECT(h, header.value().thumbnail.components == 4U); } void rejects_truncated_invalid_magic_and_bad_thumbnail(pp::tests::Harness& h) { auto truncated = valid_header(); truncated.pop_back(); auto bad_magic = valid_header(); bad_magic[0] = std::byte { 'X' }; auto bad_thumb = valid_header(); bad_thumb[32] = std::byte { 64 }; const auto truncated_result = parse_ppi_header(truncated); const auto magic_result = parse_ppi_header(bad_magic); const auto thumb_result = parse_ppi_header(bad_thumb); PP_EXPECT(h, !truncated_result.ok()); PP_EXPECT(h, truncated_result.status().code == StatusCode::out_of_range); PP_EXPECT(h, !magic_result.ok()); PP_EXPECT(h, magic_result.status().code == StatusCode::invalid_argument); PP_EXPECT(h, !thumb_result.ok()); PP_EXPECT(h, thumb_result.status().code == StatusCode::invalid_argument); } void rejects_unsupported_document_versions(pp::tests::Harness& h) { auto bad_major = valid_header(); bad_major[4] = std::byte { 1 }; auto bad_minor = valid_header(); bad_minor[8] = std::byte { 0 }; const auto major_result = parse_ppi_header(bad_major); const auto minor_result = parse_ppi_header(bad_minor); PP_EXPECT(h, !major_result.ok()); PP_EXPECT(h, major_result.status().code == StatusCode::invalid_argument); PP_EXPECT(h, !minor_result.ok()); PP_EXPECT(h, minor_result.status().code == StatusCode::invalid_argument); } void parses_project_layout_with_thumbnail_and_body(pp::tests::Harness& h) { const auto bytes = minimal_project(); const auto layout = parse_ppi_project_layout(bytes); PP_EXPECT(h, layout.ok()); 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 == 73U); } void rejects_project_layout_with_truncated_thumbnail(pp::tests::Harness& h) { auto bytes = valid_header(); bytes.resize(ppi_header_size + (128U * 128U * 4U) - 1U, std::byte { 0 }); const auto layout = parse_ppi_project_layout(bytes); PP_EXPECT(h, !layout.ok()); 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.rgba_face_payload_count == 0U); PP_EXPECT(h, summary.value().body.compressed_face_bytes == 0U); PP_EXPECT(h, summary.value().body.info_bytes == 0U); } void indexes_project_layers_frames_and_faces(pp::tests::Harness& h) { const auto project = project_with_single_face_payload(png_ihdr_payload(8, 4)); const auto index = parse_ppi_project_index(project); PP_EXPECT(h, index.ok()); PP_EXPECT(h, index.value().body.layers.size() == 1U); PP_EXPECT(h, index.value().body.layers[0].stored_order == 0U); PP_EXPECT(h, index.value().body.layers[0].name == "Ink"); PP_EXPECT(h, index.value().body.layers[0].visible); PP_EXPECT(h, index.value().body.layers[0].frames.size() == 1U); PP_EXPECT(h, index.value().body.layers[0].frames[0].duration_ms == 100U); PP_EXPECT(h, index.value().body.layers[0].frames[0].faces[0].has_data); PP_EXPECT(h, index.value().body.layers[0].frames[0].faces[0].x0 == 2U); PP_EXPECT(h, index.value().body.layers[0].frames[0].faces[0].x1 == 10U); PP_EXPECT(h, index.value().body.layers[0].frames[0].faces[0].payload_bytes == 33U); PP_EXPECT(h, index.value().body.layers[0].frames[0].faces[0].png_width == 8U); PP_EXPECT(h, !index.value().body.layers[0].frames[0].faces[1].has_data); } void validates_dirty_face_png_payload_metadata(pp::tests::Harness& h) { const auto project = project_with_single_face_payload(png_ihdr_payload(8, 4)); const auto summary = parse_ppi_project_summary(project); PP_EXPECT(h, summary.ok()); PP_EXPECT(h, summary.value().body.dirty_face_count == 1U); PP_EXPECT(h, summary.value().body.rgba_face_payload_count == 1U); PP_EXPECT(h, summary.value().body.compressed_face_bytes == 33U); } void decodes_dirty_face_png_payloads(pp::tests::Harness& h) { const auto project = project_with_single_face_payload(transparent_png_1x1(), 1, 1); const auto decoded = decode_ppi_project_images(project); PP_EXPECT(h, decoded.ok()); PP_EXPECT(h, decoded.value().faces.size() == 1U); PP_EXPECT(h, decoded.value().faces[0].layer_index == 0U); PP_EXPECT(h, decoded.value().faces[0].frame_index == 0U); PP_EXPECT(h, decoded.value().faces[0].face_index == 0U); PP_EXPECT(h, decoded.value().faces[0].image.width == 1U); PP_EXPECT(h, decoded.value().faces[0].image.height == 1U); PP_EXPECT(h, decoded.value().faces[0].image.pixels.size() == 4U); PP_EXPECT(h, decoded.value().faces[0].image.pixels[3] == 0U); } void rejects_metadata_only_payload_when_decoding_pixels(pp::tests::Harness& h) { const auto project = project_with_single_face_payload(png_ihdr_payload(8, 4)); const auto decoded = decode_ppi_project_images(project); PP_EXPECT(h, !decoded.ok()); PP_EXPECT(h, decoded.status().code == StatusCode::invalid_argument); } void rejects_invalid_dirty_face_png_payloads(pp::tests::Harness& h) { auto mismatched_dimensions = project_with_single_face_payload(png_ihdr_payload(7, 4)); auto non_rgba = project_with_single_face_payload(png_ihdr_payload(8, 4, 8, 2)); auto bad_signature_payload = png_ihdr_payload(8, 4); bad_signature_payload[0] = std::byte { 0 }; auto bad_signature = project_with_single_face_payload(bad_signature_payload); const auto mismatched_result = parse_ppi_project_summary(mismatched_dimensions); const auto non_rgba_result = parse_ppi_project_summary(non_rgba); const auto bad_signature_result = parse_ppi_project_summary(bad_signature); PP_EXPECT(h, !mismatched_result.ok()); PP_EXPECT(h, mismatched_result.status().code == StatusCode::invalid_argument); PP_EXPECT(h, !non_rgba_result.ok()); PP_EXPECT(h, non_rgba_result.status().code == StatusCode::invalid_argument); PP_EXPECT(h, !bad_signature_result.ok()); PP_EXPECT(h, bad_signature_result.status().code == StatusCode::invalid_argument); } 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); } void creates_minimal_project_for_roundtrip_load(pp::tests::Harness& h) { const auto project = create_minimal_ppi_project(pp::assets::PpiMinimalProjectConfig { .width = 256, .height = 128, .layer_name = "Roundtrip", .frame_duration_ms = 333, }); PP_EXPECT(h, project.ok()); const auto index = parse_ppi_project_index(project.value()); PP_EXPECT(h, index.ok()); PP_EXPECT(h, index.value().layout.thumbnail_bytes == 128U * 128U * 4U); PP_EXPECT(h, index.value().body.summary.width == 256U); PP_EXPECT(h, index.value().body.summary.height == 128U); PP_EXPECT(h, index.value().body.summary.layer_count == 1U); PP_EXPECT(h, index.value().body.summary.declared_frame_count == 1U); PP_EXPECT(h, index.value().body.summary.dirty_face_count == 0U); PP_EXPECT(h, index.value().body.layers[0].name == "Roundtrip"); PP_EXPECT(h, index.value().body.layers[0].opacity == 1.0F); PP_EXPECT(h, index.value().body.layers[0].visible); PP_EXPECT(h, index.value().body.layers[0].frames[0].duration_ms == 333U); } void rejects_invalid_minimal_project_writer_inputs(pp::tests::Harness& h) { const auto no_size = create_minimal_ppi_project(pp::assets::PpiMinimalProjectConfig { .width = 0, .height = 128, .layer_name = "Ink", .frame_duration_ms = 100, }); const auto no_name = create_minimal_ppi_project(pp::assets::PpiMinimalProjectConfig { .width = 128, .height = 128, .layer_name = "", .frame_duration_ms = 100, }); const auto no_duration = create_minimal_ppi_project(pp::assets::PpiMinimalProjectConfig { .width = 128, .height = 128, .layer_name = "Ink", .frame_duration_ms = 0, }); PP_EXPECT(h, !no_size.ok()); PP_EXPECT(h, no_size.status().code == StatusCode::invalid_argument); PP_EXPECT(h, !no_name.ok()); PP_EXPECT(h, no_name.status().code == StatusCode::invalid_argument); PP_EXPECT(h, !no_duration.ok()); PP_EXPECT(h, no_duration.status().code == StatusCode::invalid_argument); } } int main() { pp::tests::Harness harness; harness.run("parses_legacy_ppi_header", parses_legacy_ppi_header); harness.run("rejects_truncated_invalid_magic_and_bad_thumbnail", rejects_truncated_invalid_magic_and_bad_thumbnail); 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("indexes_project_layers_frames_and_faces", indexes_project_layers_frames_and_faces); harness.run("validates_dirty_face_png_payload_metadata", validates_dirty_face_png_payload_metadata); harness.run("decodes_dirty_face_png_payloads", decodes_dirty_face_png_payloads); harness.run("rejects_metadata_only_payload_when_decoding_pixels", rejects_metadata_only_payload_when_decoding_pixels); harness.run("rejects_invalid_dirty_face_png_payloads", rejects_invalid_dirty_face_png_payloads); harness.run("rejects_invalid_project_body_summaries", rejects_invalid_project_body_summaries); harness.run("creates_minimal_project_for_roundtrip_load", creates_minimal_project_for_roundtrip_load); harness.run("rejects_invalid_minimal_project_writer_inputs", rejects_invalid_minimal_project_writer_inputs); return harness.finish(); }