using System; using System.Reflection; namespace ProtoBuf { internal enum TimeSpanScale { Days = 0, Hours = 1, Minutes = 2, Seconds = 3, Milliseconds = 4, Ticks = 5, MinMax = 15 } /// /// Provides support for common .NET types that do not have a direct representation /// in protobuf, using the definitions from bcl.proto /// public #if FX11 sealed #else static #endif class BclHelpers { /// /// Creates a new instance of the specified type, bypassing the constructor. /// /// The type to create /// The new instance /// If the platform does not support constructor-skipping public static object GetUninitializedObject(Type type) { #if COREFX object obj = TryGetUninitializedObjectWithFormatterServices(type); if (obj != null) return obj; #endif #if PLAT_BINARYFORMATTER && !(WINRT || PHONE8 || COREFX) return System.Runtime.Serialization.FormatterServices.GetUninitializedObject(type); #else throw new NotSupportedException("Constructor-skipping is not supported on this platform"); #endif } #if COREFX // this is inspired by DCS: https://github.com/dotnet/corefx/blob/c02d33b18398199f6acc17d375dab154e9a1df66/src/System.Private.DataContractSerialization/src/System/Runtime/Serialization/XmlFormatReaderGenerator.cs#L854-L894 static Func getUninitializedObject; static internal object TryGetUninitializedObjectWithFormatterServices(Type type) { if (getUninitializedObject == null) { try { var formatterServiceType = typeof(string).GetTypeInfo().Assembly.GetType("System.Runtime.Serialization.FormatterServices"); MethodInfo method = formatterServiceType?.GetMethod("GetUninitializedObject", BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Static); if (method != null) { getUninitializedObject = (Func)method.CreateDelegate(typeof(Func)); } } catch { /* best efforts only */ } if(getUninitializedObject == null) getUninitializedObject = x => null; } return getUninitializedObject(type); } #endif #if FX11 private BclHelpers() { } // not a static class for C# 1.2 reasons #endif const int FieldTimeSpanValue = 0x01, FieldTimeSpanScale = 0x02, FieldTimeSpanKind = 0x03; internal static readonly DateTime[] EpochOrigin = { new DateTime(1970, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new DateTime(1970, 1, 1, 0, 0, 0, 0, DateTimeKind.Utc), new DateTime(1970, 1, 1, 0, 0, 0, 0, DateTimeKind.Local) }; /// /// Writes a TimeSpan to a protobuf stream /// public static void WriteTimeSpan(TimeSpan timeSpan, ProtoWriter dest) { WriteTimeSpanImpl(timeSpan, dest, DateTimeKind.Unspecified); } private static void WriteTimeSpanImpl(TimeSpan timeSpan, ProtoWriter dest, DateTimeKind kind) { if (dest == null) throw new ArgumentNullException("dest"); long value; switch(dest.WireType) { case WireType.String: case WireType.StartGroup: TimeSpanScale scale; value = timeSpan.Ticks; if (timeSpan == TimeSpan.MaxValue) { value = 1; scale = TimeSpanScale.MinMax; } else if (timeSpan == TimeSpan.MinValue) { value = -1; scale = TimeSpanScale.MinMax; } else if (value % TimeSpan.TicksPerDay == 0) { scale = TimeSpanScale.Days; value /= TimeSpan.TicksPerDay; } else if (value % TimeSpan.TicksPerHour == 0) { scale = TimeSpanScale.Hours; value /= TimeSpan.TicksPerHour; } else if (value % TimeSpan.TicksPerMinute == 0) { scale = TimeSpanScale.Minutes; value /= TimeSpan.TicksPerMinute; } else if (value % TimeSpan.TicksPerSecond == 0) { scale = TimeSpanScale.Seconds; value /= TimeSpan.TicksPerSecond; } else if (value % TimeSpan.TicksPerMillisecond == 0) { scale = TimeSpanScale.Milliseconds; value /= TimeSpan.TicksPerMillisecond; } else { scale = TimeSpanScale.Ticks; } SubItemToken token = ProtoWriter.StartSubItem(null, dest); if(value != 0) { ProtoWriter.WriteFieldHeader(FieldTimeSpanValue, WireType.SignedVariant, dest); ProtoWriter.WriteInt64(value, dest); } if(scale != TimeSpanScale.Days) { ProtoWriter.WriteFieldHeader(FieldTimeSpanScale, WireType.Variant, dest); ProtoWriter.WriteInt32((int)scale, dest); } if(kind != DateTimeKind.Unspecified) { ProtoWriter.WriteFieldHeader(FieldTimeSpanKind, WireType.Variant, dest); ProtoWriter.WriteInt32((int)kind, dest); } ProtoWriter.EndSubItem(token, dest); break; case WireType.Fixed64: ProtoWriter.WriteInt64(timeSpan.Ticks, dest); break; default: throw new ProtoException("Unexpected wire-type: " + dest.WireType.ToString()); } } /// /// Parses a TimeSpan from a protobuf stream /// public static TimeSpan ReadTimeSpan(ProtoReader source) { DateTimeKind kind; long ticks = ReadTimeSpanTicks(source, out kind); if (ticks == long.MinValue) return TimeSpan.MinValue; if (ticks == long.MaxValue) return TimeSpan.MaxValue; return TimeSpan.FromTicks(ticks); } /// /// Parses a DateTime from a protobuf stream /// public static DateTime ReadDateTime(ProtoReader source) { DateTimeKind kind; long ticks = ReadTimeSpanTicks(source, out kind); if (ticks == long.MinValue) return DateTime.MinValue; if (ticks == long.MaxValue) return DateTime.MaxValue; return EpochOrigin[(int)kind].AddTicks(ticks); } /// /// Writes a DateTime to a protobuf stream, excluding the Kind /// public static void WriteDateTime(DateTime value, ProtoWriter dest) { WriteDateTimeImpl(value, dest, false); } /// /// Writes a DateTime to a protobuf stream, including the Kind /// public static void WriteDateTimeWithKind(DateTime value, ProtoWriter dest) { WriteDateTimeImpl(value, dest, true); } private static void WriteDateTimeImpl(DateTime value, ProtoWriter dest, bool includeKind) { if (dest == null) throw new ArgumentNullException("dest"); TimeSpan delta; switch (dest.WireType) { case WireType.StartGroup: case WireType.String: if (value == DateTime.MaxValue) { delta = TimeSpan.MaxValue; includeKind = false; } else if (value == DateTime.MinValue) { delta = TimeSpan.MinValue; includeKind = false; } else { delta = value - EpochOrigin[0]; } break; default: delta = value - EpochOrigin[0]; break; } WriteTimeSpanImpl(delta, dest, includeKind ? value.Kind : DateTimeKind.Unspecified); } private static long ReadTimeSpanTicks(ProtoReader source, out DateTimeKind kind) { kind = DateTimeKind.Unspecified; switch (source.WireType) { case WireType.String: case WireType.StartGroup: SubItemToken token = ProtoReader.StartSubItem(source); int fieldNumber; TimeSpanScale scale = TimeSpanScale.Days; long value = 0; while ((fieldNumber = source.ReadFieldHeader()) > 0) { switch (fieldNumber) { case FieldTimeSpanScale: scale = (TimeSpanScale)source.ReadInt32(); break; case FieldTimeSpanValue: source.Assert(WireType.SignedVariant); value = source.ReadInt64(); break; case FieldTimeSpanKind: kind = (DateTimeKind)source.ReadInt32(); switch(kind) { case DateTimeKind.Unspecified: case DateTimeKind.Utc: case DateTimeKind.Local: break; // fine default: throw new ProtoException("Invalid date/time kind: " + kind.ToString()); } break; default: source.SkipField(); break; } } ProtoReader.EndSubItem(token, source); switch (scale) { case TimeSpanScale.Days: return value * TimeSpan.TicksPerDay; case TimeSpanScale.Hours: return value * TimeSpan.TicksPerHour; case TimeSpanScale.Minutes: return value * TimeSpan.TicksPerMinute; case TimeSpanScale.Seconds: return value * TimeSpan.TicksPerSecond; case TimeSpanScale.Milliseconds: return value * TimeSpan.TicksPerMillisecond; case TimeSpanScale.Ticks: return value; case TimeSpanScale.MinMax: switch (value) { case 1: return long.MaxValue; case -1: return long.MinValue; default: throw new ProtoException("Unknown min/max value: " + value.ToString()); } default: throw new ProtoException("Unknown timescale: " + scale.ToString()); } case WireType.Fixed64: return source.ReadInt64(); default: throw new ProtoException("Unexpected wire-type: " + source.WireType.ToString()); } } const int FieldDecimalLow = 0x01, FieldDecimalHigh = 0x02, FieldDecimalSignScale = 0x03; /// /// Parses a decimal from a protobuf stream /// public static decimal ReadDecimal(ProtoReader reader) { ulong low = 0; uint high = 0; uint signScale = 0; int fieldNumber; SubItemToken token = ProtoReader.StartSubItem(reader); while ((fieldNumber = reader.ReadFieldHeader()) > 0) { switch (fieldNumber) { case FieldDecimalLow: low = reader.ReadUInt64(); break; case FieldDecimalHigh: high = reader.ReadUInt32(); break; case FieldDecimalSignScale: signScale = reader.ReadUInt32(); break; default: reader.SkipField(); break; } } ProtoReader.EndSubItem(token, reader); if (low == 0 && high == 0) return decimal.Zero; int lo = (int)(low & 0xFFFFFFFFL), mid = (int)((low >> 32) & 0xFFFFFFFFL), hi = (int)high; bool isNeg = (signScale & 0x0001) == 0x0001; byte scale = (byte)((signScale & 0x01FE) >> 1); return new decimal(lo, mid, hi, isNeg, scale); } /// /// Writes a decimal to a protobuf stream /// public static void WriteDecimal(decimal value, ProtoWriter writer) { int[] bits = decimal.GetBits(value); ulong a = ((ulong)bits[1]) << 32, b = ((ulong)bits[0]) & 0xFFFFFFFFL; ulong low = a | b; uint high = (uint)bits[2]; uint signScale = (uint)(((bits[3] >> 15) & 0x01FE) | ((bits[3] >> 31) & 0x0001)); SubItemToken token = ProtoWriter.StartSubItem(null, writer); if (low != 0) { ProtoWriter.WriteFieldHeader(FieldDecimalLow, WireType.Variant, writer); ProtoWriter.WriteUInt64(low, writer); } if (high != 0) { ProtoWriter.WriteFieldHeader(FieldDecimalHigh, WireType.Variant, writer); ProtoWriter.WriteUInt32(high, writer); } if (signScale != 0) { ProtoWriter.WriteFieldHeader(FieldDecimalSignScale, WireType.Variant, writer); ProtoWriter.WriteUInt32(signScale, writer); } ProtoWriter.EndSubItem(token, writer); } const int FieldGuidLow = 1, FieldGuidHigh = 2; /// /// Writes a Guid to a protobuf stream /// public static void WriteGuid(Guid value, ProtoWriter dest) { byte[] blob = value.ToByteArray(); SubItemToken token = ProtoWriter.StartSubItem(null, dest); if (value != Guid.Empty) { ProtoWriter.WriteFieldHeader(FieldGuidLow, WireType.Fixed64, dest); ProtoWriter.WriteBytes(blob, 0, 8, dest); ProtoWriter.WriteFieldHeader(FieldGuidHigh, WireType.Fixed64, dest); ProtoWriter.WriteBytes(blob, 8, 8, dest); } ProtoWriter.EndSubItem(token, dest); } /// /// Parses a Guid from a protobuf stream /// public static Guid ReadGuid(ProtoReader source) { ulong low = 0, high = 0; int fieldNumber; SubItemToken token = ProtoReader.StartSubItem(source); while ((fieldNumber = source.ReadFieldHeader()) > 0) { switch (fieldNumber) { case FieldGuidLow: low = source.ReadUInt64(); break; case FieldGuidHigh: high = source.ReadUInt64(); break; default: source.SkipField(); break; } } ProtoReader.EndSubItem(token, source); if(low == 0 && high == 0) return Guid.Empty; uint a = (uint)(low >> 32), b = (uint)low, c = (uint)(high >> 32), d= (uint)high; return new Guid((int)b, (short)a, (short)(a >> 16), (byte)d, (byte)(d >> 8), (byte)(d >> 16), (byte)(d >> 24), (byte)c, (byte)(c >> 8), (byte)(c >> 16), (byte)(c >> 24)); } private const int FieldExistingObjectKey = 1, FieldNewObjectKey = 2, FieldExistingTypeKey = 3, FieldNewTypeKey = 4, FieldTypeName = 8, FieldObject = 10; /// /// Optional behaviours that introduce .NET-specific functionality /// [Flags] public enum NetObjectOptions : byte { /// /// No special behaviour /// None = 0, /// /// Enables full object-tracking/full-graph support. /// AsReference = 1, /// /// Embeds the type information into the stream, allowing usage with types not known in advance. /// DynamicType = 2, /// /// If false, the constructor for the type is bypassed during deserialization, meaning any field initializers /// or other initialization code is skipped. /// UseConstructor = 4, /// /// Should the object index be reserved, rather than creating an object promptly /// LateSet = 8 } /// /// Reads an *implementation specific* bundled .NET object, including (as options) type-metadata, identity/re-use, etc. /// public static object ReadNetObject(object value, ProtoReader source, int key, Type type, NetObjectOptions options) { #if FEAT_IKVM throw new NotSupportedException(); #else SubItemToken token = ProtoReader.StartSubItem(source); int fieldNumber; int newObjectKey = -1, newTypeKey = -1, tmp; while ((fieldNumber = source.ReadFieldHeader()) > 0) { switch (fieldNumber) { case FieldExistingObjectKey: tmp = source.ReadInt32(); value = source.NetCache.GetKeyedObject(tmp); break; case FieldNewObjectKey: newObjectKey = source.ReadInt32(); break; case FieldExistingTypeKey: tmp = source.ReadInt32(); type = (Type)source.NetCache.GetKeyedObject(tmp); key = source.GetTypeKey(ref type); break; case FieldNewTypeKey: newTypeKey = source.ReadInt32(); break; case FieldTypeName: string typeName = source.ReadString(); type = source.DeserializeType(typeName); if(type == null) { throw new ProtoException("Unable to resolve type: " + typeName + " (you can use the TypeModel.DynamicTypeFormatting event to provide a custom mapping)"); } if (type == typeof(string)) { key = -1; } else { key = source.GetTypeKey(ref type); if (key < 0) throw new InvalidOperationException("Dynamic type is not a contract-type: " + type.Name); } break; case FieldObject: bool isString = type == typeof(string); bool wasNull = value == null; bool lateSet = wasNull && (isString || ((options & NetObjectOptions.LateSet) != 0)); if (newObjectKey >= 0 && !lateSet) { if (value == null) { source.TrapNextObject(newObjectKey); } else { source.NetCache.SetKeyedObject(newObjectKey, value); } if (newTypeKey >= 0) source.NetCache.SetKeyedObject(newTypeKey, type); } object oldValue = value; if (isString) { value = source.ReadString(); } else { value = ProtoReader.ReadTypedObject(oldValue, key, source, type); } if (newObjectKey >= 0) { if(wasNull && !lateSet) { // this both ensures (via exception) that it *was* set, and makes sure we don't shout // about changed references oldValue = source.NetCache.GetKeyedObject(newObjectKey); } if (lateSet) { source.NetCache.SetKeyedObject(newObjectKey, value); if (newTypeKey >= 0) source.NetCache.SetKeyedObject(newTypeKey, type); } } if (newObjectKey >= 0 && !lateSet && !ReferenceEquals(oldValue, value)) { throw new ProtoException("A reference-tracked object changed reference during deserialization"); } if (newObjectKey < 0 && newTypeKey >= 0) { // have a new type, but not a new object source.NetCache.SetKeyedObject(newTypeKey, type); } break; default: source.SkipField(); break; } } if(newObjectKey >= 0 && (options & NetObjectOptions.AsReference) == 0) { throw new ProtoException("Object key in input stream, but reference-tracking was not expected"); } ProtoReader.EndSubItem(token, source); return value; #endif } /// /// Writes an *implementation specific* bundled .NET object, including (as options) type-metadata, identity/re-use, etc. /// public static void WriteNetObject(object value, ProtoWriter dest, int key, NetObjectOptions options) { #if FEAT_IKVM throw new NotSupportedException(); #else if (dest == null) throw new ArgumentNullException("dest"); bool dynamicType = (options & NetObjectOptions.DynamicType) != 0, asReference = (options & NetObjectOptions.AsReference) != 0; WireType wireType = dest.WireType; SubItemToken token = ProtoWriter.StartSubItem(null, dest); bool writeObject = true; if (asReference) { bool existing; int objectKey = dest.NetCache.AddObjectKey(value, out existing); ProtoWriter.WriteFieldHeader(existing ? FieldExistingObjectKey : FieldNewObjectKey, WireType.Variant, dest); ProtoWriter.WriteInt32(objectKey, dest); if (existing) { writeObject = false; } } if (writeObject) { if (dynamicType) { bool existing; Type type = value.GetType(); if (!(value is string)) { key = dest.GetTypeKey(ref type); if (key < 0) throw new InvalidOperationException("Dynamic type is not a contract-type: " + type.Name); } int typeKey = dest.NetCache.AddObjectKey(type, out existing); ProtoWriter.WriteFieldHeader(existing ? FieldExistingTypeKey : FieldNewTypeKey, WireType.Variant, dest); ProtoWriter.WriteInt32(typeKey, dest); if (!existing) { ProtoWriter.WriteFieldHeader(FieldTypeName, WireType.String, dest); ProtoWriter.WriteString(dest.SerializeType(type), dest); } } ProtoWriter.WriteFieldHeader(FieldObject, wireType, dest); if (value is string) { ProtoWriter.WriteString((string)value, dest); } else { ProtoWriter.WriteObject(value, key, dest); } } ProtoWriter.EndSubItem(token, dest); #endif } } }