|
// 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.Globalization;
using System.IO;
using System.Reflection;
using System.Reflection.Metadata;
using System.Runtime.CompilerServices;
using System.Runtime.Serialization;
using System.Threading;
namespace System.Formats.Nrbf.Utils;
internal static class BinaryReaderExtensions
{
private static object? s_baseAmbiguousDstDateTime;
internal static SerializationRecordType ReadSerializationRecordType(this BinaryReader reader, AllowedRecordTypes allowed)
{
byte nextByte = reader.ReadByte();
if (nextByte > (byte)SerializationRecordType.MethodReturn // MethodReturn is the last defined value.
|| (nextByte > (byte)SerializationRecordType.ArraySingleString && nextByte < (byte)SerializationRecordType.MethodCall) // not part of the spec
|| ((uint)allowed & (1u << nextByte)) == 0) // valid, but not allowed
{
ThrowHelper.ThrowForUnexpectedRecordType(nextByte);
}
return (SerializationRecordType)nextByte;
}
internal static BinaryArrayType ReadArrayType(this BinaryReader reader)
{
// To simplify the behavior and security review of the BinaryArrayRecord type, we
// do not support reading non-zero-offset arrays. If this should change in the
// future, the NrbfDecoder.DecodeBinaryArrayRecord method and supporting infrastructure
// will need re-review.
byte arrayType = reader.ReadByte();
// Rectangular is the last defined value.
if (arrayType > (byte)BinaryArrayType.Rectangular)
{
// Custom offset arrays
if (arrayType >= 3 && arrayType <= 5)
{
throw new NotSupportedException(SR.NotSupported_NonZeroOffsets);
}
ThrowHelper.ThrowInvalidValue(arrayType);
}
return (BinaryArrayType)arrayType;
}
internal static BinaryType ReadBinaryType(this BinaryReader reader)
{
byte binaryType = reader.ReadByte();
// PrimitiveArray is the last defined value.
if (binaryType > (byte)BinaryType.PrimitiveArray)
{
ThrowHelper.ThrowInvalidValue(binaryType);
}
return (BinaryType)binaryType;
}
internal static PrimitiveType ReadPrimitiveType(this BinaryReader reader)
{
byte primitiveType = reader.ReadByte();
// Boolean is the first valid value (1), UInt64 (16) is the last one. 4 is not used at all.
if (primitiveType is 4 or < (byte)PrimitiveType.Boolean or > (byte)PrimitiveType.UInt64)
{
ThrowHelper.ThrowInvalidValue(primitiveType);
}
return (PrimitiveType)primitiveType;
}
/// <summary>
/// Reads a primitive of <paramref name="primitiveType"/> from the given <paramref name="reader"/>.
/// </summary>
internal static object ReadPrimitiveValue(this BinaryReader reader, PrimitiveType primitiveType)
=> primitiveType switch
{
PrimitiveType.Boolean => reader.ReadBoolean(),
PrimitiveType.Byte => reader.ReadByte(),
PrimitiveType.SByte => reader.ReadSByte(),
PrimitiveType.Char => reader.ParseChar(),
PrimitiveType.Int16 => reader.ReadInt16(),
PrimitiveType.UInt16 => reader.ReadUInt16(),
PrimitiveType.Int32 => reader.ReadInt32(),
PrimitiveType.UInt32 => reader.ReadUInt32(),
PrimitiveType.Int64 => reader.ReadInt64(),
PrimitiveType.UInt64 => reader.ReadUInt64(),
PrimitiveType.Single => reader.ReadSingle(),
PrimitiveType.Double => reader.ReadDouble(),
PrimitiveType.Decimal => reader.ParseDecimal(),
PrimitiveType.DateTime => CreateDateTimeFromData(reader.ReadUInt64()),
PrimitiveType.TimeSpan => new TimeSpan(reader.ReadInt64()),
_ => throw new InvalidOperationException(),
};
// BinaryFormatter serializes decimals as strings and we can't BinaryReader.ReadDecimal.
internal static decimal ParseDecimal(this BinaryReader reader)
{
// The spec (MS NRBF 2.1.1.6, https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-nrbf/10b218f5-9b2b-4947-b4b7-07725a2c8127)
// says that the length of LengthPrefixedString must be of optimal size (using as few bytes as possible).
// BinaryReader.ReadString does not enforce that and we are OK with that,
// as it takes care of handling multiple edge cases and we don't want to re-implement it.
string text = reader.ReadString();
if (!decimal.TryParse(text, NumberStyles.Number, CultureInfo.InvariantCulture, out decimal result))
{
ThrowHelper.ThrowInvalidFormat();
}
return result;
}
internal static char ParseChar(this BinaryReader reader)
{
try
{
return reader.ReadChar();
}
catch (ArgumentException) // A surrogate character was read.
{
throw new SerializationException(SR.Serialization_SurrogateCharacter);
}
}
internal static char[] ParseChars(this BinaryReader reader, int count)
{
char[]? result;
try
{
result = reader.ReadChars(count);
}
catch (ArgumentException) // A surrogate character was read.
{
throw new SerializationException(SR.Serialization_SurrogateCharacter);
}
if (result.Length != count)
{
// We might hit EOF before fully reading the requested
// number of chars. This means that ReadChars(count) could return a char[] with
// *fewer* than 'count' elements.
ThrowHelper.ThrowEndOfStreamException();
}
return result;
}
/// <summary>
/// Creates a <see cref="DateTime"/> object from raw data with validation.
/// </summary>
/// <exception cref="SerializationException"><paramref name="dateData"/> was invalid.</exception>
internal static DateTime CreateDateTimeFromData(ulong dateData)
{
ulong ticks = dateData & 0x3FFFFFFF_FFFFFFFFUL;
DateTimeKind kind = (DateTimeKind)(dateData >> 62);
try
{
return ((uint)kind <= (uint)DateTimeKind.Local) ? new DateTime((long)ticks, kind) : CreateFromAmbiguousDst(ticks);
}
catch (ArgumentException ex)
{
throw new SerializationException(ex.Message, ex);
}
[MethodImpl(MethodImplOptions.NoInlining)]
static DateTime CreateFromAmbiguousDst(ulong ticks)
{
// There's no public API to create a DateTime from an ambiguous DST, and we
// can't use private reflection to access undocumented .NET Framework APIs.
// However, the ISerializable pattern *is* a documented protocol, so we can
// use DateTime's serialization ctor to create a zero-tick "ambiguous" instance,
// then keep reusing it as the base to which we can add our tick offsets.
if (s_baseAmbiguousDstDateTime is not DateTime baseDateTime)
{
#pragma warning disable SYSLIB0050 // Type or member is obsolete
SerializationInfo si = new(typeof(DateTime), new FormatterConverter());
// We don't know the value of "ticks", so we don't specify it.
// If the code somehow runs on a very old runtime that does not know the concept of "dateData"
// (it should not be possible as the library targets .NET Standard 2.0)
// the ctor is going to throw rather than silently return an invalid value.
si.AddValue("dateData", 0xC0000000_00000000UL); // new value (serialized as ulong)
#if NET
baseDateTime = CallPrivateSerializationConstructor(si, new StreamingContext(StreamingContextStates.All));
#else
ConstructorInfo ci = typeof(DateTime).GetConstructor(
BindingFlags.Instance | BindingFlags.NonPublic,
binder: null,
new Type[] { typeof(SerializationInfo), typeof(StreamingContext) },
modifiers: null);
baseDateTime = (DateTime)ci.Invoke(new object[] { si, new StreamingContext(StreamingContextStates.All) });
#endif
#pragma warning restore SYSLIB0050 // Type or member is obsolete
Volatile.Write(ref s_baseAmbiguousDstDateTime, baseDateTime); // it's ok if two threads race here
}
return baseDateTime.AddTicks((long)ticks);
}
#if NET
[UnsafeAccessor(UnsafeAccessorKind.Constructor)]
extern static DateTime CallPrivateSerializationConstructor(SerializationInfo si, StreamingContext ct);
#endif
}
internal static bool? IsDataAvailable(this BinaryReader reader, long requiredBytes)
{
Debug.Assert(requiredBytes >= 0);
if (!reader.BaseStream.CanSeek)
{
return null;
}
try
{
// If the values are equal, it's still not enough, as every NRBF payload
// needs to end with EndMessageByte and requiredBytes does not take it into account.
return (reader.BaseStream.Length - reader.BaseStream.Position) > requiredBytes;
}
catch
{
// seekable Stream can still throw when accessing Length and Position
return null;
}
}
}
|