using System.Text; using AtomUI.Controls; using Shouldly; using Xunit; namespace AtomUI.Controls.Shared.Tests.ImageLoading; public class SvgContentValidatorTests { [Fact] public void Same_Document_References_Inline_Css_And_Data_Raster_Are_Allowed() { var pngData = Convert.ToBase64String(ImageLoadingTestSupport.CreatePngHeader(2, 3)); var probe = ValidateSvg($$""" """); probe.Format.ShouldBe(ImageContentFormat.Svg); probe.MediaType.ShouldBe("image/svg+xml"); probe.SvgMetadata.ShouldNotBeNull(); probe.SvgMetadata.ElementCount.ShouldBeGreaterThan(5); probe.SvgMetadata.EmbeddedImageCount.ShouldBe(1); probe.SvgMetadata.EmbeddedImageDecodedBytes.ShouldBe(24); probe.SvgMetadata.EstimatedDecodedCost.ShouldBeGreaterThan(0); } [Theory] [InlineData("image/svg+xml")] [InlineData("application/xml")] [InlineData("text/xml")] [InlineData("application/octet-stream")] [InlineData(null)] public void Approved_Svg_Media_Types_Are_Accepted(string? mediaType) { ValidateSvg( "", mediaType) .Format.ShouldBe(ImageContentFormat.Svg); } [Fact] public void Compatible_Mode_Accepts_Duplicate_Ids() { var probe = ValidateSvg(""" """); probe.Format.ShouldBe(ImageContentFormat.Svg); } [Fact] public void Strict_Mode_Rejects_Duplicate_Ids() { AssertSvgFailure( ImageLoadErrorCode.InvalidImageData, """ """, configure: builder => builder.Svg.ConformanceMode = SvgConformanceMode.Strict); } [Fact] public void Strict_Duplicate_Id_Does_Not_Mask_Unsafe_Content() { AssertSvgFailure( ImageLoadErrorCode.UnsafeVectorContent, """ """, configure: builder => builder.Svg.ConformanceMode = SvgConformanceMode.Strict); } [Fact] public void Strict_Duplicate_Id_Does_Not_Mask_Reference_Complexity() { AssertSvgFailure( ImageLoadErrorCode.VectorComplexityLimitExceeded, """ """, configure: builder => builder.Svg.ConformanceMode = SvgConformanceMode.Strict); } [Theory] [InlineData(SvgConformanceMode.Compatible)] [InlineData(SvgConformanceMode.Strict)] public void Duplicate_Id_Nested_References_Cannot_Bypass_Reference_Depth(SvgConformanceMode mode) { AssertSvgFailure( ImageLoadErrorCode.VectorComplexityLimitExceeded, """ """, configure: builder => { builder.Svg.ConformanceMode = mode; builder.Svg.MaxReferenceDepth = 1; }); } [Fact] public void Compatible_Mode_Keeps_Duplicate_Id_Occurrences_As_Separate_Reference_Nodes() { var probe = ValidateSvg(""" """); probe.SvgMetadata.ShouldNotBeNull(); probe.SvgMetadata.MaxReferenceDepth.ShouldBe(2); } [Fact] public void Compatible_Mode_Counts_References_From_Every_Duplicate_Id_Occurrence() { AssertSvgFailure( ImageLoadErrorCode.VectorComplexityLimitExceeded, """ """, configure: builder => builder.Svg.MaxReferenceDepth = 1); } [Theory] [InlineData(SvgConformanceMode.Compatible)] [InlineData(SvgConformanceMode.Strict)] public void Conformance_Mode_Does_Not_Change_Security_Rejection(SvgConformanceMode mode) { AssertSvgFailure( ImageLoadErrorCode.UnsafeVectorContent, "", configure: builder => builder.Svg.ConformanceMode = mode); } [Theory] [InlineData("]>&xxe;")] [InlineData("")] [InlineData("")] [InlineData("")] [InlineData("")] [InlineData("")] [InlineData("")] [InlineData("")] [InlineData("")] [InlineData("")] public void Unsafe_Vector_Content_Is_Rejected(string svg) { AssertSvgFailure(ImageLoadErrorCode.UnsafeVectorContent, svg); } [Theory] [InlineData("")] [InlineData("")] [InlineData("")] public void Invalid_Xml_Or_Root_Is_Rejected(string svg) { AssertSvgFailure(ImageLoadErrorCode.InvalidImageData, svg); } [Fact] public void Raster_Declared_Media_Type_Cannot_Contain_Svg() { AssertSvgFailure( ImageLoadErrorCode.ContentTypeMismatch, "", "image/png"); } [Fact] public void Element_Attribute_Depth_Path_And_Reference_Limits_Are_Enforced() { AssertSvgFailure( ImageLoadErrorCode.VectorComplexityLimitExceeded, "", configure: builder => builder.Svg.MaxElementCount = 2); AssertSvgFailure( ImageLoadErrorCode.VectorComplexityLimitExceeded, "", configure: builder => builder.Svg.MaxAttributeCount = 2); AssertSvgFailure( ImageLoadErrorCode.VectorComplexityLimitExceeded, "", configure: builder => builder.Svg.MaxElementDepth = 2); AssertSvgFailure( ImageLoadErrorCode.VectorComplexityLimitExceeded, "", configure: builder => builder.Svg.MaxPathDataCharacters = 5); AssertSvgFailure( ImageLoadErrorCode.VectorComplexityLimitExceeded, """ """, configure: builder => builder.Svg.MaxReferenceDepth = 1); } [Fact] public void Embedded_Image_Count_And_Byte_Limits_Are_Enforced() { var pngData = Convert.ToBase64String(ImageLoadingTestSupport.CreatePngHeader(1, 1)); var svg = $$""" """; AssertSvgFailure( ImageLoadErrorCode.EmbeddedResourceLimitExceeded, svg, configure: builder => builder.Svg.MaxEmbeddedImageCount = 1); AssertSvgFailure( ImageLoadErrorCode.EmbeddedResourceLimitExceeded, svg, configure: builder => builder.Svg.MaxEmbeddedImageBytes = 1); } [Fact] public void Cancellation_Is_Observed_During_Validation() { using var cancellation = new CancellationTokenSource(); cancellation.Cancel(); var validator = new ImageContentValidator(ImageLoadingTestSupport.CreateOptions()); Should.Throw(() => validator.Validate( CreateContent("", "image/svg+xml"), ImageSource.Parse("https://example.com/avatar.svg"), cancellation.Token)); } private static ImageProbeResult ValidateSvg( string svg, string? mediaType = "image/svg+xml", Action? configure = null) { var validator = new ImageContentValidator(ImageLoadingTestSupport.CreateOptions(configure)); return validator.Validate( CreateContent(svg, mediaType), ImageSource.Parse("https://example.com/avatar.svg")); } private static void AssertSvgFailure( ImageLoadErrorCode expected, string svg, string? mediaType = "image/svg+xml", Action? configure = null) { var exception = Should.Throw(() => ValidateSvg(svg, mediaType, configure)); exception.Error.Code.ShouldBe(expected); } private static ImageEncodedContent CreateContent(string svg, string? mediaType) { return ImageLoadingTestSupport.CreateContent(Encoding.UTF8.GetBytes(svg), mediaType); } }