using ZB.MOM.WW.OtOpcUa.Core.Scripting.Protocol.V1; namespace ZB.MOM.WW.OtOpcUa.Core.Scripting.Protocol; /// /// CLR value ↔ (design §4.4). The exact boxed CLR type survives the round trip — a script /// doing (short)ctx.GetTag(x).Value throws if a short arrives as an /// int — so the type tag names the CLR type, not just an OPC UA one. A type with no tag is never converted: /// refuses it, and the caller reports UNSUPPORTED_TYPE (design §14 #11). /// public static class VariantCodec { /// Encodes . /// The boxed CLR value (null allowed). /// The encoded variant when supported. /// The CLR type name when not supported. /// true when the type has a tag. public static bool TryEncode(object? value, out Variant variant, out string? unsupportedType) { unsupportedType = null; variant = new Variant(); switch (value) { case null: variant.Type = Variant.Types.TypeTag.Null; return true; case bool b: variant.Type = Variant.Types.TypeTag.Boolean; variant.B = b; return true; case sbyte v: variant.Type = Variant.Types.TypeTag.Sbyte; variant.I = v; return true; case byte v: variant.Type = Variant.Types.TypeTag.Byte; variant.U = v; return true; case short v: variant.Type = Variant.Types.TypeTag.Int16; variant.I = v; return true; case ushort v: variant.Type = Variant.Types.TypeTag.Uint16; variant.U = v; return true; case int v: variant.Type = Variant.Types.TypeTag.Int32; variant.I = v; return true; case uint v: variant.Type = Variant.Types.TypeTag.Uint32; variant.U = v; return true; case long v: variant.Type = Variant.Types.TypeTag.Int64; variant.I = v; return true; case ulong v: variant.Type = Variant.Types.TypeTag.Uint64; variant.U = v; return true; case float v: variant.Type = Variant.Types.TypeTag.Float; variant.F = v; return true; case double v: variant.Type = Variant.Types.TypeTag.Double; variant.D = v; return true; case string v: variant.Type = Variant.Types.TypeTag.String; variant.S = v; return true; case DateTime v: variant.Type = Variant.Types.TypeTag.DatetimeUtc; variant.DtTicks = ToUtc(v).Ticks; return true; case Array array when array.Rank == 1 && TryElementTag(array.GetType().GetElementType()!, out var elementTag): { var av = new ArrayValue { ElementType = elementTag }; foreach (var item in array) { if (!TryEncode(item, out var element, out unsupportedType)) return false; av.Items.Add(element); } variant.Type = Variant.Types.TypeTag.Array; variant.Array = av; return true; } default: unsupportedType = value.GetType().FullName ?? value.GetType().Name; return false; } } /// Decodes back to the boxed CLR value it was encoded from. /// The variant. /// The boxed value. /// An unknown tag, or a payload that does not match the tag. public static object? Decode(Variant? variant) { if (variant is null) return null; return variant.Type switch { Variant.Types.TypeTag.Null => null, Variant.Types.TypeTag.Boolean => Expect(variant, Variant.VOneofCase.B).B, Variant.Types.TypeTag.Sbyte => checked((sbyte)Expect(variant, Variant.VOneofCase.I).I), Variant.Types.TypeTag.Byte => checked((byte)Expect(variant, Variant.VOneofCase.U).U), Variant.Types.TypeTag.Int16 => checked((short)Expect(variant, Variant.VOneofCase.I).I), Variant.Types.TypeTag.Uint16 => checked((ushort)Expect(variant, Variant.VOneofCase.U).U), Variant.Types.TypeTag.Int32 => checked((int)Expect(variant, Variant.VOneofCase.I).I), Variant.Types.TypeTag.Uint32 => checked((uint)Expect(variant, Variant.VOneofCase.U).U), Variant.Types.TypeTag.Int64 => Expect(variant, Variant.VOneofCase.I).I, Variant.Types.TypeTag.Uint64 => Expect(variant, Variant.VOneofCase.U).U, Variant.Types.TypeTag.Float => Expect(variant, Variant.VOneofCase.F).F, Variant.Types.TypeTag.Double => Expect(variant, Variant.VOneofCase.D).D, Variant.Types.TypeTag.String => Expect(variant, Variant.VOneofCase.S).S, Variant.Types.TypeTag.DatetimeUtc => new DateTime(Expect(variant, Variant.VOneofCase.DtTicks).DtTicks, DateTimeKind.Utc), Variant.Types.TypeTag.Array => DecodeArray(Expect(variant, Variant.VOneofCase.Array).Array), _ => throw new ScriptWorkerProtocolException($"unknown variant type tag {(int)variant.Type}"), }; } /// A as UTC: a Local value is converted; an Unspecified one is taken as UTC. /// The value. /// The UTC value. public static DateTime ToUtc(DateTime value) => value.Kind switch { DateTimeKind.Utc => value, DateTimeKind.Local => value.ToUniversalTime(), _ => DateTime.SpecifyKind(value, DateTimeKind.Utc), }; private static Variant Expect(Variant variant, Variant.VOneofCase expected) { // proto3 drops a default scalar (0, false, "") from the oneof, so an absent payload is the default value. if (variant.VCase != expected && variant.VCase != Variant.VOneofCase.None) throw new ScriptWorkerProtocolException($"variant tagged {variant.Type} carries a {variant.VCase} payload"); return variant; } private static Array DecodeArray(ArrayValue? array) { if (array is null) throw new ScriptWorkerProtocolException("array variant without an array payload"); var elementType = ClrType(array.ElementType); var result = Array.CreateInstance(elementType, array.Items.Count); for (var i = 0; i < array.Items.Count; i++) { var item = array.Items[i]; if (item.Type != array.ElementType && item.Type != Variant.Types.TypeTag.Null) throw new ScriptWorkerProtocolException($"array of {array.ElementType} holds a {item.Type} element"); result.SetValue(Decode(item), i); } return result; } private static bool TryElementTag(Type elementType, out Variant.Types.TypeTag tag) { tag = elementType switch { _ when elementType == typeof(bool) => Variant.Types.TypeTag.Boolean, _ when elementType == typeof(sbyte) => Variant.Types.TypeTag.Sbyte, _ when elementType == typeof(byte) => Variant.Types.TypeTag.Byte, _ when elementType == typeof(short) => Variant.Types.TypeTag.Int16, _ when elementType == typeof(ushort) => Variant.Types.TypeTag.Uint16, _ when elementType == typeof(int) => Variant.Types.TypeTag.Int32, _ when elementType == typeof(uint) => Variant.Types.TypeTag.Uint32, _ when elementType == typeof(long) => Variant.Types.TypeTag.Int64, _ when elementType == typeof(ulong) => Variant.Types.TypeTag.Uint64, _ when elementType == typeof(float) => Variant.Types.TypeTag.Float, _ when elementType == typeof(double) => Variant.Types.TypeTag.Double, _ when elementType == typeof(string) => Variant.Types.TypeTag.String, _ when elementType == typeof(DateTime) => Variant.Types.TypeTag.DatetimeUtc, _ => Variant.Types.TypeTag.Null, }; return tag != Variant.Types.TypeTag.Null; } private static Type ClrType(Variant.Types.TypeTag tag) => tag switch { Variant.Types.TypeTag.Boolean => typeof(bool), Variant.Types.TypeTag.Sbyte => typeof(sbyte), Variant.Types.TypeTag.Byte => typeof(byte), Variant.Types.TypeTag.Int16 => typeof(short), Variant.Types.TypeTag.Uint16 => typeof(ushort), Variant.Types.TypeTag.Int32 => typeof(int), Variant.Types.TypeTag.Uint32 => typeof(uint), Variant.Types.TypeTag.Int64 => typeof(long), Variant.Types.TypeTag.Uint64 => typeof(ulong), Variant.Types.TypeTag.Float => typeof(float), Variant.Types.TypeTag.Double => typeof(double), Variant.Types.TypeTag.String => typeof(string), Variant.Types.TypeTag.DatetimeUtc => typeof(DateTime), _ => throw new ScriptWorkerProtocolException($"array element type {tag} is not supported"), }; }