// Copyright Epic Games, Inc. All Rights Reserved. using EpicGames.Core; using System; using System.Buffers; using System.Buffers.Binary; using System.Collections; using System.Collections.Generic; using System.Diagnostics; using System.Linq; using System.Numerics; using System.Text; using System.Text.Json; 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, /// /// Reserved for future use as a flag. Do not add types in this range. /// Reserved = 0x20, /// /// 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, } /// /// 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 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; public object? Value; } /// /// 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))] 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 /// public int NameLen; /// /// Offset of the payload within the memory /// public 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) { this.TypeWithFlags = Other.TypeWithFlags; this.Memory = Other.Memory; this.NameLen = Other.NameLen; this.PayloadOffset = Other.PayloadOffset; this.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.Read(Data.Slice(Offset).Span, out int NameLenByteCount); Offset += NameLenByteCount + NameLen; } this.Memory = Data; this.TypeWithFlags = Type; this.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: case CbFieldType.BinaryAttachment: case CbFieldType.Hash: return AsAttachment(); case CbFieldType.Uuid: return AsUuid(); case CbFieldType.DateTime: return AsDateTime(); case CbFieldType.TimeSpan: return AsTimeSpan(); 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(ReadOnlyMemory Default = default) { if (CbFieldUtils.IsBinary(TypeWithFlags)) { Error = CbFieldError.None; ulong Length = VarInt.Read(Payload.Span, out int BytesRead); return Payload.Slice(BytesRead, (int)Length); } else { Error = CbFieldError.TypeError; return Default; } } /// /// Access the field as a UTF-8 string. /// /// public Utf8String AsString() { return AsString(default); } /// /// Access the field as a UTF-8 string. Returns the provided default on error. /// /// Default value to return /// public Utf8String AsString(Utf8String Default) { if (CbFieldUtils.IsString(TypeWithFlags)) { ulong ValueSize = VarInt.Read(Payload.Span, out int ValueSizeByteCount); if (ValueSize >= (1UL << 31)) { Error = CbFieldError.RangeError; return Default; } else { Error = CbFieldError.None; return new Utf8String(Payload.Slice(ValueSizeByteCount, (int)ValueSize)); } } else { Error = CbFieldError.TypeError; return Default; } } /// /// Access the field as an int8. Returns the provided default on error. /// public sbyte AsInt8(sbyte Default = 0) { return (sbyte)AsInteger((ulong)Default, 7, true); } /// /// Access the field as an int16. Returns the provided default on error. /// public short AsInt16(short Default = 0) { return (short)AsInteger((ulong)Default, 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 Default) { return (int)AsInteger((ulong)Default, 31, true); } /// /// Access the field as an int64. Returns the provided default on error. /// public long AsInt64(long Default = 0) { return (long)AsInteger((ulong)Default, 63, true); } /// /// Access the field as an int8. Returns the provided default on error. /// public byte AsUInt8(byte Default = 0) { return (byte)AsInteger(Default, 8, false); } /// /// Access the field as an int16. Returns the provided default on error. /// public ushort AsUInt16(ushort Default = 0) { return (ushort)AsInteger(Default, 16, false); } /// /// Access the field as an int32. Returns the provided default on error. /// public uint AsUInt32(uint Default = 0) { return (uint)AsInteger(Default, 32, false); } /// /// Access the field as an int64. Returns the provided default on error. /// public ulong AsUInt64(ulong Default = 0) { return (ulong)AsInteger(Default, 64, false); } /// /// Access the field as an integer, checking that it's in the correct range /// /// /// /// /// private ulong AsInteger(ulong Default, 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; int MagnitudeByteCount; ulong Magnitude = VarInt.Read(Payload.Span, out MagnitudeByteCount); ulong Value = Magnitude ^ (ulong)-(long)(IsNegative); if ((Magnitude & OutOfRangeMask) == 0 && (IsNegative == 0 || IsSigned)) { Error = CbFieldError.None; return Value; } else { Error = CbFieldError.RangeError; return Default; } } else { Error = CbFieldError.TypeError; return Default; } } /// /// Access the field as a float. Returns the provided default on error. /// /// Default value /// Value of the field public float AsFloat(float Default = 0.0f) { switch (GetType()) { case CbFieldType.IntegerPositive: case CbFieldType.IntegerNegative: { ulong IsNegative = (ulong)TypeWithFlags & 1; ulong OutOfRangeMask = ~((1UL << /*FLT_MANT_DIG*/ 24) - 1); int MagnitudeByteCount; ulong Magnitude = VarInt.Read(Payload.Span, out MagnitudeByteCount) + IsNegative; bool IsInRange = (Magnitude & OutOfRangeMask) == 0; Error = IsInRange ? CbFieldError.None : CbFieldError.RangeError; return IsInRange ? (float)((IsNegative != 0) ? (float)-(long)Magnitude : (float)Magnitude) : Default; } case CbFieldType.Float32: Error = CbFieldError.None; return BitConverter.Int32BitsToSingle(BinaryPrimitives.ReadInt32BigEndian(Payload.Span)); case CbFieldType.Float64: Error = CbFieldError.RangeError; return Default; default: Error = CbFieldError.TypeError; return Default; } } /// /// Access the field as a double. Returns the provided default on error. /// /// Default value /// Value of the field public double AsDouble(double Default = 0.0f) { switch (GetType()) { case CbFieldType.IntegerPositive: case CbFieldType.IntegerNegative: { ulong IsNegative = (ulong)TypeWithFlags & 1; ulong OutOfRangeMask = ~((1UL << /*DBL_MANT_DIG*/ 53) - 1); int MagnitudeByteCount; ulong Magnitude = VarInt.Read(Payload.Span, out MagnitudeByteCount) + IsNegative; bool IsInRange = (Magnitude & OutOfRangeMask) == 0; Error = IsInRange ? CbFieldError.None : CbFieldError.RangeError; return IsInRange ? (double)((IsNegative != 0) ? (double)-(long)Magnitude : (double)Magnitude) : Default; } 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 Default; } } /// /// 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 Default) { switch (GetType()) { case CbFieldType.BoolTrue: Error = CbFieldError.None; return true; case CbFieldType.BoolFalse: Error = CbFieldError.None; return false; default: Error = CbFieldError.TypeError; return Default; } } /// /// Access the field as a hash referencing an object attachment. Returns the provided default on error. /// /// Value of the field public IoHash AsObjectAttachment() => AsObjectAttachment(IoHash.Zero); /// /// Access the field as a hash referencing an object attachment. Returns the provided default on error. /// /// Default value /// Value of the field public IoHash AsObjectAttachment(IoHash Default) { if (CbFieldUtils.IsObjectAttachment(TypeWithFlags)) { Error = CbFieldError.None; return new IoHash(Payload); } else { Error = CbFieldError.TypeError; return Default; } } /// /// Access the field as a hash referencing a binary attachment. Returns the provided default on error. /// /// Default value /// Value of the field public IoHash AsBinaryAttachment() => AsBinaryAttachment(IoHash.Zero); /// /// Access the field as a hash referencing a binary attachment. Returns the provided default on error. /// /// Default value /// Value of the field public IoHash AsBinaryAttachment(IoHash Default) { if (CbFieldUtils.IsBinaryAttachment(TypeWithFlags)) { Error = CbFieldError.None; return new IoHash(Payload); } else { Error = CbFieldError.TypeError; return Default; } } /// /// Access the field as a hash referencing an attachment. Returns the provided default on error. /// /// Default value /// 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 Default) { if (CbFieldUtils.IsAttachment(TypeWithFlags)) { Error = CbFieldError.None; return new IoHash(Payload); } else { Error = CbFieldError.TypeError; return Default; } } /// /// 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 Default) { if (CbFieldUtils.IsHash(TypeWithFlags)) { Error = CbFieldError.None; return new IoHash(Payload); } else { Error = CbFieldError.TypeError; return Default; } } /// /// Access the field as a UUID. Returns a nil UUID on error. /// /// Default value /// Value of the field public Guid AsUuid(Guid Default = default(Guid)) { 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 Default; } } /// /// Reads a date time as number of ticks from the stream /// /// /// public long AsDateTimeTicks(long Default = 0) { if (CbFieldUtils.IsDateTime(TypeWithFlags)) { Error = CbFieldError.None; return BinaryPrimitives.ReadInt64BigEndian(Payload.Span); } else { Error = CbFieldError.TypeError; return Default; } } /// /// Access the field as a DateTime. /// /// public DateTime AsDateTime() { return AsDateTime(default); } /// /// Access the field as a DateTime. /// /// /// public DateTime AsDateTime(DateTime Default) { return new DateTime(AsDateTimeTicks(Default.ToUniversalTime().Ticks), DateTimeKind.Utc); } /// /// Reads a timespan as number of ticks from the stream /// /// /// public long AsTimeSpanTicks(long Default = 0) { if (CbFieldUtils.IsTimeSpan(TypeWithFlags)) { Error = CbFieldError.None; return BinaryPrimitives.ReadInt64BigEndian(Payload.Span); } else { Error = CbFieldError.TypeError; return Default; } } /// /// Reads a timespan as number of ticks from the stream /// /// /// public TimeSpan AsTimeSpan(TimeSpan Default = default) => new TimeSpan(AsTimeSpanTicks(Default.Ticks)); /// 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); /// /// 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) { byte[] Data = new byte[] { (byte)CbFieldUtils.GetSerializedType(TypeWithFlags) }; Hasher.Update(Data); Hasher.Update(GetViewNoType().Span); } /// /// 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. /// /// /// public CbField this[Utf8String Name] { get { return this.FirstOrDefault(Field => Field.Name == Name) ?? CbField.Empty; } } /// /// 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.Read(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.Measure((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. /// /// /// /// 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.Read(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; } } #region Mimic inheritance from TCbBufferFactory public static CbField Clone(ReadOnlyMemory Data) => Clone(new CbField(Data)); public static CbField Clone(CbField Other) => Other; public static CbField MakeView(ReadOnlyMemory Data) => new CbField(Data); public static CbField MakeView(CbField Other) => Other; #endregion } public class CbFieldEnumerator : IEnumerator { /// /// The underlying buffer /// ReadOnlyMemory Data; /// /// Type for all fields /// CbFieldType UniformType { get; } /// public CbField Current { get; private set; } = null!; /// object? IEnumerator.Current => Current; /// /// Constructor /// /// /// public CbFieldEnumerator(ReadOnlyMemory Data, CbFieldType UniformType) { this.Data = Data; this.UniformType = UniformType; } /// 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 /// 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) { this.NextData = Data; this.UniformType = UniformType; MoveNext(); } /// /// Copy constructor /// /// public CbFieldIterator(CbFieldIterator Other) { this.NextData = Other.NextData; this.UniformType = Other.UniformType; this.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; } /// /// Copy the field range into a buffer of exactly GetRangeSize() bytes. /// /// public void CopyRangeTo(Span Buffer) { ReadOnlyMemory Source; if (TryGetRangeView(out Source)) { Source.Span.CopyTo(Buffer); } else { for (CbFieldIterator It = new CbFieldIterator(this); It; It.MoveNext()) { int Size = It.Current.GetSize(); It.Current.CopyTo(Buffer); Buffer = Buffer.Slice(Size); } } } /// /// 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); } } } } /// /// Try to get a view of every field in the range as they would be serialized. /// /// A view is available if each field contains its type. Access the equivalent for other field /// ranges through FCbFieldIterator::CloneRange or CopyRangeTo. /// /// bool TryGetRangeView(out ReadOnlyMemory OutView) { throw new NotImplementedException(); /* FMemoryView View; if (FieldType::TryGetView(View)) { OutView = MakeMemoryView(View.GetData(), FieldsEnd); return true; } return false;*/ } /// /// 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; public CbArrayDebugView(CbArray Array) => this.Array = 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.Read(PayloadBytes.Span, out int NumByteCount); } } /// /// 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(), int.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 = new byte[] { (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 #region Mimic inheritance from TCbBufferFactory public static CbArray Clone(ReadOnlyMemory Data) => Clone(new CbArray(Data)); public static CbArray Clone(CbArray Other) => Other; public static CbArray MakeView(ReadOnlyMemory Data) => new CbArray(Data); public static CbArray MakeView(CbArray Other) => Other; #endregion } /// /// Simplified view of for display in the debugger /// class CbObjectDebugView { [DebuggerDisplay("{Name}: {Value}")] public class Property { public string? Name { get; set; } public object? Value { get; set; } } CbObject Object; public CbObjectDebugView(CbObject Object) => this.Object = Object; [DebuggerBrowsable(DebuggerBrowsableState.RootHidden)] public Property[] Properties => Object.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))] public class CbObject : IEnumerable { /// /// Empty array constant /// public static CbObject Empty = CbObject.FromFieldNoCheck(new CbField(new byte[] { (byte)CbFieldType.Object, 0 })); /// /// The inner field object /// private CbField InnerField; /// /// Constructor /// /// private CbObject(CbField Field) { InnerField = new CbField(Field.Memory, Field.TypeWithFlags); } /// /// Constructor /// /// 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(Utf8String Name) => InnerField[Name]; /// /// 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(Utf8String Name) => InnerField.FirstOrDefault(Field => Utf8StringComparer.OrdinalIgnoreCase.Equals(Field.Name, Name)) ?? 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. public CbField this[Utf8String Name] => InnerField[Name]; /// 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 = new byte[] { (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 Object) => Object; #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(); } /// /// Write a single field to a writer /// /// /// private static void WriteField(CbField Field, Utf8JsonWriter Writer) { if (Field.IsObject()) { Writer.WriteStartObject(); CbObject Object = Field.AsObject(); foreach (CbField ObjectField in Object.InnerField) { WriteField(ObjectField, Writer); } Writer.WriteEndObject(); } else if (Field.IsArray()) { Writer.WriteStartArray(); Writer.WriteEndArray(); } else if (Field.IsInteger()) { if (Field.GetType() == CbFieldType.IntegerNegative) { Writer.WriteNumber(Field.Name.Span, -Field.AsInt64()); } else { Writer.WriteNumber(Field.Name.Span, Field.AsUInt64()); } } else if (Field.IsBool()) { Writer.WriteBoolean(Field.Name.Span, Field.AsBool()); } else if (Field.IsNull()) { Writer.WriteNullValue(); } else if (Field.IsDateTime()) { Writer.WriteString(Field.Name.Span, Field.AsDateTime()); } else if (Field.IsHash()) { Writer.WriteString(Field.Name.Span, StringUtils.FormatUtf8HexString(Field.AsHash().Span).Span); } else if (Field.IsString()) { Writer.WriteString(Field.Name.Span, Field.AsString().Span); } else { throw new NotImplementedException($"Unhandled type {Field.GetType()} when attempting to convert to json"); } } #endregion } }