// Copyright Epic Games, Inc. All Rights Reserved. using System; using System.Buffers; using System.Buffers.Binary; using System.Collections; using System.Collections.Generic; using System.ComponentModel; using System.Diagnostics; using System.Globalization; using System.Linq; using System.Text; using System.Text.Json; using System.Text.Json.Serialization; using System.Threading; using EpicGames.Core; #pragma warning disable CA1721 // Property names should not match get methods #pragma warning disable CA1028 // Enum Storage should be Int32 namespace EpicGames.Serialization { /// /// Field types and flags for FCbField[View]. /// /// DO NOT CHANGE THE VALUE OF ANY MEMBERS OF THIS ENUM! /// BACKWARD COMPATIBILITY REQUIRES THAT THESE VALUES BE FIXED! /// SERIALIZATION USES HARD-CODED CONSTANTS BASED ON THESE VALUES! /// [Flags] public enum CbFieldType : byte { /// /// A field type that does not occur in a valid object. /// None = 0x00, /// /// Null. Payload is empty. /// Null = 0x01, /// /// Object is an array of fields with unique non-empty names. /// /// Payload is a VarUInt byte count for the encoded fields followed by the fields. /// Object = 0x02, /// /// UniformObject is an array of fields with the same field types and unique non-empty names. /// /// Payload is a VarUInt byte count for the encoded fields followed by the fields. /// UniformObject = 0x03, /// /// Array is an array of fields with no name that may be of different types. /// /// Payload is a VarUInt byte count, followed by a VarUInt item count, followed by the fields. /// Array = 0x04, /// /// UniformArray is an array of fields with no name and with the same field type. /// /// Payload is a VarUInt byte count, followed by a VarUInt item count, followed by field type, /// followed by the fields without their field type. /// UniformArray = 0x05, /// /// Binary. Payload is a VarUInt byte count followed by the data. /// /// Binary = 0x06, /// /// String in UTF-8. Payload is a VarUInt byte count then an unterminated UTF-8 string. /// String = 0x07, /// /// Non-negative integer with the range of a 64-bit unsigned integer. /// /// Payload is the value encoded as a VarUInt. /// IntegerPositive = 0x08, /// /// Negative integer with the range of a 64-bit signed integer. /// /// Payload is the ones' complement of the value encoded as a VarUInt. /// IntegerNegative = 0x09, /// /// Single precision float. Payload is one big endian IEEE 754 binary32 float. /// /// Float32 = 0x0a, /// /// Double precision float. Payload is one big endian IEEE 754 binary64 float. /// Float64 = 0x0b, /// /// Boolean false value. Payload is empty. /// BoolFalse = 0x0c, /// /// Boolean true value. Payload is empty. /// BoolTrue = 0x0d, /// /// CompactBinaryAttachment is a reference to a compact binary attachment stored externally. /// /// Payload is a 160-bit hash digest of the referenced compact binary data. /// ObjectAttachment = 0x0e, /// /// BinaryAttachment is a reference to a binary attachment stored externally. /// /// Payload is a 160-bit hash digest of the referenced binary data. /// BinaryAttachment = 0x0f, /// /// Hash. Payload is a 160-bit hash digest. /// Hash = 0x10, /// /// UUID/GUID. Payload is a 128-bit UUID as defined by RFC 4122. /// Uuid = 0x11, /// /// Date and time between 0001-01-01 00:00:00.0000000 and 9999-12-31 23:59:59.9999999. /// /// Payload is a big endian int64 count of 100ns ticks since 0001-01-01 00:00:00.0000000. /// DateTime = 0x12, /// /// Difference between two date/time values. /// /// Payload is a big endian int64 count of 100ns ticks in the span, and may be negative. /// TimeSpan = 0x13, /// /// ObjectId is an opaque object identifier. See FCbObjectId. /// /// Payload is a 12-byte object identifier. /// ObjectId = 0x14, /// /// CustomById identifies the sub-type of its payload by an integer identifier. /// /// Payload is a VarUInt byte count of the sub-type identifier and the sub-type payload, followed /// by a VarUInt of the sub-type identifier then the payload of the sub-type. /// CustomById = 0x1e, /// /// CustomByType identifies the sub-type of its payload by a string identifier. /// /// Payload is a VarUInt byte count of the sub-type identifier and the sub-type payload, followed /// by a VarUInt byte count of the unterminated sub-type identifier, then the sub-type identifier /// without termination, then the payload of the sub-type. /// CustomByName = 0x1f, /// /// A transient flag which indicates that the object or array containing this field has stored /// the field type before the payload and name. Non-uniform objects and fields will set this. /// /// Note: Since the flag must never be serialized, this bit may be repurposed in the future. /// HasFieldType = 0x40, /// /// A persisted flag which indicates that the field has a name stored before the payload. /// HasFieldName = 0x80, } /// /// A binary attachment, referenced by /// /// Hash of the referenced object [DebuggerDisplay("{Hash}")] [JsonConverter(typeof(CbBinaryAttachmentJsonConverter))] [TypeConverter(typeof(CbBinaryAttachmentTypeConverter))] public readonly struct CbBinaryAttachment(IoHash hash) : IEquatable { /// /// Attachment with a hash of zero /// public static CbBinaryAttachment Zero { get; } = new CbBinaryAttachment(IoHash.Zero); /// /// Hash of the referenced object /// public IoHash Hash { get; } = hash; /// public override string ToString() => Hash.ToString(); /// public override bool Equals(object? obj) => obj is CbBinaryAttachment other && Equals(other); /// public bool Equals(CbBinaryAttachment other) => other.Hash == Hash; /// public override int GetHashCode() => Hash.GetHashCode(); /// public static bool operator ==(CbBinaryAttachment lhs, CbBinaryAttachment rhs) => lhs.Hash == rhs.Hash; /// public static bool operator !=(CbBinaryAttachment lhs, CbBinaryAttachment rhs) => lhs.Hash != rhs.Hash; /// /// Convert a hash to a binary attachment /// /// The attachment to convert public static implicit operator CbBinaryAttachment(IoHash hash) => new CbBinaryAttachment(hash); /// /// Use a binary attachment as a hash /// /// The attachment to convert public static implicit operator IoHash(CbBinaryAttachment attachment) => attachment.Hash; } /// /// Type converter for IoHash to and from JSON /// sealed class CbBinaryAttachmentJsonConverter : JsonConverter { /// public override CbBinaryAttachment Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) => IoHash.Parse(reader.ValueSpan); /// public override void Write(Utf8JsonWriter writer, CbBinaryAttachment value, JsonSerializerOptions options) => writer.WriteStringValue(value.Hash.ToUtf8String().Span); } /// /// Type converter from strings to IoHash objects /// sealed class CbBinaryAttachmentTypeConverter : TypeConverter { /// public override bool CanConvertFrom(ITypeDescriptorContext? context, Type sourceType) => sourceType == typeof(string); /// public override object ConvertFrom(ITypeDescriptorContext? context, CultureInfo? culture, object value) => new CbBinaryAttachment(IoHash.Parse((string)value)); } /// /// An object attachment, referenced by /// /// Hash of the referenced object [DebuggerDisplay("{Hash}")] [JsonConverter(typeof(CbObjectAttachmentJsonConverter))] [TypeConverter(typeof(CbObjectAttachmentTypeConverter))] public readonly struct CbObjectAttachment(IoHash hash) : IEquatable { /// /// Attachment with a hash of zero /// public static CbObjectAttachment Zero { get; } = new CbObjectAttachment(IoHash.Zero); /// /// Hash of the referenced object /// public IoHash Hash { get; } = hash; /// public override string ToString() => Hash.ToString(); /// public override bool Equals(object? obj) => obj is CbObjectAttachment other && Equals(other); /// public bool Equals(CbObjectAttachment other) => other.Hash == Hash; /// public override int GetHashCode() => Hash.GetHashCode(); /// public static bool operator ==(CbObjectAttachment lhs, CbObjectAttachment rhs) => lhs.Hash == rhs.Hash; /// public static bool operator !=(CbObjectAttachment lhs, CbObjectAttachment rhs) => lhs.Hash != rhs.Hash; /// /// Use an object attachment as a hash /// /// The attachment to convert public static implicit operator CbObjectAttachment(IoHash hash) => new CbObjectAttachment(hash); /// /// Use an object attachment as a hash /// /// The attachment to convert public static implicit operator IoHash(CbObjectAttachment attachment) => attachment.Hash; } /// /// Type converter for IoHash to and from JSON /// sealed class CbObjectAttachmentJsonConverter : JsonConverter { /// public override CbObjectAttachment Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) => IoHash.Parse(reader.ValueSpan); /// public override void Write(Utf8JsonWriter writer, CbObjectAttachment value, JsonSerializerOptions options) => writer.WriteStringValue(value.Hash.ToUtf8String().Span); } /// /// Type converter from strings to IoHash objects /// sealed class CbObjectAttachmentTypeConverter : TypeConverter { /// public override bool CanConvertFrom(ITypeDescriptorContext? context, Type sourceType) => sourceType == typeof(string); /// public override object ConvertFrom(ITypeDescriptorContext? context, CultureInfo? culture, object value) => new CbObjectAttachment(IoHash.Parse((string)value)); } /// /// Methods that operate on . /// public static class CbFieldUtils { private const CbFieldType SerializedTypeMask = (CbFieldType)0b_1001_1111; private const CbFieldType TypeMask = (CbFieldType)0b_0001_1111; private const CbFieldType ObjectMask = (CbFieldType)0b_0001_1110; private const CbFieldType ObjectBase = (CbFieldType)0b_0000_0010; private const CbFieldType ArrayMask = (CbFieldType)0b_0001_1110; private const CbFieldType ArrayBase = (CbFieldType)0b_0000_0100; private const CbFieldType IntegerMask = (CbFieldType)0b_0011_1110; private const CbFieldType IntegerBase = (CbFieldType)0b_0000_1000; private const CbFieldType FloatMask = (CbFieldType)0b_0001_1100; private const CbFieldType FloatBase = (CbFieldType)0b_0000_1000; private const CbFieldType BoolMask = (CbFieldType)0b_0001_1110; private const CbFieldType BoolBase = (CbFieldType)0b_0000_1100; private const CbFieldType AttachmentMask = (CbFieldType)0b_0001_1110; private const CbFieldType AttachmentBase = (CbFieldType)0b_0000_1110; /// /// Removes flags from the given type /// /// Type to check /// Type without flag fields public static CbFieldType GetType(CbFieldType type) { return type & TypeMask; } /// /// Gets the serialized type /// /// Type to check /// Type without flag fields public static CbFieldType GetSerializedType(CbFieldType type) { return type & SerializedTypeMask; } /// /// Tests if the given field has a type /// /// Type to check /// True if the field has a type public static bool HasFieldType(CbFieldType type) { return (type & CbFieldType.HasFieldType) != 0; } /// /// Tests if the given field has a name /// /// Type to check /// True if the field has a name public static bool HasFieldName(CbFieldType type) { return (type & CbFieldType.HasFieldName) != 0; } /// /// Tests if the given field type is none /// /// Type to check /// True if the field is none public static bool IsNone(CbFieldType type) { return GetType(type) == CbFieldType.None; } /// /// Tests if the given field type is a null value /// /// Type to check /// True if the field is a null public static bool IsNull(CbFieldType type) { return GetType(type) == CbFieldType.Null; } /// /// Tests if the given field type is an object /// /// Type to check /// True if the field is an object type public static bool IsObject(CbFieldType type) { return (type & ObjectMask) == ObjectBase; } /// /// Tests if the given field type is an array /// /// Type to check /// True if the field is an array type public static bool IsArray(CbFieldType type) { return (type & ArrayMask) == ArrayBase; } /// /// Tests if the given field type is binary /// /// Type to check /// True if the field is binary public static bool IsBinary(CbFieldType type) { return GetType(type) == CbFieldType.Binary; } /// /// Tests if the given field type is a string /// /// Type to check /// True if the field is an array type public static bool IsString(CbFieldType type) { return GetType(type) == CbFieldType.String; } /// /// Tests if the given field type is an integer /// /// Type to check /// True if the field is an integer type public static bool IsInteger(CbFieldType type) { return (type & IntegerMask) == IntegerBase; } /// /// Tests if the given field type is a float (or integer, due to implicit conversion) /// /// Type to check /// True if the field is a float type public static bool IsFloat(CbFieldType type) { return (type & FloatMask) == FloatBase; } /// /// Tests if the given field type is a boolean /// /// Type to check /// True if the field is an bool type public static bool IsBool(CbFieldType type) { return (type & BoolMask) == BoolBase; } /// /// Tests if the given field type is a compact binary attachment /// /// Type to check /// True if the field is a compact binary attachment public static bool IsObjectAttachment(CbFieldType type) { return GetType(type) == CbFieldType.ObjectAttachment; } /// /// Tests if the given field type is a binary attachment /// /// Type to check /// True if the field is a binary attachment public static bool IsBinaryAttachment(CbFieldType type) { return GetType(type) == CbFieldType.BinaryAttachment; } /// /// Tests if the given field type is an attachment /// /// Type to check /// True if the field is an attachment type public static bool IsAttachment(CbFieldType type) { return (type & AttachmentMask) == AttachmentBase; } /// /// Tests if the given field type is a hash /// /// Type to check /// True if the field is a hash public static bool IsHash(CbFieldType type) { return GetType(type) == CbFieldType.Hash || IsAttachment(type); } /// /// Tests if the given field type is a UUID /// /// Type to check /// True if the field is a UUID public static bool IsUuid(CbFieldType type) { return GetType(type) == CbFieldType.Uuid; } /// /// Tests if the given field type is a date/time /// /// Type to check /// True if the field is a date/time public static bool IsDateTime(CbFieldType type) { return GetType(type) == CbFieldType.DateTime; } /// /// Tests if the given field type is a timespan /// /// Type to check /// True if the field is a timespan public static bool IsTimeSpan(CbFieldType type) { return GetType(type) == CbFieldType.TimeSpan; } /// /// Tests if the given field type is a object id /// /// Type to check /// True if the field is a object id public static bool IsObjectId(CbFieldType type) { return GetType(type) == CbFieldType.ObjectId; } /// /// Tests if the given field type has fields /// /// Type to check /// True if the field has fields public static bool HasFields(CbFieldType type) { CbFieldType noFlags = GetType(type); return noFlags >= CbFieldType.Object && noFlags <= CbFieldType.UniformArray; } /// /// Tests if the given field type has uniform fields (array/object) /// /// Type to check /// True if the field has uniform fields public static bool HasUniformFields(CbFieldType type) { CbFieldType localType = GetType(type); return localType == CbFieldType.UniformObject || localType == CbFieldType.UniformArray; } /// /// Tests if the type is or may contain fields of any attachment type. /// public static bool MayContainAttachments(CbFieldType type) { return IsObject(type) | IsArray(type) | IsAttachment(type); } } /// /// Errors that can occur when accessing a field. */ /// public enum CbFieldError : byte { /// /// The field is not in an error state. /// None, /// /// The value type does not match the requested type. /// TypeError, /// /// The value is out of range for the requested type. /// RangeError, } /// /// Simplified view of in the debugger, for fields with a name /// class CbFieldWithNameDebugView { public string? Name { get; set; } public object? Value { get; set; } } /// /// Simplified view of for the debugger /// class CbFieldDebugView { public CbFieldDebugView(CbField field) => Value = field.HasName() ? new CbFieldWithNameDebugView { Name = field.Name.ToString(), Value = field.Value } : field.Value; [DebuggerBrowsable(DebuggerBrowsableState.RootHidden)] public object? Value { get; } } /// /// An atom of data in the compact binary format. /// /// Accessing the value of a field is always a safe operation, even if accessed as the wrong type. /// An invalid access will return a default value for the requested type, and set an error code on /// the field that can be checked with GetLastError and HasLastError. A valid access will clear an /// error from a previous invalid access. /// /// A field is encoded in one or more bytes, depending on its type and the type of object or array /// that contains it. A field of an object or array which is non-uniform encodes its field type in /// the first byte, and includes the HasFieldName flag for a field in an object. The field name is /// encoded in a variable-length unsigned integer of its size in bytes, for named fields, followed /// by that many bytes of the UTF-8 encoding of the name with no null terminator.The remainder of /// the field is the payload and is described in the field type enum. Every field must be uniquely /// addressable when encoded, which means a zero-byte field is not permitted, and only arises in a /// uniform array of fields with no payload, where the answer is to encode as a non-uniform array. /// [DebuggerDisplay("{DebugValue,nq}")] [DebuggerTypeProxy(typeof(CbFieldDebugView))] [JsonConverter(typeof(CbFieldJsonConverter))] public class CbField : IEquatable, IEnumerable { /// /// Type returned for none values /// [DebuggerDisplay("")] class NoneValueType { } /// /// Special value returned for "none" fields. /// static NoneValueType None { get; } = new NoneValueType(); /// /// Formatter for the debug string /// object? DebugValue => HasName() ? $"{Name} = {Value}" : Value; /// /// Default empty field /// public static CbField Empty { get; } = new CbField(); /// /// The field type, with the transient HasFieldType flag if the field contains its type /// public CbFieldType TypeWithFlags { get; } /// /// Data for this field /// public ReadOnlyMemory Memory { get; } /// /// Offset of the name with the memory /// internal int _nameLen; /// /// Offset of the payload within the memory /// internal int _payloadOffset; /// /// Error for parsing the current field type /// public CbFieldError Error { get; private set; } /// /// Default constructor /// public CbField() : this(ReadOnlyMemory.Empty, CbFieldType.None) { } /// /// Copy constructor /// /// public CbField(CbField other) { TypeWithFlags = other.TypeWithFlags; Memory = other.Memory; _nameLen = other._nameLen; _payloadOffset = other._payloadOffset; Error = other.Error; } /// /// Construct a field from a pointer to its data and an optional externally-provided type. /// /// Data Pointer to the start of the field data. /// Type HasFieldType means that Data contains the type. Otherwise, use the given type. public CbField(ReadOnlyMemory data, CbFieldType type = CbFieldType.HasFieldType) { int offset = 0; if (CbFieldUtils.HasFieldType(type)) { type = (CbFieldType)data.Span[offset] | CbFieldType.HasFieldType; offset++; } if (CbFieldUtils.HasFieldName(type)) { _nameLen = (int)VarInt.ReadUnsigned(data.Slice(offset).Span, out int nameLenByteCount); offset += nameLenByteCount + _nameLen; } Memory = data; TypeWithFlags = type; _payloadOffset = offset; Memory = Memory.Slice(0, (int)Math.Min((ulong)Memory.Length, (ulong)_payloadOffset + GetPayloadSize())); } /// /// Returns the name of the field if it has a name, otherwise an empty view. /// public Utf8String Name => new Utf8String(Memory.Slice(_payloadOffset - _nameLen, _nameLen)); /// /// Gets the value of this field /// public object? Value { get { CbFieldType fieldType = CbFieldUtils.GetType(TypeWithFlags); switch (fieldType) { case CbFieldType.None: return None; case CbFieldType.Null: return null; case CbFieldType.Object: case CbFieldType.UniformObject: return AsObject(); case CbFieldType.Array: case CbFieldType.UniformArray: return AsArray(); case CbFieldType.Binary: return AsBinary(); case CbFieldType.String: return AsString(); case CbFieldType.IntegerPositive: return AsUInt64(); case CbFieldType.IntegerNegative: return AsInt64(); case CbFieldType.Float32: return AsFloat(); case CbFieldType.Float64: return AsDouble(); case CbFieldType.BoolFalse: return false; case CbFieldType.BoolTrue: return true; case CbFieldType.ObjectAttachment: return AsObjectAttachment(); case CbFieldType.BinaryAttachment: return AsBinaryAttachment(); case CbFieldType.Hash: return AsHash(); case CbFieldType.Uuid: return AsUuid(); case CbFieldType.DateTime: return AsDateTime(); case CbFieldType.TimeSpan: return AsTimeSpan(); case CbFieldType.ObjectId: return AsObjectId(); default: throw new NotImplementedException($"Unknown field type ({fieldType})"); } } } /// public Utf8String GetName() => Name; /// /// Access the field as an object. Defaults to an empty object on error. /// /// public CbObject AsObject() { if (CbFieldUtils.IsObject(TypeWithFlags)) { Error = CbFieldError.None; return CbObject.FromFieldNoCheck(this); } else { Error = CbFieldError.TypeError; return CbObject.Empty; } } /// /// Access the field as an array. Defaults to an empty array on error. /// /// public CbArray AsArray() { if (CbFieldUtils.IsArray(TypeWithFlags)) { Error = CbFieldError.None; return CbArray.FromFieldNoCheck(this); } else { Error = CbFieldError.TypeError; return CbArray.Empty; } } /// /// Access the field as binary data. /// /// public ReadOnlyMemory AsBinary() { return AsBinary(ReadOnlyMemory.Empty); } /// /// Access the field as binary data. /// /// public ReadOnlyMemory AsBinary(ReadOnlyMemory defaultValue) { if (CbFieldUtils.IsBinary(TypeWithFlags)) { Error = CbFieldError.None; ulong length = VarInt.ReadUnsigned(Payload.Span, out int bytesRead); return Payload.Slice(bytesRead, (int)length); } else { Error = CbFieldError.TypeError; return defaultValue; } } /// /// Access the field as binary data. /// /// public byte[] AsBinaryArray() { return AsBinaryArray([]); } /// /// Access the field as binary data. /// /// public byte[] AsBinaryArray(byte[] defaultValue) { return AsBinary(defaultValue).ToArray(); } /// /// Access the field as a UTF-8 string. /// /// public string AsString() => AsUtf8String().ToString(); /// /// Access the field as a UTF-8 string. /// /// public string AsString(string defaultValue) => AsUtf8String(new Utf8String(defaultValue)).ToString(); /// /// Access the field as a UTF-8 string. /// /// public Utf8String AsUtf8String() { return AsUtf8String(default); } /// /// Access the field as a UTF-8 string. Returns the provided default on error. /// /// Default value to return /// public Utf8String AsUtf8String(Utf8String defaultValue) { if (CbFieldUtils.IsString(TypeWithFlags)) { ulong valueSize = VarInt.ReadUnsigned(Payload.Span, out int valueSizeByteCount); if (valueSize >= (1UL << 31)) { Error = CbFieldError.RangeError; return defaultValue; } else { Error = CbFieldError.None; return new Utf8String(Payload.Slice(valueSizeByteCount, (int)valueSize)); } } else { Error = CbFieldError.TypeError; return defaultValue; } } /// /// Access the field as an int8. Returns the provided default on error. /// public sbyte AsInt8(sbyte defaultValue = 0) { return (sbyte)AsInteger((ulong)defaultValue, 7, true); } /// /// Access the field as an int16. Returns the provided default on error. /// public short AsInt16(short defaultValue = 0) { return (short)AsInteger((ulong)defaultValue, 15, true); } /// /// Access the field as an int32. Returns the provided default on error. /// public int AsInt32() { return AsInt32(0); } /// /// Access the field as an int32. Returns the provided default on error. /// public int AsInt32(int defaultValue) { return (int)AsInteger((ulong)defaultValue, 31, true); } /// /// Access the field as an int64. Returns the provided default on error. /// public long AsInt64() { return AsInt64(0); } /// /// Access the field as an int64. Returns the provided default on error. /// public long AsInt64(long defaultValue) { return (long)AsInteger((ulong)defaultValue, 63, true); } /// /// Access the field as an int8. Returns the provided default on error. /// public byte AsUInt8(byte defaultValue = 0) { return (byte)AsInteger(defaultValue, 8, false); } /// /// Access the field as an int16. Returns the provided default on error. /// public ushort AsUInt16(ushort defaultValue = 0) { return (ushort)AsInteger(defaultValue, 16, false); } /// /// Access the field as an int32. Returns the provided default on error. /// public uint AsUInt32() { return AsUInt32(0); } /// /// Access the field as an int32. Returns the provided default on error. /// public uint AsUInt32(uint defaultValue) { return (uint)AsInteger(defaultValue, 32, false); } /// /// Access the field as an int64. Returns the provided default on error. /// public ulong AsUInt64() { return AsUInt64(0); } /// /// Access the field as an int64. Returns the provided default on error. /// public ulong AsUInt64(ulong defaultValue) { return (ulong)AsInteger(defaultValue, 64, false); } /// /// Access the field as an integer, checking that it's in the correct range /// /// /// /// /// private ulong AsInteger(ulong defaultValue, int magnitudeBits, bool isSigned) { if (CbFieldUtils.IsInteger(TypeWithFlags)) { // A shift of a 64-bit value by 64 is undefined so shift by one less because magnitude is never zero. ulong outOfRangeMask = ~(ulong)1 << (magnitudeBits - 1); ulong isNegative = (ulong)(byte)(TypeWithFlags) & 1; ulong magnitude = VarInt.ReadUnsigned(Payload.Span, out _); ulong value = magnitude ^ (ulong)-(long)(isNegative); if ((magnitude & outOfRangeMask) == 0 && (isNegative == 0 || isSigned)) { Error = CbFieldError.None; return value; } else { Error = CbFieldError.RangeError; return defaultValue; } } else { Error = CbFieldError.TypeError; return defaultValue; } } /// /// Access the field as a float. Returns the provided default on error. /// /// Default value /// Value of the field public float AsFloat(float defaultValue = 0.0f) { switch (GetType()) { case CbFieldType.IntegerPositive: case CbFieldType.IntegerNegative: { ulong isNegative = (ulong)TypeWithFlags & 1; ulong outOfRangeMask = ~((1UL << /*FLT_MANT_DIG*/ 24) - 1); ulong magnitude = VarInt.ReadUnsigned(Payload.Span, out _) + isNegative; bool isInRange = (magnitude & outOfRangeMask) == 0; Error = isInRange ? CbFieldError.None : CbFieldError.RangeError; return isInRange ? (float)((isNegative != 0) ? (float)-(long)magnitude : (float)magnitude) : defaultValue; } case CbFieldType.Float32: Error = CbFieldError.None; return BitConverter.Int32BitsToSingle(BinaryPrimitives.ReadInt32BigEndian(Payload.Span)); case CbFieldType.Float64: Error = CbFieldError.RangeError; return defaultValue; default: Error = CbFieldError.TypeError; return defaultValue; } } /// /// Access the field as a double. /// /// Value of the field public double AsDouble() => AsDouble(0.0); /// /// Access the field as a double. Returns the provided default on error. /// /// Default value /// Value of the field public double AsDouble(double defaultValue) { switch (GetType()) { case CbFieldType.IntegerPositive: case CbFieldType.IntegerNegative: { ulong isNegative = (ulong)TypeWithFlags & 1; ulong outOfRangeMask = ~((1UL << /*DBL_MANT_DIG*/ 53) - 1); ulong magnitude = VarInt.ReadUnsigned(Payload.Span, out _) + isNegative; bool isInRange = (magnitude & outOfRangeMask) == 0; Error = isInRange ? CbFieldError.None : CbFieldError.RangeError; return isInRange ? (double)((isNegative != 0) ? (double)-(long)magnitude : (double)magnitude) : defaultValue; } case CbFieldType.Float32: { Error = CbFieldError.None; return BitConverter.Int32BitsToSingle(BinaryPrimitives.ReadInt32BigEndian(Payload.Span)); } case CbFieldType.Float64: { Error = CbFieldError.None; return BitConverter.Int64BitsToDouble(BinaryPrimitives.ReadInt64BigEndian(Payload.Span)); } default: Error = CbFieldError.TypeError; return defaultValue; } } /// /// Access the field as a bool. Returns the provided default on error. /// /// Value of the field public bool AsBool() => AsBool(false); /// /// Access the field as a bool. Returns the provided default on error. /// /// Default value /// Value of the field public bool AsBool(bool defaultValue) { switch (GetType()) { case CbFieldType.BoolTrue: Error = CbFieldError.None; return true; case CbFieldType.BoolFalse: Error = CbFieldError.None; return false; default: Error = CbFieldError.TypeError; return defaultValue; } } /// /// Access the field as a hash referencing an object attachment. Returns the provided default on error. /// /// Value of the field public CbObjectAttachment AsObjectAttachment() => AsObjectAttachment(CbObjectAttachment.Zero); /// /// Access the field as a hash referencing an object attachment. Returns the provided default on error. /// /// Default value /// Value of the field public CbObjectAttachment AsObjectAttachment(CbObjectAttachment defaultValue) { if (CbFieldUtils.IsObjectAttachment(TypeWithFlags)) { Error = CbFieldError.None; return new IoHash(Payload.Span); } else { Error = CbFieldError.TypeError; return defaultValue; } } /// /// Access the field as a hash referencing a binary attachment. Returns the provided default on error. /// /// Value of the field public CbBinaryAttachment AsBinaryAttachment() => AsBinaryAttachment(CbBinaryAttachment.Zero); /// /// Access the field as a hash referencing a binary attachment. Returns the provided default on error. /// /// Default value /// Value of the field public CbBinaryAttachment AsBinaryAttachment(CbBinaryAttachment defaultValue) { if (CbFieldUtils.IsBinaryAttachment(TypeWithFlags)) { Error = CbFieldError.None; return new IoHash(Payload.Span); } else { Error = CbFieldError.TypeError; return defaultValue; } } /// /// Access the field as a hash referencing an attachment. Returns the provided default on error. /// /// Value of the field public IoHash AsAttachment() => AsAttachment(IoHash.Zero); /// /// Access the field as a hash referencing an attachment. Returns the provided default on error. /// /// Default value /// Value of the field public IoHash AsAttachment(IoHash defaultValue) { if (CbFieldUtils.IsAttachment(TypeWithFlags)) { Error = CbFieldError.None; return new IoHash(Payload.Span); } else { Error = CbFieldError.TypeError; return defaultValue; } } /// /// Access the field as a hash referencing an attachment. Returns the provided default on error. /// /// Value of the field public IoHash AsHash() => AsHash(IoHash.Zero); /// /// Access the field as a hash referencing an attachment. Returns the provided default on error. /// /// Default value /// Value of the field public IoHash AsHash(IoHash defaultValue) { if (CbFieldUtils.IsHash(TypeWithFlags)) { Error = CbFieldError.None; return new IoHash(Payload.Span); } else { Error = CbFieldError.TypeError; return defaultValue; } } /// /// Access the field as a UUID. Returns a nil UUID on error. /// /// Default value /// Value of the field public Guid AsUuid(Guid defaultValue = default) { if (CbFieldUtils.IsUuid(TypeWithFlags)) { Error = CbFieldError.None; ReadOnlySpan span = Payload.Span; uint a = BinaryPrimitives.ReadUInt32BigEndian(span); ushort b = BinaryPrimitives.ReadUInt16BigEndian(span.Slice(4)); ushort c = BinaryPrimitives.ReadUInt16BigEndian(span.Slice(6)); return new Guid(a, b, c, span[8], span[9], span[10], span[11], span[12], span[13], span[14], span[15]); } else { Error = CbFieldError.TypeError; return defaultValue; } } /// /// Reads a date time as number of ticks from the stream /// /// /// public long AsDateTimeTicks(long defaultValue = 0) { if (CbFieldUtils.IsDateTime(TypeWithFlags)) { Error = CbFieldError.None; return BinaryPrimitives.ReadInt64BigEndian(Payload.Span); } else { Error = CbFieldError.TypeError; return defaultValue; } } /// /// Access the field as a DateTime. /// /// public DateTime AsDateTime() { return AsDateTime(new DateTime(0, DateTimeKind.Utc)); } /// /// Access the field as a DateTime. /// /// /// public DateTime AsDateTime(DateTime defaultValue) { return new DateTime(AsDateTimeTicks(defaultValue.ToUniversalTime().Ticks), DateTimeKind.Utc); } /// /// Reads a timespan as number of ticks from the stream /// /// /// public long AsTimeSpanTicks(long defaultValue = 0) { if (CbFieldUtils.IsTimeSpan(TypeWithFlags)) { Error = CbFieldError.None; return BinaryPrimitives.ReadInt64BigEndian(Payload.Span); } else { Error = CbFieldError.TypeError; return defaultValue; } } /// /// Reads a timespan as number of ticks from the stream /// /// /// public TimeSpan AsTimeSpan(TimeSpan defaultValue = default) => new TimeSpan(AsTimeSpanTicks(defaultValue.Ticks)); /// /// Access the field as a object id /// /// public CbObjectId AsObjectId() => AsObjectId(CbObjectId.Zero); /// /// Access the field as a object id /// /// /// public CbObjectId AsObjectId(CbObjectId defaultValue) { if (CbFieldUtils.IsObjectId(TypeWithFlags)) { Error = CbFieldError.None; return new CbObjectId(Payload.Span); } else { Error = CbFieldError.TypeError; return defaultValue; } } /// public bool HasName() => CbFieldUtils.HasFieldName(TypeWithFlags); /// public bool IsNull() => CbFieldUtils.IsNull(TypeWithFlags); /// public bool IsObject() => CbFieldUtils.IsObject(TypeWithFlags); /// public bool IsArray() => CbFieldUtils.IsArray(TypeWithFlags); /// public bool IsBinary() => CbFieldUtils.IsBinary(TypeWithFlags); /// public bool IsString() => CbFieldUtils.IsString(TypeWithFlags); /// public bool IsInteger() => CbFieldUtils.IsInteger(TypeWithFlags); /// public bool IsFloat() => CbFieldUtils.IsFloat(TypeWithFlags); /// public bool IsBool() => CbFieldUtils.IsBool(TypeWithFlags); /// public bool IsObjectAttachment() => CbFieldUtils.IsObjectAttachment(TypeWithFlags); /// public bool IsBinaryAttachment() => CbFieldUtils.IsBinaryAttachment(TypeWithFlags); /// public bool IsAttachment() => CbFieldUtils.IsAttachment(TypeWithFlags); /// public bool IsHash() => CbFieldUtils.IsHash(TypeWithFlags); /// public bool IsUuid() => CbFieldUtils.IsUuid(TypeWithFlags); /// public bool IsDateTime() => CbFieldUtils.IsDateTime(TypeWithFlags); /// public bool IsTimeSpan() => CbFieldUtils.IsTimeSpan(TypeWithFlags); /// public bool IsObjectId() => CbFieldUtils.IsObjectId(TypeWithFlags); /// /// Whether the field has a value /// /// public static explicit operator bool(CbField field) => field.HasValue(); /// /// Whether the field has a value. /// /// All fields in a valid object or array have a value. A field with no value is returned when /// finding a field by name fails or when accessing an iterator past the end. /// public bool HasValue() => !CbFieldUtils.IsNone(TypeWithFlags); /// /// Whether the last field access encountered an error. /// public bool HasError() => Error != CbFieldError.None; /// public CbFieldError GetError() => Error; /// /// Returns the size of the field in bytes, including the type and name /// /// public int GetSize() => sizeof(CbFieldType) + GetViewNoType().Length; /// /// Calculate the hash of the field, including the type and name. /// /// public Blake3Hash GetHash() { using (Blake3.Hasher hasher = Blake3.Hasher.New()) { AppendHash(hasher); byte[] hash = new byte[32]; hasher.Finalize(hash); return new Blake3Hash(hash); } } /// /// Append the hash of the field, including the type and name /// /// void AppendHash(Blake3.Hasher hasher) { Span data = [(byte)CbFieldUtils.GetSerializedType(TypeWithFlags)]; hasher.Update(data); hasher.Update(GetViewNoType().Span); } /// public override int GetHashCode() => throw new NotImplementedException(); /// public override bool Equals(object? other) => Equals(other as CbField); /// /// Whether this field is identical to the other field. /// /// Performs a deep comparison of any contained arrays or objects and their fields. Comparison /// assumes that both fields are valid and are written in the canonical format. Fields must be /// written in the same order in arrays and objects, and name comparison is case sensitive. If /// these assumptions do not hold, this may return false for equivalent inputs. Validation can /// be performed with ValidateCompactBinary, except for field order and field name case. /// /// /// public bool Equals(CbField? other) { return other != null && CbFieldUtils.GetSerializedType(TypeWithFlags) == CbFieldUtils.GetSerializedType(other.TypeWithFlags) && GetViewNoType().Span.SequenceEqual(other.GetViewNoType().Span); } /// /// Copy the field into a buffer of exactly GetSize() bytes, including the type and name. /// /// public void CopyTo(Span buffer) { buffer[0] = (byte)CbFieldUtils.GetSerializedType(TypeWithFlags); GetViewNoType().Span.CopyTo(buffer.Slice(1)); } /// /// Invoke the visitor for every attachment in the field. /// /// public void IterateAttachments(Action visitor) { switch (GetType()) { case CbFieldType.Object: case CbFieldType.UniformObject: CbObject.FromFieldNoCheck(this).IterateAttachments(visitor); break; case CbFieldType.Array: case CbFieldType.UniformArray: CbArray.FromFieldNoCheck(this).IterateAttachments(visitor); break; case CbFieldType.ObjectAttachment: case CbFieldType.BinaryAttachment: visitor(this); break; } } /// /// Try to get a view of the field as it would be serialized, such as by CopyTo. /// /// A view is available if the field contains its type. Access the equivalent for other fields /// through FCbField::GetBuffer, FCbField::Clone, or CopyTo. /// /// /// public bool TryGetView(out ReadOnlyMemory outView) { if (CbFieldUtils.HasFieldType(TypeWithFlags)) { outView = Memory; return true; } else { outView = ReadOnlyMemory.Empty; return false; } } /// /// Find a field of an object by case-sensitive name comparison, otherwise a field with no value. /// /// /// #pragma warning disable CA1043 // Use Integral Or String Argument For Indexers public CbField this[CbFieldName name] => this.FirstOrDefault(field => field.Name == name.Text) ?? CbField.Empty; #pragma warning restore CA1043 // Use Integral Or String Argument For Indexers /// /// Create an iterator for the fields of an array or object, otherwise an empty iterator. /// /// public CbFieldIterator CreateIterator() { CbFieldType localTypeWithFlags = TypeWithFlags; if (CbFieldUtils.HasFields(localTypeWithFlags)) { ReadOnlyMemory payloadBytes = Payload; int payloadSizeByteCount; int payloadSize = (int)VarInt.ReadUnsigned(payloadBytes.Span, out payloadSizeByteCount); payloadBytes = payloadBytes.Slice(payloadSizeByteCount); int numByteCount = CbFieldUtils.IsArray(localTypeWithFlags) ? (int)VarInt.Measure(payloadBytes.Span) : 0; if (payloadSize > numByteCount) { payloadBytes = payloadBytes.Slice(numByteCount); CbFieldType uniformType = CbFieldType.HasFieldType; if (CbFieldUtils.HasUniformFields(TypeWithFlags)) { uniformType = (CbFieldType)payloadBytes.Span[0]; payloadBytes = payloadBytes.Slice(1); } return new CbFieldIterator(payloadBytes, uniformType); } } return new CbFieldIterator(ReadOnlyMemory.Empty, CbFieldType.HasFieldType); } /// public IEnumerator GetEnumerator() { for (CbFieldIterator iter = CreateIterator(); iter; iter.MoveNext()) { yield return iter.Current; } } /// IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); /// /// Returns a view of the name and value payload, which excludes the type. /// /// private ReadOnlyMemory GetViewNoType() { int nameSize = CbFieldUtils.HasFieldName(TypeWithFlags) ? _nameLen + (int)VarInt.MeasureUnsigned((uint)_nameLen) : 0; return Memory.Slice(_payloadOffset - nameSize); } /// /// Accessor for the payload /// internal ReadOnlyMemory Payload => Memory.Slice(_payloadOffset); /// /// Returns a view of the value payload, which excludes the type and name. /// /// internal ReadOnlyMemory GetPayloadView() => Memory.Slice(_payloadOffset); /// /// Returns the type of the field excluding flags. /// internal new CbFieldType GetType() => CbFieldUtils.GetType(TypeWithFlags); /// /// Returns the type of the field excluding flags. /// internal CbFieldType GetTypeWithFlags() => TypeWithFlags; /// /// Returns the size of the value payload in bytes, which is the field excluding the type and name. /// /// Size of the payload public ulong GetPayloadSize() { switch (GetType()) { case CbFieldType.None: case CbFieldType.Null: return 0; case CbFieldType.Object: case CbFieldType.UniformObject: case CbFieldType.Array: case CbFieldType.UniformArray: case CbFieldType.Binary: case CbFieldType.String: { ulong payloadSize = VarInt.ReadUnsigned(Payload.Span, out int bytesRead); return payloadSize + (ulong)bytesRead; } case CbFieldType.IntegerPositive: case CbFieldType.IntegerNegative: { return (ulong)VarInt.Measure(Payload.Span); } case CbFieldType.Float32: return 4; case CbFieldType.Float64: return 8; case CbFieldType.BoolFalse: case CbFieldType.BoolTrue: return 0; case CbFieldType.ObjectAttachment: case CbFieldType.BinaryAttachment: case CbFieldType.Hash: return 20; case CbFieldType.Uuid: return 16; case CbFieldType.DateTime: case CbFieldType.TimeSpan: return 8; case CbFieldType.ObjectId: return 12; default: return 0; } } /// /// Verifies if type and value is equal between two fields, ignoring the name /// /// /// public bool ValueEquals(CbField? other) { return other != null && CbFieldUtils.GetType(TypeWithFlags) == CbFieldUtils.GetType(other.TypeWithFlags) && GetPayloadView().Span.SequenceEqual(other.GetPayloadView().Span); } } /// /// Converter to and from JSON objects /// public class CbFieldJsonConverter : JsonConverter { /// public override CbField Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { throw new NotImplementedException(); } /// public override void Write(Utf8JsonWriter writer, CbField field, JsonSerializerOptions options) { WriteField(writer, field, options); } /// /// Writes a CbField to a Jtf8JsonWriter /// /// The json writer /// The field to write /// Serialization options /// public static void WriteField(Utf8JsonWriter writer, CbField field, JsonSerializerOptions options) { switch (field.GetType()) { case CbFieldType.Null: if (field.HasName()) { writer.WriteNull(field.Name.Span); } else { writer.WriteNullValue(); } break; case CbFieldType.Object: case CbFieldType.UniformObject: if (field.HasName()) { writer.WriteStartObject(field.Name.Span); } else { writer.WriteStartObject(); } foreach (CbField member in field.AsObject()) { WriteField(writer, member, options); } writer.WriteEndObject(); break; case CbFieldType.Array: case CbFieldType.UniformArray: if (field.HasName()) { writer.WriteStartArray(field.Name.Span); } else { writer.WriteStartArray(); } foreach (CbField element in field.AsArray()) { WriteField(writer, element, options); } writer.WriteEndArray(); break; case CbFieldType.Binary: if (field.HasName()) { writer.WriteBase64String(field.Name.Span, field.AsBinary().Span); } else { writer.WriteBase64StringValue(field.AsBinary().Span); } break; case CbFieldType.String: if (field.HasName()) { writer.WriteString(field.Name.Span, field.AsUtf8String().Span); } else { writer.WriteStringValue(field.AsUtf8String().Span); } break; case CbFieldType.IntegerPositive: if (field.HasName()) { writer.WriteNumber(field.Name.Span, field.AsUInt64()); } else { writer.WriteNumberValue(field.AsUInt64()); } break; case CbFieldType.IntegerNegative: if (field.HasName()) { writer.WriteNumber(field.Name.Span, field.AsInt64()); } else { writer.WriteNumberValue(field.AsInt64()); } break; case CbFieldType.Float32: case CbFieldType.Float64: if (field.HasName()) { writer.WriteNumber(field.Name.Span, field.AsDouble()); } else { writer.WriteNumberValue(field.AsDouble()); } break; case CbFieldType.BoolFalse: case CbFieldType.BoolTrue: if (field.HasName()) { writer.WriteBoolean(field.Name.Span, field.AsBool()); } else { writer.WriteBooleanValue(field.AsBool()); } break; case CbFieldType.ObjectAttachment: case CbFieldType.BinaryAttachment: case CbFieldType.Hash: if (field.HasName()) { writer.WriteString(field.Name.Span, field.AsHash().ToString()); } else { writer.WriteStringValue(field.AsHash().ToString()); } break; case CbFieldType.Uuid: case CbFieldType.ObjectId: if (field.HasName()) { writer.WriteString(field.Name.Span, field.AsUuid().ToString()); } else { writer.WriteStringValue(field.AsUuid().ToString()); } break; case CbFieldType.DateTime: if (field.HasName()) { writer.WriteNumber(field.Name.Span, field.AsDateTimeTicks()); } else { writer.WriteNumberValue(field.AsDateTimeTicks()); } break; case CbFieldType.TimeSpan: if (field.HasName()) { writer.WriteNumber(field.Name.Span, field.AsTimeSpanTicks()); } else { writer.WriteNumberValue(field.AsTimeSpanTicks()); } break; default: throw new NotImplementedException($"Unhandled type '{field.GetType().ToString()}' in cb-json converter"); } } } /// /// Converter to and from JSON objects /// public class CbObjectJsonConverter : JsonConverter { /// public override CbObject Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { throw new NotImplementedException(); } /// public override void Write(Utf8JsonWriter writer, CbObject o, JsonSerializerOptions options) { writer.WriteStartObject(); foreach (CbField member in o) { CbFieldJsonConverter.WriteField(writer, member, options); } writer.WriteEndObject(); } } /// /// Enumerator for contents of a field /// /// /// public sealed class CbFieldEnumerator(ReadOnlyMemory data, CbFieldType uniformType) : IEnumerator { /// public CbField Current { get; private set; } = null!; /// object? IEnumerator.Current => Current; /// public void Dispose() { } /// public void Reset() { throw new InvalidOperationException(); } /// public bool MoveNext() { if (data.Length > 0) { Current = new CbField(data, uniformType); return true; } else { Current = null!; return false; } } /// /// Clone this enumerator /// /// public CbFieldEnumerator Clone() { return new CbFieldEnumerator(data, uniformType); } } /// /// Iterator for fields /// public class CbFieldIterator { /// /// The underlying buffer /// ReadOnlyMemory _nextData; /// /// Type for all fields /// readonly CbFieldType _uniformType; /// /// The current iterator /// public CbField Current { get; private set; } = null!; /// /// Default constructor /// public CbFieldIterator() : this(ReadOnlyMemory.Empty, CbFieldType.HasFieldType) { } /// /// Constructor for single field iterator /// /// private CbFieldIterator(CbField field) { _nextData = ReadOnlyMemory.Empty; Current = field; } /// /// Constructor /// /// /// public CbFieldIterator(ReadOnlyMemory data, CbFieldType uniformType) { _nextData = data; _uniformType = uniformType; MoveNext(); } /// /// Copy constructor /// /// public CbFieldIterator(CbFieldIterator other) { _nextData = other._nextData; _uniformType = other._uniformType; Current = other.Current; } /// /// Construct a field range that contains exactly one field. /// /// /// public static CbFieldIterator MakeSingle(CbField field) { return new CbFieldIterator(field); } /// /// Construct a field range from a buffer containing zero or more valid fields. /// /// A buffer containing zero or more valid fields. /// HasFieldType means that View contains the type.Otherwise, use the given type. /// public static CbFieldIterator MakeRange(ReadOnlyMemory view, CbFieldType type = CbFieldType.HasFieldType) { return new CbFieldIterator(view, type); } /// /// Check if the current value is valid /// /// public bool IsValid() { return Current.GetType() != CbFieldType.None; } /// /// Accessor for the current value /// /// public CbField GetCurrent() { return Current; } /// /// Invoke the visitor for every attachment in the field range. /// /// public void IterateRangeAttachments(Action visitor) { // Always iterate over non-uniform ranges because we do not know if they contain an attachment. if (CbFieldUtils.HasFieldType(Current.GetTypeWithFlags())) { for (CbFieldIterator it = new CbFieldIterator(this); it; ++it) { if (CbFieldUtils.MayContainAttachments(it.Current.GetTypeWithFlags())) { it.Current.IterateAttachments(visitor); } } } // Only iterate over uniform ranges if the uniform type may contain an attachment. else { if (CbFieldUtils.MayContainAttachments(Current.GetTypeWithFlags())) { for (CbFieldIterator it = new CbFieldIterator(this); it; ++it) { it.Current.IterateAttachments(visitor); } } } } /// /// Move to the next element /// /// public bool MoveNext() { if (_nextData.Length > 0) { Current = new CbField(_nextData, _uniformType); _nextData = _nextData.Slice(Current.Memory.Length); return true; } else { Current = CbField.Empty; return false; } } /// /// Test whether the iterator is valid /// /// public static implicit operator bool(CbFieldIterator iterator) { return iterator.IsValid(); } /// /// Move to the next item /// /// /// public static CbFieldIterator operator ++(CbFieldIterator iterator) { return new CbFieldIterator(iterator._nextData, iterator._uniformType); } /// public override bool Equals(object? obj) { throw new NotImplementedException(); } /// public override int GetHashCode() { throw new NotImplementedException(); } /// public static bool operator ==(CbFieldIterator a, CbFieldIterator b) { return a.Current.Equals(b.Current); } /// public static bool operator !=(CbFieldIterator a, CbFieldIterator b) { return !a.Current.Equals(b.Current); } } /// /// Simplified view of for display in the debugger /// class CbArrayDebugView(CbArray array) { [DebuggerBrowsable(DebuggerBrowsableState.RootHidden)] public object?[] Value => array.Select(x => x.Value).ToArray(); } /// /// Array of CbField that have no names. /// /// Accessing a field of the array requires iteration. Access by index is not provided because the /// cost of accessing an item by index scales linearly with the index. /// [DebuggerDisplay("Count = {Count}")] [DebuggerTypeProxy(typeof(CbArrayDebugView))] public class CbArray : IEnumerable { /// /// The field containing this array /// readonly CbField _innerField; /// /// Empty array constant /// public static CbArray Empty { get; } = new CbArray(new byte[] { (byte)CbFieldType.Array, 1, 0 }); /// /// Construct an array with no fields /// public CbArray() { _innerField = Empty._innerField; } /// /// Constructor /// /// private CbArray(CbField field) { _innerField = field; } /// /// Constructor /// /// /// public CbArray(ReadOnlyMemory data, CbFieldType type = CbFieldType.HasFieldType) { _innerField = new CbField(data, type); } /// /// Number of items in this array /// public int Count { get { ReadOnlyMemory payloadBytes = _innerField.Payload; payloadBytes = payloadBytes.Slice(VarInt.Measure(payloadBytes.Span)); return (int)VarInt.ReadUnsigned(payloadBytes.Span, out int _); } } /// /// Access the array as an array field. /// /// public CbField AsField() => _innerField; /// /// Construct an array from an array field. No type check is performed! /// /// /// public static CbArray FromFieldNoCheck(CbField field) => new CbArray(field); /// /// Returns the size of the array in bytes if serialized by itself with no name. /// /// public int GetSize() { return (int)Math.Min((ulong)sizeof(CbFieldType) + _innerField.GetPayloadSize(), Int32.MaxValue); } /// /// Calculate the hash of the array if serialized by itself with no name. /// /// public Blake3Hash GetHash() { using (Blake3.Hasher hasher = Blake3.Hasher.New()) { AppendHash(hasher); byte[] result = new byte[Blake3Hash.NumBytes]; hasher.Finalize(result); return new Blake3Hash(result); } } /// /// Append the hash of the array if serialized by itself with no name. /// public void AppendHash(Blake3.Hasher hasher) { byte[] serializedType = [(byte)_innerField.GetType()]; hasher.Update(serializedType); hasher.Update(_innerField.Payload.Span); } /// public override bool Equals(object? obj) => Equals(obj as CbArray); /// public override int GetHashCode() => BinaryPrimitives.ReadInt32BigEndian(GetHash().Span); /// /// Whether this array is identical to the other array. /// /// Performs a deep comparison of any contained arrays or objects and their fields. Comparison /// assumes that both fields are valid and are written in the canonical format.Fields must be /// written in the same order in arrays and objects, and name comparison is case sensitive.If /// these assumptions do not hold, this may return false for equivalent inputs. Validation can /// be done with the All mode to check these assumptions about the format of the inputs. /// /// /// public bool Equals(CbArray? other) { return other != null && GetType() == other.GetType() && GetPayloadView().Span.SequenceEqual(other.GetPayloadView().Span); } /// /// Copy the array into a buffer of exactly GetSize() bytes, with no name. /// /// public void CopyTo(Span buffer) { buffer[0] = (byte)GetType(); GetPayloadView().Span.CopyTo(buffer.Slice(1)); } /** Invoke the visitor for every attachment in the array. */ public void IterateAttachments(Action visitor) => CreateIterator().IterateRangeAttachments(visitor); /// /// Try to get a view of the array as it would be serialized, such as by CopyTo. /// /// A view is available if the array contains its type and has no name. Access the equivalent /// for other arrays through FCbArray::GetBuffer, FCbArray::Clone, or CopyTo. /// public bool TryGetView(out ReadOnlyMemory outView) { if (_innerField.HasName()) { outView = ReadOnlyMemory.Empty; return false; } return _innerField.TryGetView(out outView); } /// public CbFieldIterator CreateIterator() => _innerField.CreateIterator(); /// public IEnumerator GetEnumerator() => _innerField.GetEnumerator(); /// IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); #region Mimic inheritance from CbField /// internal new CbFieldType GetType() => _innerField.GetType(); /// internal ReadOnlyMemory GetPayloadView() => _innerField.GetPayloadView(); #endregion } /// /// Simplified view of for display in the debugger /// class CbObjectDebugView(CbObject obj) { [DebuggerDisplay("{Name}: {Value}")] public class Property { public string? Name { get; set; } public object? Value { get; set; } } [DebuggerBrowsable(DebuggerBrowsableState.RootHidden)] public Property[] Properties => obj.Select(x => new Property { Name = x.Name.ToString(), Value = x.Value }).ToArray(); } /// /// Array of CbField that have unique names. /// /// Accessing the fields of an object is always a safe operation, even if the requested field does /// not exist. Fields may be accessed by name or through iteration. When a field is requested that /// is not found in the object, the field that it returns has no value (evaluates to false) though /// attempting to access the empty field is also safe, as described by FCbFieldView. /// [DebuggerTypeProxy(typeof(CbObjectDebugView))] [JsonConverter(typeof(CbObjectJsonConverter))] public class CbObject : IEnumerable { /// /// Empty array constant /// public static CbObject Empty { get; } = CbObject.FromFieldNoCheck(new CbField(new byte[] { (byte)CbFieldType.Object, 0 })); /// /// The inner field object /// private readonly CbField _innerField; /// /// Constructor /// /// private CbObject(CbField field) { _innerField = new CbField(field.Memory, field.TypeWithFlags); } /// /// Constructor /// /// /// Explicit type of the data in buffer public CbObject(ReadOnlyMemory buffer, CbFieldType fieldType = CbFieldType.HasFieldType) { _innerField = new CbField(buffer, fieldType); } /// /// Builds an object by calling a delegate with a writer /// /// /// public static CbObject Build(Action build) { CbWriter writer = new CbWriter(); writer.BeginObject(); build(writer); writer.EndObject(); return new CbObject(writer.ToByteArray()); } /// /// Find a field by case-sensitive name comparison. /// /// The cost of this operation scales linearly with the number of fields in the object. Prefer to /// iterate over the fields only once when consuming an object. /// /// The name of the field. /// The matching field if found, otherwise a field with no value. public CbField Find(CbFieldName name) => _innerField[name.Text]; /// /// Find a field by case-insensitive name comparison. /// /// The name of the field. /// The matching field if found, otherwise a field with no value. public CbField FindIgnoreCase(CbFieldName name) => _innerField.FirstOrDefault(field => Utf8StringComparer.OrdinalIgnoreCase.Equals(field.Name, name.Text)) ?? new CbField(); /// /// Find a field by case-sensitive name comparison. /// /// The name of the field. /// The matching field if found, otherwise a field with no value. #pragma warning disable CA1043 // Use Integral Or String Argument For Indexers public CbField this[CbFieldName name] => _innerField[name.Text]; #pragma warning restore CA1043 // Use Integral Or String Argument For Indexers /// /// Gets the underlying field for this object /// /// public CbField AsField() => _innerField; /// /// Construct an object from an object field. No type check is performed! /// /// /// public static CbObject FromFieldNoCheck(CbField field) => new CbObject(field); /// /// Returns the size of the object in bytes if serialized by itself with no name. /// /// public int GetSize() { return sizeof(CbFieldType) + _innerField.Payload.Length; } /// /// Calculate the hash of the object if serialized by itself with no name. /// /// public Blake3Hash GetHash() { using (Blake3.Hasher hasher = Blake3.Hasher.New()) { AppendHash(hasher); byte[] data = new byte[Blake3Hash.NumBytes]; hasher.Finalize(data); return new Blake3Hash(data); } } /// /// Append the hash of the object if serialized by itself with no name. /// /// public void AppendHash(Blake3.Hasher hasher) { byte[] temp = [(byte)_innerField.GetType()]; hasher.Update(temp); hasher.Update(_innerField.Payload.Span); } /// public override bool Equals(object? obj) => Equals(obj as CbObject); /// public override int GetHashCode() => BinaryPrimitives.ReadInt32BigEndian(GetHash().Span); /// /// Whether this object is identical to the other object. /// /// Performs a deep comparison of any contained arrays or objects and their fields. Comparison /// assumes that both fields are valid and are written in the canonical format. Fields must be /// written in the same order in arrays and objects, and name comparison is case sensitive. If /// these assumptions do not hold, this may return false for equivalent inputs. Validation can /// be done with the All mode to check these assumptions about the format of the inputs. /// /// /// public bool Equals(CbObject? other) { return other != null && _innerField.GetType() == other._innerField.GetType() && _innerField.Payload.Span.SequenceEqual(other._innerField.Payload.Span); } /// /// Copy the object into a buffer of exactly GetSize() bytes, with no name. /// /// public void CopyTo(Span buffer) { buffer[0] = (byte)_innerField.GetType(); _innerField.Payload.Span.CopyTo(buffer.Slice(1)); } /// /// Invoke the visitor for every attachment in the object. /// /// public void IterateAttachments(Action visitor) => CreateIterator().IterateRangeAttachments(visitor); /// /// Creates a view of the object, excluding the name /// /// public ReadOnlyMemory GetView() { ReadOnlyMemory memory; if (!TryGetView(out memory)) { byte[] data = new byte[GetSize()]; CopyTo(data); memory = data; } return memory; } /// /// Try to get a view of the object as it would be serialized, such as by CopyTo. /// /// A view is available if the object contains its type and has no name. Access the equivalent /// for other objects through FCbObject::GetBuffer, FCbObject::Clone, or CopyTo. /// /// /// public bool TryGetView(out ReadOnlyMemory outView) { if (_innerField.HasName()) { outView = ReadOnlyMemory.Empty; return false; } return _innerField.TryGetView(out outView); } /// public CbFieldIterator CreateIterator() => _innerField.CreateIterator(); /// public IEnumerator GetEnumerator() => _innerField.GetEnumerator(); /// IEnumerator IEnumerable.GetEnumerator() => _innerField.GetEnumerator(); /// /// Clone this object /// /// /// public static CbObject Clone(CbObject obj) => obj; #region Conversion to Json /// /// Convert this object to JSON /// /// public string ToJson() { ArrayBufferWriter buffer = new ArrayBufferWriter(); using (Utf8JsonWriter jsonWriter = new Utf8JsonWriter(buffer)) { ToJson(jsonWriter); } return Encoding.UTF8.GetString(buffer.WrittenMemory.Span); } /// /// Write this object to JSON /// /// public void ToJson(Utf8JsonWriter writer) { writer.WriteStartObject(); foreach (CbField field in _innerField) { WriteField(field, writer); } writer.WriteEndObject(); } /// /// Converts a json object into a CbObject /// /// public static CbObject FromJson(string json) { return CbJsonReader.FromJson(json); } /// /// Write a single field to a writer /// /// /// private static void WriteField(CbField field, Utf8JsonWriter writer) { if (field.IsObject()) { if (field._nameLen != 0) { writer.WriteStartObject(field.Name.Span); } else { writer.WriteStartObject(); } CbObject obj = field.AsObject(); foreach (CbField objField in obj._innerField) { WriteField(objField, writer); } writer.WriteEndObject(); } else if (field.IsArray()) { if (field._nameLen != 0) { writer.WriteStartArray(field.Name.Span); } else { writer.WriteStartArray(); } CbArray array = field.AsArray(); foreach (CbField objectField in array) { WriteField(objectField, writer); } writer.WriteEndArray(); } else if (field.IsInteger()) { if (field.GetType() == CbFieldType.IntegerNegative) { if (field._nameLen != 0) { writer.WriteNumber(field.Name.Span, -field.AsInt64()); } else { writer.WriteNumberValue(-field.AsInt64()); } } else { if (field._nameLen != 0) { writer.WriteNumber(field.Name.Span, field.AsUInt64()); } else { writer.WriteNumberValue(field.AsUInt64()); } } } else if (field.IsBool()) { if (field._nameLen != 0) { writer.WriteBoolean(field.Name.Span, field.AsBool()); } else { writer.WriteBooleanValue(field.AsBool()); } } else if (field.IsNull()) { if (field._nameLen != 0) { writer.WriteNull(field.Name.Span); } else { writer.WriteNullValue(); } } else if (field.IsDateTime()) { if (field._nameLen != 0) { writer.WriteString(field.Name.Span, field.AsDateTime()); } else { writer.WriteStringValue(field.AsDateTime()); } } else if (field.IsHash()) { ReadOnlySpan stringValue = field.AsHash().ToUtf8String().Span; if (field._nameLen != 0) { writer.WriteString(field.Name.Span, stringValue); } else { writer.WriteStringValue(stringValue); } } else if (field.IsString()) { ReadOnlySpan stringValue = field.AsUtf8String().Span; if (field._nameLen != 0) { writer.WriteString(field.Name.Span, stringValue); } else { writer.WriteStringValue(stringValue); } } else if (field.IsObjectId()) { ReadOnlySpan stringValue = field.AsObjectId().ToUtf8String().Span; if (field._nameLen != 0) { writer.WriteString(field.Name.Span, stringValue); } else { writer.WriteStringValue(stringValue); } } else { throw new NotImplementedException($"Unhandled type {field.GetType()} when attempting to convert to json"); } } #endregion } /// /// ObjectId used within compact binary, similar to an uuid type 4 /// /// time component /// serial component /// run component [JsonConverter(typeof(CbObjectIdJsonConverter))] [TypeConverter(typeof(CbObjectIdTypeConverter))] public readonly struct CbObjectId(uint a, uint b, uint c) : IEquatable { readonly uint _a = a; readonly uint _b = b; readonly uint _c = c; // initialize the serial to a random integer private static uint s_objectIdSerial = CalculateRunId(); /// /// The zero value of an object id /// public static CbObjectId Zero { get; } = new CbObjectId(0u, 0u, 0u); /// /// Constructor /// /// public CbObjectId(ReadOnlySpan payload) : this(BinaryPrimitives.ReadUInt32BigEndian(payload[0..4]), BinaryPrimitives.ReadUInt32BigEndian(payload[4..8]), BinaryPrimitives.ReadUInt32BigEndian(payload[8..12])) { } /// /// Parses a object id from the given hex string /// /// /// public static CbObjectId Parse(string text) { byte[] bytes = StringUtils.ParseHexString(text); return new CbObjectId(bytes); } /// /// Parses a digest from the given hex string /// /// /// Receives the object id if successful /// public static bool TryParse(string text, out CbObjectId objectId) { if (!StringUtils.TryParseHexString(text, out byte[]? bytes)) { objectId = CbObjectId.Zero; return false; } if (bytes.Length != 12) { objectId = CbObjectId.Zero; return false; } objectId = new CbObjectId(bytes); return true; } /// /// Generates a new object id /// /// public static CbObjectId NewObjectId() { const long Offset = 1_609_459_200; // Seconds from 1970 -> 2021 long longTime = DateTimeOffset.Now.ToUnixTimeSeconds() - Offset; uint time = (uint)longTime; uint serial = Interlocked.Increment(ref s_objectIdSerial); uint runId = RunId; return new CbObjectId(time, serial, runId); } private static uint RunId { get; } = CalculateRunId(); private static uint CalculateRunId() { Guid g = Guid.NewGuid(); return (uint)g.GetHashCode(); } /// /// Copies this objectid into a span /// /// public void CopyTo(Span span) { BinaryPrimitives.WriteUInt32BigEndian(span, _a); BinaryPrimitives.WriteUInt32BigEndian(span[4..], _b); BinaryPrimitives.WriteUInt32BigEndian(span[8..], _c); } /// /// Converts this object id to a byte array /// /// public byte[] ToByteArray() { byte[] b = new byte[12]; CopyTo(b); return b; } /// /// Creates a utf8string of the object id /// /// public Utf8String ToUtf8String() { return StringUtils.FormatUtf8HexString(ToByteArray()); } /// public override string ToString() { return StringUtils.FormatHexString(ToByteArray()); } /// public bool Equals(CbObjectId other) => _a == other._a && _b == other._b && _c == other._c; /// public override bool Equals(object? obj) => (obj is CbObjectId cid) && Equals(cid); /// public override int GetHashCode() => (int)_a; /// /// Test two object ids for equality /// public static bool operator ==(CbObjectId a, CbObjectId b) => a.Equals(b); /// /// Test two object ids for equality /// public static bool operator !=(CbObjectId a, CbObjectId b) => !(a == b); } /// /// Type converter for CbObjectId to and from JSON /// sealed class CbObjectIdJsonConverter : JsonConverter { /// public override CbObjectId Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) => CbObjectId.Parse(reader.GetString()!); /// public override void Write(Utf8JsonWriter writer, CbObjectId value, JsonSerializerOptions options) => writer.WriteStringValue(value.ToString()); } /// /// Type converter for CbObjectId /// sealed class CbObjectIdTypeConverter : TypeConverter { public override bool CanConvertFrom(ITypeDescriptorContext? context, Type sourceType) => sourceType == typeof(string); /// public override object ConvertFrom(ITypeDescriptorContext? context, CultureInfo? culture, object value) => CbObjectId.Parse((string)value); } }