int written = 0;
byte[] rented = null;
+ ReadOnlySpan<byte> utf8Bom = JsonConstants.Utf8Bom;
+
try
{
if (stream.CanSeek)
{
// Ask for 1 more than the length to avoid resizing later,
// which is unnecessary in the common case where the stream length doesn't change.
- long expectedLength = Math.Max(0, stream.Length - stream.Position) + 1;
+ long expectedLength = Math.Max(utf8Bom.Length, stream.Length - stream.Position) + 1;
rented = ArrayPool<byte>.Shared.Rent(checked((int)expectedLength));
}
else
int lastRead;
+ // Read up to 3 bytes to see if it's the UTF-8 BOM
+ do
+ {
+ // No need for checking for growth, the minimal rent sizes both guarantee it'll fit.
+ Debug.Assert(rented.Length >= utf8Bom.Length);
+
+ lastRead = stream.Read(
+ rented,
+ written,
+ utf8Bom.Length - written);
+
+ written += lastRead;
+ } while (lastRead > 0 && written < utf8Bom.Length);
+
+ // If we have 3 bytes, and they're the BOM, reset the write position to 0.
+ if (written == utf8Bom.Length &&
+ utf8Bom.SequenceEqual(rented.AsSpan(0, utf8Bom.Length)))
+ {
+ written = 0;
+ }
+
do
{
if (rented.Length == written)
try
{
+ // Save the length to a local to be reused across awaits.
+ int utf8BomLength = JsonConstants.Utf8Bom.Length;
+
if (stream.CanSeek)
{
// Ask for 1 more than the length to avoid resizing later,
// which is unnecessary in the common case where the stream length doesn't change.
- long expectedLength = Math.Max(0, stream.Length - stream.Position) + 1;
+ long expectedLength = Math.Max(utf8BomLength, stream.Length - stream.Position) + 1;
rented = ArrayPool<byte>.Shared.Rent(checked((int)expectedLength));
}
else
int lastRead;
+ // Read up to 3 bytes to see if it's the UTF-8 BOM
+ do
+ {
+ // No need for checking for growth, the minimal rent sizes both guarantee it'll fit.
+ Debug.Assert(rented.Length >= JsonConstants.Utf8Bom.Length);
+
+ lastRead = await stream.ReadAsync(
+ rented,
+ written,
+ utf8BomLength - written,
+ cancellationToken).ConfigureAwait(false);
+
+ written += lastRead;
+ } while (lastRead > 0 && written < utf8BomLength);
+
+ // If we have 3 bytes, and they're the BOM, reset the write position to 0.
+ if (written == utf8BomLength &&
+ JsonConstants.Utf8Bom.SequenceEqual(rented.AsSpan(0, utf8BomLength)))
+ {
+ written = 0;
+ }
+
do
{
if (rented.Length == written)
{
public static class JsonDocumentTests
{
+ private static readonly byte[] Utf8Bom = { 0xEF, 0xBB, 0xBF };
+
private static readonly Dictionary<TestCaseType, string> s_expectedConcat =
new Dictionary<TestCaseType, string>();
private static readonly Dictionary<TestCaseType, string> s_compactJson =
new Dictionary<TestCaseType, string>();
+ public static IEnumerable<object[]> BadBOMCases { get; } =
+ new object[][]
+ {
+ new object[] { "\u00EF" },
+ new object[] { "\u00EF1" },
+ new object[] { "\u00EF\u00BB" },
+ new object[] { "\u00EF\u00BB1" },
+ new object[] { "\u00EF\u00BB\u00BE" },
+ new object[] { "\u00EF\u00BB\u00BE1" },
+ new object[] { "\u00EF\u00BB\u00FB" },
+ new object[] { "\u00EF\u00BB\u00FB1" },
+
+ // Legal BOM, but no payload.
+ new object[] { "\u00EF\u00BB\u00BF" },
+ };
+
public static IEnumerable<object[]> ReducedTestCases { get; } =
new List<object[]>
{
GetAwaiter().GetResult());
}
+
+ [Fact]
+ public static void ParseJson_SeekableStream_Small()
+ {
+ byte[] data = { (byte)'1', (byte)'1' };
+
+ using (JsonDocument doc = JsonDocument.Parse(new MemoryStream(data)))
+ {
+ JsonElement root = doc.RootElement;
+ Assert.Equal(JsonValueType.Number, root.Type);
+ Assert.Equal(11, root.GetInt32());
+ }
+ }
+
+ [Fact]
+ public static void ParseJson_UnseekableStream_Small()
+ {
+ byte[] data = { (byte)'1', (byte)'1' };
+
+ using (JsonDocument doc =
+ JsonDocument.Parse(new WrappedMemoryStream(canRead: true, canWrite: false, canSeek: false, data: data)))
+ {
+ JsonElement root = doc.RootElement;
+ Assert.Equal(JsonValueType.Number, root.Type);
+ Assert.Equal(11, root.GetInt32());
+ }
+ }
+
+ [Fact]
+ public static async Task ParseJson_SeekableStream_Small_Async()
+ {
+ byte[] data = { (byte)'1', (byte)'1' };
+
+ using (JsonDocument doc = await JsonDocument.ParseAsync(new MemoryStream(data)))
+ {
+ JsonElement root = doc.RootElement;
+ Assert.Equal(JsonValueType.Number, root.Type);
+ Assert.Equal(11, root.GetInt32());
+ }
+ }
+
+ [Fact]
+ public static async Task ParseJson_UnseekableStream_Small_Async()
+ {
+ byte[] data = { (byte)'1', (byte)'1' };
+
+ using (JsonDocument doc = await JsonDocument.ParseAsync(
+ new WrappedMemoryStream(canRead: true, canWrite: false, canSeek: false, data: data)))
+ {
+ JsonElement root = doc.RootElement;
+ Assert.Equal(JsonValueType.Number, root.Type);
+ Assert.Equal(11, root.GetInt32());
+ }
+ }
+
+ [Theory]
+ [MemberData(nameof(ReducedTestCases))]
+ public static void ParseJson_SeekableStream_WithBOM(bool compactData, TestCaseType type, string jsonString)
+ {
+ ParseJson(
+ compactData,
+ type,
+ jsonString,
+ null,
+ bytes => JsonDocument.Parse(new MemoryStream(Utf8Bom.Concat(bytes).ToArray())));
+ }
+
+ [Theory]
+ [MemberData(nameof(ReducedTestCases))]
+ public static void ParseJson_SeekableStream_Async_WithBOM(bool compactData, TestCaseType type, string jsonString)
+ {
+ ParseJson(
+ compactData,
+ type,
+ jsonString,
+ null,
+ bytes => JsonDocument.ParseAsync(new MemoryStream(Utf8Bom.Concat(bytes).ToArray())).GetAwaiter().GetResult());
+ }
+
+ [Theory]
+ [MemberData(nameof(ReducedTestCases))]
+ public static void ParseJson_UnseekableStream_WithBOM(bool compactData, TestCaseType type, string jsonString)
+ {
+ ParseJson(
+ compactData,
+ type,
+ jsonString,
+ null,
+ bytes => JsonDocument.Parse(
+ new WrappedMemoryStream(canRead: true, canWrite: false, canSeek: false, Utf8Bom.Concat(bytes).ToArray())));
+ }
+
+ [Theory]
+ [MemberData(nameof(ReducedTestCases))]
+ public static void ParseJson_UnseekableStream_Async_WithBOM(bool compactData, TestCaseType type, string jsonString)
+ {
+ ParseJson(
+ compactData,
+ type,
+ jsonString,
+ null,
+ bytes => JsonDocument.ParseAsync(
+ new WrappedMemoryStream(canRead: true, canWrite: false, canSeek: false, Utf8Bom.Concat(bytes).ToArray())).
+ GetAwaiter().GetResult());
+ }
+
+ [Theory]
+ [MemberData(nameof(BadBOMCases))]
+ public static void ParseJson_SeekableStream_BadBOM(string json)
+ {
+ byte[] data = Encoding.UTF8.GetBytes(json);
+ Assert.Throws<JsonReaderException>(() => JsonDocument.Parse(new MemoryStream(data)));
+ }
+
+ [Theory]
+ [MemberData(nameof(BadBOMCases))]
+ public static Task ParseJson_SeekableStream_Async_BadBOM(string json)
+ {
+ byte[] data = Encoding.UTF8.GetBytes(json);
+ return Assert.ThrowsAsync<JsonReaderException>(() => JsonDocument.ParseAsync(new MemoryStream(data)));
+ }
+
+ [Theory]
+ [MemberData(nameof(BadBOMCases))]
+ public static void ParseJson_UnseekableStream_BadBOM(string json)
+ {
+ byte[] data = Encoding.UTF8.GetBytes(json);
+
+ Assert.Throws<JsonReaderException>(
+ () => JsonDocument.Parse(
+ new WrappedMemoryStream(canRead: true, canWrite: false, canSeek: false, data)));
+ }
+
+ [Theory]
+ [MemberData(nameof(BadBOMCases))]
+ public static Task ParseJson_UnseekableStream_Async_BadBOM(string json)
+ {
+ byte[] data = Encoding.UTF8.GetBytes(json);
+
+ return Assert.ThrowsAsync<JsonReaderException>(
+ () => JsonDocument.ParseAsync(
+ new WrappedMemoryStream(canRead: true, canWrite: false, canSeek: false, data)));
+ }
+
[Theory]
[MemberData(nameof(ReducedTestCases))]
public static void ParseJson_SequenceBytes_Single(bool compactData, TestCaseType type, string jsonString)