< Summary

Information
Class: DotnetFuzzing.Fuzzers.Utf8JsonWriterFuzzer
Assembly: DotnetFuzzing
File(s): D:\runner\runtime\src\libraries\Fuzzing\DotnetFuzzing\Fuzzers\Utf8JsonWriterFuzzer.cs
Line coverage
0%
Covered lines: 1
Uncovered lines: 152
Coverable lines: 153
Total lines: 261
Line coverage: 0.6%
Branch coverage
0%
Covered branches: 0
Total branches: 42
Branch coverage: 0%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

MethodBranch coverage Cyclomatic complexity NPath complexity Sequence coverage
FuzzTarget(...)0%32320%
WriteStringValueSegments(...)0%660%
WriteStringValueSegments(...)0%220%
EncodeToUtf8(...)0%220%
EncodeToUtf8(...)100%110%
EncodeTranscode(...)100%110%

File(s)

D:\runner\runtime\src\libraries\Fuzzing\DotnetFuzzing\Fuzzers\Utf8JsonWriterFuzzer.cs

#LineLine coverage
 1// Licensed to the .NET Foundation under one or more agreements.
 2// The .NET Foundation licenses this file to you under the MIT license.
 3
 4using System;
 5using System.Buffers;
 6using System.Buffers.Text;
 7using System.Collections;
 8using System.Collections.Generic;
 9using System.Diagnostics;
 10using System.IO;
 11using System.Runtime.CompilerServices;
 12using System.Runtime.InteropServices;
 13using System.Text;
 14using System.Text.Encodings.Web;
 15using System.Text.Json;
 16using System.Text.Unicode;
 17using SharpFuzz;
 18
 19namespace DotnetFuzzing.Fuzzers;
 20
 21internal sealed class Utf8JsonWriterFuzzer : IFuzzer
 22{
 123    public string[] TargetAssemblies { get; } = ["System.Text.Json"];
 24
 025    public string[] TargetCoreLibPrefixes => [];
 26
 27    // One of the bytes in the input is used to set various test options.
 28    // Each bit in that byte represents a different option as indicated here.
 29
 30    // Options for JsonWriterOptions
 31    private const byte IndentFlag = 1;
 32    private const byte EncoderFlag = 1 << 1;
 33    private const byte MaxDepthFlag = 1 << 2;
 34    private const byte NewLineFlag = 1 << 3;
 35    private const byte SkipValidationFlag = 1 << 4;
 36
 37    // Options for choosing between base64, UTF-8 and UTF-16 encoding
 38    private const byte EncodingMask = 0b11 << 5;
 39    private const byte Utf8EncodingFlag = 0b00 << 5;
 40    private const byte Utf16EncodingFlag = 0b01 << 5;
 41    private const byte Base64EncodingFlag = 0b10 << 5;
 42
 43    public void FuzzTarget(ReadOnlySpan<byte> bytes)
 044    {
 45        const int minLength = 10; // 2 ints, 1 byte, and 1 padding to align chars
 046        if (bytes.Length < minLength)
 047        {
 048            return;
 49        }
 50
 51        // First 2 ints are used as indices to slice the input and the following byte is used for options
 052        ReadOnlySpan<int> ints = MemoryMarshal.Cast<byte, int>(bytes);
 053        int slice1 = ints[0];
 054        int slice2 = ints[1];
 055        byte optionsByte = bytes[8];
 056        bytes = bytes.Slice(minLength);
 057        ReadOnlySpan<char> chars = MemoryMarshal.Cast<byte, char>(bytes);
 58
 59        // Validate that the indices are within bounds of the input
 060        int encoding = optionsByte & EncodingMask;
 061        if (encoding is not Utf8EncodingFlag and not Utf16EncodingFlag and not Base64EncodingFlag)
 062        {
 063            return;
 64        }
 65
 066        if (!(0 <= slice1 && slice1 <= slice2 && slice2 <= (encoding is Utf16EncodingFlag ? chars.Length : bytes.Length)
 067        {
 068            return;
 69        }
 70
 71        // Set up options based on the first byte
 072        bool indented = (optionsByte & IndentFlag) == 0;
 073        JsonWriterOptions options = new()
 074        {
 075            Encoder = (optionsByte & EncoderFlag) == 0 ? JavaScriptEncoder.Default : JavaScriptEncoder.UnsafeRelaxedJson
 076            Indented = indented,
 077            MaxDepth = (optionsByte & MaxDepthFlag) == 0 ? 1 : 0,
 078            NewLine = (optionsByte & NewLineFlag) == 0 ? "\n" : "\r\n",
 079            SkipValidation = (optionsByte & SkipValidationFlag) == 0,
 080        };
 81
 82        // Compute the expected result by using the encoder directly and the input
 083        int maxExpandedSizeBytes = 6 * bytes.Length + 2;
 084        byte[] expectedBuffer = ArrayPool<byte>.Shared.Rent(maxExpandedSizeBytes);
 085        Span<byte> expected =
 086            expectedBuffer.AsSpan(0, encoding == Utf16EncodingFlag
 087                ? EncodeToUtf8(chars, expectedBuffer, options.Encoder)
 088                : EncodeToUtf8(bytes, expectedBuffer, options.Encoder, encoding == Base64EncodingFlag));
 89
 90        // Compute the actual result by using Utf8JsonWriter. Each iteration is a different slice of the input, but the 
 091        byte[] actualBuffer = new byte[expected.Length];
 092        foreach (ReadOnlySpan<Range> ranges in new[]
 093        {
 094            new[] { 0.. },
 095            new[] { 0..slice1, slice1.. },
 096            new[] { 0..slice1, slice1..slice2, slice2.. },
 097        })
 098        {
 099            using MemoryStream stream = new(actualBuffer);
 0100            using Utf8JsonWriter writer = new(stream, options);
 101
 0102            if (encoding == Utf16EncodingFlag)
 0103            {
 0104                WriteStringValueSegments(writer, chars, ranges);
 0105            }
 106            else
 0107            {
 0108                WriteStringValueSegments(writer, bytes, ranges, encoding == Base64EncodingFlag);
 0109            }
 110
 0111            writer.Flush();
 112
 113            // Compare the expected and actual results
 0114            Assert.SequenceEqual(expected, actualBuffer);
 0115            Assert.Equal(expected.Length, writer.BytesCommitted);
 0116            Assert.Equal(0, writer.BytesPending);
 117
 0118            Array.Clear(actualBuffer);
 0119        }
 120
 121        // Additional test for mixing UTF-8 and UTF-16 encoding. The alignment math is easier in UTF-16 mode so just run
 0122        if (encoding == Utf16EncodingFlag && !options.SkipValidation)
 0123        {
 0124            Array.Clear(expectedBuffer);
 125
 0126            {
 0127                ReadOnlySpan<char> firstSegment = chars[slice1..];
 0128                ReadOnlySpan<byte> secondSegment = bytes[0..(2 * slice1)];
 129
 0130                expected = expectedBuffer.AsSpan(0, EncodeToUtf8(firstSegment, expectedBuffer, options.Encoder));
 131
 0132                actualBuffer = new byte[expected.Length];
 0133                using MemoryStream stream = new(actualBuffer);
 0134                using Utf8JsonWriter writer = new(stream, options);
 135
 136                // UTF-16 + UTF-8
 0137                writer.WriteStringValueSegment(firstSegment, false);
 0138                Assert.Throws<InvalidOperationException, ReadOnlySpan<byte>>(state => writer.WriteStringValueSegment(sta
 139
 0140                stream.Position = 0;
 0141                writer.Reset();
 142
 143                // UTF-16 + Base64
 0144                writer.WriteStringValueSegment(firstSegment, false);
 0145                Assert.Throws<InvalidOperationException, ReadOnlySpan<byte>>(state => writer.WriteBase64StringSegment(st
 0146            }
 147
 0148            Array.Clear(expectedBuffer);
 149
 0150            {
 0151                ReadOnlySpan<byte> firstSegment = bytes[0..(2 * slice1)];
 0152                ReadOnlySpan<char> secondSegment = chars[slice1..];
 153
 0154                expected = expectedBuffer.AsSpan(0, EncodeToUtf8(firstSegment, expectedBuffer, options.Encoder, base64En
 155
 0156                actualBuffer = new byte[expected.Length];
 0157                using MemoryStream stream = new(actualBuffer);
 0158                using Utf8JsonWriter writer = new(stream, options);
 159
 160                // UTF-8 + UTF-16
 0161                writer.WriteStringValueSegment(firstSegment, false);
 0162                Assert.Throws<InvalidOperationException, ReadOnlySpan<char>>(state => writer.WriteStringValueSegment(sta
 163
 0164                stream.Position = 0;
 0165                writer.Reset();
 166
 167                // UTF-8 + Base64
 0168                writer.WriteStringValueSegment(firstSegment, false);
 0169                Assert.Throws<InvalidOperationException, ReadOnlySpan<byte>>(state => writer.WriteBase64StringSegment(st
 0170            }
 171
 0172            Array.Clear(expectedBuffer);
 173
 0174            {
 0175                ReadOnlySpan<byte> firstSegment = bytes[0..(2 * slice1)];
 0176                ReadOnlySpan<char> secondSegment = chars[slice1..];
 177
 0178                expected = expectedBuffer.AsSpan(0, EncodeToUtf8(firstSegment, expectedBuffer, options.Encoder, base64En
 179
 0180                actualBuffer = new byte[expected.Length];
 0181                using MemoryStream stream = new(actualBuffer);
 0182                using Utf8JsonWriter writer = new(stream, options);
 183
 184                // Base64 + UTF-16
 0185                writer.WriteBase64StringSegment(firstSegment, false);
 0186                Assert.Throws<InvalidOperationException, ReadOnlySpan<char>>(state => writer.WriteStringValueSegment(sta
 187
 0188                stream.Position = 0;
 0189                writer.Reset();
 190
 191                // Base64 + UTF-8
 0192                writer.WriteBase64StringSegment(firstSegment, false);
 0193                Assert.Throws<InvalidOperationException, ReadOnlySpan<byte>>(state => writer.WriteStringValueSegment(sta
 0194            }
 0195        }
 196
 0197        ArrayPool<byte>.Shared.Return(expectedBuffer);
 0198    }
 199
 200    private static void WriteStringValueSegments(Utf8JsonWriter writer, ReadOnlySpan<byte> bytes, ReadOnlySpan<Range> ra
 0201    {
 0202        if (base64Encode)
 0203        {
 0204            for (int i = 0; i < ranges.Length; i++)
 0205            {
 0206                writer.WriteBase64StringSegment(bytes[ranges[i]], i == ranges.Length - 1);
 0207            }
 0208        }
 209        else
 0210        {
 0211            for (int i = 0; i < ranges.Length; i++)
 0212            {
 0213                writer.WriteStringValueSegment(bytes[ranges[i]], i == ranges.Length - 1);
 0214            }
 0215        }
 0216    }
 217
 218    private static void WriteStringValueSegments(Utf8JsonWriter writer, ReadOnlySpan<char> chars, ReadOnlySpan<Range> ra
 0219    {
 0220        for (int i = 0; i < ranges.Length; i++)
 0221        {
 0222            writer.WriteStringValueSegment(chars[ranges[i]], i == ranges.Length - 1);
 0223        }
 0224    }
 225
 226    private static int EncodeToUtf8(ReadOnlySpan<byte> bytes, Span<byte> destBuffer, JavaScriptEncoder encoder, bool bas
 0227    {
 0228        destBuffer[0] = (byte)'"';
 229
 230        int written;
 0231        if (base64Encode)
 0232        {
 0233            Base64.EncodeToUtf8(bytes, destBuffer[1..], out _, out written, isFinalBlock: true);
 0234        }
 235        else
 0236        {
 0237            encoder.EncodeUtf8(bytes, destBuffer[1..], out _, out written, isFinalBlock: true);
 0238        }
 239
 0240        destBuffer[++written] = (byte)'"';
 0241        return written + 1;
 0242    }
 243
 244    private static int EncodeToUtf8(ReadOnlySpan<char> chars, Span<byte> destBuffer, JavaScriptEncoder encoder)
 0245    {
 0246        int written = 1;
 0247        destBuffer[0] = (byte)'"';
 0248        destBuffer[written += EncodeTranscode(chars, destBuffer[1..], encoder)] = (byte)'"';
 0249        return written + 1;
 0250    }
 251
 252    private static int EncodeTranscode(ReadOnlySpan<char> chars, Span<byte> destBuffer, JavaScriptEncoder encoder, bool 
 0253    {
 0254        var utf16buffer = ArrayPool<char>.Shared.Rent(6 * chars.Length);
 0255        encoder.Encode(chars, utf16buffer, out _, out int written, isFinalBlock: true);
 256
 0257        Utf8.FromUtf16(utf16buffer.AsSpan(0, written), destBuffer, out _, out written, isFinalBlock);
 0258        ArrayPool<char>.Shared.Return(utf16buffer);
 0259        return written;
 0260    }
 261}