Skip to content

How NRBF Payload Reader should read DateTimes? #102826

Closed
@adamsitnik

Description

@adamsitnik

@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:

private static unsafe DateTime FromBinaryRaw(long dateData)
{
// 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.
const long TicksMask = 0x3FFFFFFFFFFFFFFF;
new DateTime(dateData & TicksMask);
return *(DateTime*)&dateData;
}

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>

Metadata

Metadata

Assignees

Labels

area-System.Formats.Nrbfbinaryformatter-migrationIssues related to the removal of BinaryFormatter and migrations away from itblocking-releasein-prThere is an active PR which will close this issue when it is merged

Type

No type

Projects

No projects

Milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions