Closed
Description
@bartonjs has reminded me of an issue @GrabYourPitchforks has reported when reviewing my initial version of NRBF Payload Reader adamsitnik/SafePayloadReader#2
I tried all mentioned solutions and only the current solution authored by @JeremyKuhne returns exact same results as BF.
FWIW I took @GrabYourPitchforks solution from https://github.com/GrabYourPitchforks/type-parsing/blob/217c548e6dbf9c2eaeb419ef74b06816ba31362c/Pitchfork.BFCompat/DateTimeHelper.cs#L11C1-L48C10
BF implementation can be found here:
Small repro:
using System;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Runtime.Serialization;
using System.Runtime.Serialization.Formatters.Binary;
using System.Threading;
#pragma warning disable SYSLIB0011 // Type or member is obsolete
namespace ParsingDateTimes
{
internal class Program
{
static void Main(string[] args)
{
DateTime[] input = new DateTime[]
{
new DateTime(1990, 11, 24, 0, 0, 0, DateTimeKind.Local),
new DateTime(1990, 11, 25, 0, 0, 0, DateTimeKind.Utc),
new DateTime(1990, 11, 26, 0, 0, 0, DateTimeKind.Unspecified)
};
using MemoryStream stream = new();
BinaryFormatter binaryFormatter = new();
binaryFormatter.Serialize(stream, input);
stream.Position = 0;
DateTime[] output = (DateTime[])binaryFormatter.Deserialize(stream);
stream.Position = 0;
byte[] serializedDateTimes = stream.ToArray().Skip(
sizeof(byte) + // RecordType.SerializedStreamHeader
4 * sizeof(int) + // SerializedStreamHeaderRecord
sizeof(byte) + // RecordType.ArraySinglePrimitive
2 * sizeof(int) // ArrayInfo
+ sizeof(byte) // PrimitiveType.DateTime
).Take(input.Length * sizeof(long)).ToArray();
Span<long> longs = MemoryMarshal.Cast<byte, long>(serializedDateTimes);
for (int i = 0; i < output.Length; i++)
{
if (!AreEqual(input[i], output[i]))
{
Print("BF", input[i], output[i]);
}
if (!AreEqual(input[i], JeremyK(longs[i])))
{
Print("JK", input[i], JeremyK(longs[i]));
}
if (!AreEqual(input[i], JeremyB(longs[i])))
{
Print("JB", input[i], JeremyB(longs[i]));
}
if (!AreEqual(input[i], Levi((ulong)longs[i])))
{
Print("Levi", input[i], Levi((ulong)longs[i]));
}
}
// DateTime's Equals ignores Kind
static bool AreEqual(DateTime left, DateTime right)
=> left.Kind == right.Kind && left.Ticks == right.Ticks;
static void Print(string name, DateTime left, DateTime right)
=> Console.WriteLine($"{name}: serialized '{left.Kind}, {left.Ticks}', got: '{right.Kind}, {right.Ticks}'");
}
private static DateTime JeremyK(long data)
{
// Copied from System.Runtime.Serialization.Formatters.Binary.BinaryParser
// Use DateTime's public constructor to validate the input, but we
// can't return that result as it strips off the kind. To address
// that, store the value directly into a DateTime via an unsafe cast.
// See BinaryFormatterWriter.WriteDateTime for details.
try
{
const long TicksMask = 0x3FFFFFFFFFFFFFFF;
_ = new DateTime(data & TicksMask);
}
catch (ArgumentException ex)
{
// Bad data
throw new SerializationException(ex.Message, ex);
}
return Unsafe.As<long, DateTime>(ref data);
}
private static DateTime JeremyB(long data) => DateTime.FromBinary(data);
private static object? _baseAmbiguousDstDateTime;
private static DateTime Levi(ulong dateData)
{
ulong ticks = dateData & 0x3FFFFFFF_FFFFFFFFUL;
DateTimeKind kind = (DateTimeKind)(ticks >> 30);
return ((uint)kind <= (uint)DateTimeKind.Local) ? new DateTime((long)ticks, kind) : CreateFromAmbiguousDst(ticks);
[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 (_baseAmbiguousDstDateTime is not DateTime baseDateTime)
{
ConstructorInfo? ci = typeof(DateTime).GetConstructor(BindingFlags.Instance | BindingFlags.NonPublic, new Type[] { typeof(SerializationInfo), typeof(StreamingContext) });
if (ci is null)
{
// TODO: better error message
throw new PlatformNotSupportedException();
}
SerializationInfo si = new SerializationInfo(typeof(DateTime), new FormatterConverter());
si.AddValue("ticks", 0L); // legacy value (serialized as long) - specify both just to be safe
si.AddValue("dateData", 0xC0000000_00000000UL); // new value (serialized as ulong)
baseDateTime = (DateTime)ci.Invoke(new object[] { si, new StreamingContext(StreamingContextStates.All) });
Volatile.Write(ref _baseAmbiguousDstDateTime, baseDateTime); // it's ok if two threads race here
}
return baseDateTime.AddTicks((long)ticks);
}
}
}
}
#pragma warning restore SYSLIB0011 // Type or member is obsolete
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net8.0</TargetFramework>
<EnableUnsafeBinaryFormatterSerialization>true</EnableUnsafeBinaryFormatterSerialization>
<Nullable>enable</Nullable>
</PropertyGroup>
</Project>