|
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.IO;
using System.Text;
using System.Xml;
namespace System.Runtime.Serialization.Json
{
// This wrapper does not support seek.
// Supports: UTF-8, Unicode, BigEndianUnicode
// ASSUMPTION (Microsoft): This class will only be used for EITHER reading OR writing. It can be done, it would just mean more buffers.
internal sealed class JsonEncodingStreamWrapper : Stream
{
private const int BufferLength = 128;
private int _byteCount;
private int _byteOffset;
private byte[]? _bytes;
private char[]? _chars;
private Decoder? _dec;
private Encoder? _enc;
private Encoding? _encoding;
private SupportedEncoding _encodingCode;
private readonly bool _isReading;
private BufferedStream _stream = null!; // initialized in InitForXXX
public JsonEncodingStreamWrapper(Stream stream, Encoding? encoding, bool isReader)
{
_isReading = isReader;
if (isReader)
{
InitForReading(stream, encoding);
}
else
{
ArgumentNullException.ThrowIfNull(encoding);
InitForWriting(stream, encoding);
}
}
private enum SupportedEncoding
{
UTF8,
UTF16LE,
UTF16BE,
None
}
// This stream wrapper does not support duplex
public override bool CanRead
{
get
{
if (!_isReading)
{
return false;
}
return _stream.CanRead;
}
}
// The encoding conversion and buffering breaks seeking.
public override bool CanSeek
{
get { return false; }
}
// Delegate properties
public override bool CanTimeout
{
get { return _stream.CanTimeout; }
}
// This stream wrapper does not support duplex
public override bool CanWrite
{
get
{
if (_isReading)
{
return false;
}
return _stream.CanWrite;
}
}
public override long Length
{
get { return _stream.Length; }
}
// The encoding conversion and buffering breaks seeking.
public override long Position
{
get
{
#pragma warning suppress 56503 // The contract for non seekable stream is to throw exception
throw new NotSupportedException();
}
set { throw new NotSupportedException(); }
}
public override int ReadTimeout
{
get { return _stream.ReadTimeout; }
set { _stream.ReadTimeout = value; }
}
public override int WriteTimeout
{
get { return _stream.WriteTimeout; }
set { _stream.WriteTimeout = value; }
}
public static ArraySegment<byte> ProcessBuffer(byte[] buffer, int offset, int count, Encoding? encoding)
{
try
{
SupportedEncoding expectedEnc = GetSupportedEncoding(encoding);
SupportedEncoding dataEnc;
if (count < 2)
{
dataEnc = SupportedEncoding.UTF8;
}
else
{
dataEnc = ReadEncoding(buffer[offset], buffer[offset + 1]);
}
if ((expectedEnc != SupportedEncoding.None) && (expectedEnc != dataEnc))
{
ThrowExpectedEncodingMismatch(expectedEnc, dataEnc);
}
// Fastpath: UTF-8
if (dataEnc == SupportedEncoding.UTF8)
{
return new ArraySegment<byte>(buffer, offset, count);
}
// Convert to UTF-8
return
new ArraySegment<byte>(DataContractSerializer.ValidatingUTF8.GetBytes(GetEncoding(dataEnc).GetChars(buffer, offset, count)));
}
catch (DecoderFallbackException e)
{
throw new XmlException(SR.JsonInvalidBytes, e);
}
}
protected override void Dispose(bool disposing)
{
Flush();
_stream.Dispose();
base.Dispose(disposing);
}
public override void Flush()
{
_stream.Flush();
}
public override int Read(byte[] buffer, int offset, int count) =>
Read(new Span<byte>(buffer, offset, count));
public override int Read(Span<byte> buffer)
{
try
{
if (_byteCount == 0)
{
if (_encodingCode == SupportedEncoding.UTF8)
{
return _stream.Read(buffer);
}
Debug.Assert(_bytes != null);
Debug.Assert(_chars != null);
// No more bytes than can be turned into characters
_byteOffset = 0;
_byteCount = _stream.Read(_bytes, _byteCount, (_chars.Length - 1) * 2);
// Check for end of stream
if (_byteCount == 0)
{
return 0;
}
// Fix up incomplete chars
CleanupCharBreak();
// Change encoding
int charCount = _encoding!.GetChars(_bytes, 0, _byteCount, _chars, 0);
_byteCount = Encoding.UTF8.GetBytes(_chars, 0, charCount, _bytes, 0);
}
// Give them bytes
int count = buffer.Length;
if (_byteCount < count)
{
count = _byteCount;
}
_bytes.AsSpan(_byteOffset, count).CopyTo(buffer);
_byteOffset += count;
_byteCount -= count;
return count;
}
catch (DecoderFallbackException ex)
{
throw new XmlException(SR.JsonInvalidBytes, ex);
}
}
public override int ReadByte()
{
if (_byteCount == 0 && _encodingCode == SupportedEncoding.UTF8)
{
return _stream.ReadByte();
}
byte b = 0;
if (Read(new Span<byte>(ref b)) == 0)
{
return -1;
}
return b;
}
public override long Seek(long offset, SeekOrigin origin)
{
throw new NotSupportedException();
}
// Delegate methods
public override void SetLength(long value)
{
throw new NotSupportedException();
}
public override void Write(byte[] buffer, int offset, int count) =>
Write(new ReadOnlySpan<byte>(buffer, offset, count));
public override void Write(ReadOnlySpan<byte> buffer)
{
// Optimize UTF-8 case
if (_encodingCode == SupportedEncoding.UTF8)
{
_stream.Write(buffer);
return;
}
Debug.Assert(_bytes != null);
Debug.Assert(_chars != null);
while (buffer.Length > 0)
{
int size = Math.Min(_chars.Length, buffer.Length);
int charCount = _dec!.GetChars(buffer.Slice(0, size), _chars, false);
_byteCount = _enc!.GetBytes(_chars, 0, charCount, _bytes, 0, false);
_stream.Write(_bytes, 0, _byteCount);
buffer = buffer.Slice(size);
}
}
public override void WriteByte(byte b)
{
if (_encodingCode == SupportedEncoding.UTF8)
{
_stream.WriteByte(b);
return;
}
Write(new ReadOnlySpan<byte>(in b));
}
private static Encoding GetEncoding(SupportedEncoding e) =>
e switch
{
SupportedEncoding.UTF8 => DataContractSerializer.ValidatingUTF8,
SupportedEncoding.UTF16LE => DataContractSerializer.ValidatingUTF16,
SupportedEncoding.UTF16BE => DataContractSerializer.ValidatingBEUTF16,
_ => throw new XmlException(SR.JsonEncodingNotSupported),
};
private static string GetEncodingName(SupportedEncoding enc) =>
enc switch
{
SupportedEncoding.UTF8 => "utf-8",
SupportedEncoding.UTF16LE => "utf-16LE",
SupportedEncoding.UTF16BE => "utf-16BE",
_ => throw new XmlException(SR.JsonEncodingNotSupported),
};
private static SupportedEncoding GetSupportedEncoding(Encoding? encoding)
{
if (encoding == null)
{
return SupportedEncoding.None;
}
if (encoding.WebName == DataContractSerializer.ValidatingUTF8.WebName)
{
return SupportedEncoding.UTF8;
}
else if (encoding.WebName == DataContractSerializer.ValidatingUTF16.WebName)
{
return SupportedEncoding.UTF16LE;
}
else if (encoding.WebName == DataContractSerializer.ValidatingBEUTF16.WebName)
{
return SupportedEncoding.UTF16BE;
}
else
{
throw new XmlException(SR.JsonEncodingNotSupported);
}
}
private static SupportedEncoding ReadEncoding(byte b1, byte b2)
{
if (b1 == 0x00 && b2 != 0x00)
{
return SupportedEncoding.UTF16BE;
}
else if (b1 != 0x00 && b2 == 0x00)
{
// 857 It's possible to misdetect UTF-32LE as UTF-16LE, but that's OK.
return SupportedEncoding.UTF16LE;
}
else if (b1 == 0x00 && b2 == 0x00)
{
// UTF-32BE not supported
throw new XmlException(SR.JsonInvalidBytes);
}
else
{
return SupportedEncoding.UTF8;
}
}
private static void ThrowExpectedEncodingMismatch(SupportedEncoding expEnc, SupportedEncoding actualEnc)
{
throw new XmlException(SR.Format(SR.JsonExpectedEncoding, GetEncodingName(expEnc), GetEncodingName(actualEnc)));
}
private void CleanupCharBreak()
{
Debug.Assert(_bytes != null);
int max = _byteOffset + _byteCount;
// Read on 2 byte boundaries
if ((_byteCount % 2) != 0)
{
int b = _stream.ReadByte();
if (b < 0)
{
throw new XmlException(SR.JsonUnexpectedEndOfFile);
}
_bytes[max++] = (byte)b;
_byteCount++;
}
// Don't cut off a surrogate character
int w;
if (_encodingCode == SupportedEncoding.UTF16LE)
{
w = _bytes[max - 2] + (_bytes[max - 1] << 8);
}
else
{
w = _bytes[max - 1] + (_bytes[max - 2] << 8);
}
if ((w & 0xDC00) != 0xDC00 && w >= 0xD800 && w <= 0xDBFF) // First 16-bit number of surrogate pair
{
int b1 = _stream.ReadByte();
int b2 = _stream.ReadByte();
if (b2 < 0)
{
throw new XmlException(SR.JsonUnexpectedEndOfFile);
}
_bytes[max++] = (byte)b1;
_bytes[max++] = (byte)b2;
_byteCount += 2;
}
}
[MemberNotNull(nameof(_chars))]
[MemberNotNull(nameof(_bytes))]
private void EnsureBuffers()
{
EnsureByteBuffer();
_chars ??= new char[BufferLength];
}
[MemberNotNull(nameof(_bytes))]
private void EnsureByteBuffer()
{
if (_bytes != null)
{
return;
}
_bytes = new byte[BufferLength * 4];
_byteOffset = 0;
_byteCount = 0;
}
private void FillBuffer(int count)
{
Debug.Assert(_bytes != null);
count -= _byteCount;
if (count > 0)
{
_byteCount += _stream.ReadAtLeast(_bytes.AsSpan(_byteOffset + _byteCount, count), count, throwOnEndOfStream: false);
}
}
private void InitForReading(Stream inputStream, Encoding? expectedEncoding)
{
try
{
_stream = new BufferedStream(inputStream);
SupportedEncoding expectedEnc = GetSupportedEncoding(expectedEncoding);
SupportedEncoding dataEnc = ReadEncoding();
if ((expectedEnc != SupportedEncoding.None) && (expectedEnc != dataEnc))
{
ThrowExpectedEncodingMismatch(expectedEnc, dataEnc);
}
// Fastpath: UTF-8 (do nothing)
if (dataEnc != SupportedEncoding.UTF8)
{
// Convert to UTF-8
EnsureBuffers();
FillBuffer((BufferLength - 1) * 2);
_encodingCode = dataEnc;
_encoding = GetEncoding(dataEnc);
CleanupCharBreak();
int count = _encoding.GetChars(_bytes, _byteOffset, _byteCount, _chars, 0);
_byteOffset = 0;
_byteCount = DataContractSerializer.ValidatingUTF8.GetBytes(_chars, 0, count, _bytes, 0);
}
}
catch (DecoderFallbackException ex)
{
throw new XmlException(SR.JsonInvalidBytes, ex);
}
}
private void InitForWriting(Stream outputStream, Encoding writeEncoding)
{
_encoding = writeEncoding;
_stream = new BufferedStream(outputStream);
// Set the encoding code
_encodingCode = GetSupportedEncoding(writeEncoding);
if (_encodingCode != SupportedEncoding.UTF8)
{
EnsureBuffers();
_dec = DataContractSerializer.ValidatingUTF8.GetDecoder();
_enc = _encoding.GetEncoder();
}
}
private SupportedEncoding ReadEncoding()
{
int b1 = _stream.ReadByte();
int b2 = _stream.ReadByte();
EnsureByteBuffer();
SupportedEncoding e;
if (b1 == -1)
{
e = SupportedEncoding.UTF8;
_byteCount = 0;
}
else if (b2 == -1)
{
e = SupportedEncoding.UTF8;
_bytes[0] = (byte)b1;
_byteCount = 1;
}
else
{
e = ReadEncoding((byte)b1, (byte)b2);
_bytes[0] = (byte)b1;
_bytes[1] = (byte)b2;
_byteCount = 2;
}
return e;
}
}
}
|