using System.Buffers.Binary; using System.Text; using Shouldly; using Xunit; using ZB.MOM.WW.OtOpcUa.Driver.AbCip; namespace ZB.MOM.WW.OtOpcUa.Driver.AbCip.Tests; [Trait("Category", "Unit")] public sealed class CipTemplateObjectDecoderTests { /// /// Construct a Template Object blob — header + member blocks + semicolon-delimited /// strings (UDT name first, then member names). /// private static byte[] BuildTemplate( string udtName, uint instanceSize, params (string name, ushort info, ushort arraySize, uint offset)[] members) { var memberCount = (ushort)members.Length; var headerSize = 12; var memberBlockSize = 8; var blocksSize = memberBlockSize * members.Length; var stringsBuf = new MemoryStream(); void AppendString(string s) { var bytes = Encoding.ASCII.GetBytes(s + ";\0"); stringsBuf.Write(bytes, 0, bytes.Length); } AppendString(udtName); foreach (var m in members) AppendString(m.name); var strings = stringsBuf.ToArray(); var buf = new byte[headerSize + blocksSize + strings.Length]; BinaryPrimitives.WriteUInt16LittleEndian(buf.AsSpan(0), memberCount); BinaryPrimitives.WriteUInt16LittleEndian(buf.AsSpan(2), 0x1234); BinaryPrimitives.WriteUInt32LittleEndian(buf.AsSpan(4), instanceSize); BinaryPrimitives.WriteUInt32LittleEndian(buf.AsSpan(8), 0); for (var i = 0; i < members.Length; i++) { var o = headerSize + (i * memberBlockSize); BinaryPrimitives.WriteUInt16LittleEndian(buf.AsSpan(o), members[i].info); BinaryPrimitives.WriteUInt16LittleEndian(buf.AsSpan(o + 2), members[i].arraySize); BinaryPrimitives.WriteUInt32LittleEndian(buf.AsSpan(o + 4), members[i].offset); } Buffer.BlockCopy(strings, 0, buf, headerSize + blocksSize, strings.Length); return buf; } [Fact] public void Simple_two_member_UDT_decodes_correctly() { var bytes = BuildTemplate("MotorUdt", instanceSize: 8, ("Speed", info: 0xC4, arraySize: 0, offset: 0), // DINT at offset 0 ("Enabled", info: 0xC1, arraySize: 0, offset: 4)); // BOOL at offset 4 var shape = CipTemplateObjectDecoder.Decode(bytes); shape.ShouldNotBeNull(); shape.TypeName.ShouldBe("MotorUdt"); shape.TotalSize.ShouldBe(8); shape.Members.Count.ShouldBe(2); shape.Members[0].Name.ShouldBe("Speed"); shape.Members[0].DataType.ShouldBe(AbCipDataType.DInt); shape.Members[0].Offset.ShouldBe(0); shape.Members[0].ArrayLength.ShouldBe(1); shape.Members[1].Name.ShouldBe("Enabled"); shape.Members[1].DataType.ShouldBe(AbCipDataType.Bool); shape.Members[1].Offset.ShouldBe(4); } [Fact] public void Struct_member_flag_surfaces_Structure_type() { var bytes = BuildTemplate("ContainerUdt", instanceSize: 32, ("InnerStruct", info: 0x8042, arraySize: 0, offset: 0)); // struct flag + template-id 0x42 var shape = CipTemplateObjectDecoder.Decode(bytes); shape.ShouldNotBeNull(); shape.Members.Single().DataType.ShouldBe(AbCipDataType.Structure); } [Fact] public void Array_member_carries_non_one_ArrayLength() { var bytes = BuildTemplate("ArrayUdt", instanceSize: 40, ("Values", info: 0xC4, arraySize: 10, offset: 0)); var shape = CipTemplateObjectDecoder.Decode(bytes); shape.ShouldNotBeNull(); shape.Members.Single().ArrayLength.ShouldBe(10); } [Fact] public void Multiple_atomic_types_preserve_offsets_and_types() { var bytes = BuildTemplate("MixedUdt", instanceSize: 24, ("A", 0xC1, 0, 0), // BOOL ("B", 0xC2, 0, 1), // SINT ("C", 0xC3, 0, 2), // INT ("D", 0xC4, 0, 4), // DINT ("E", 0xCA, 0, 8), // REAL ("F", 0xCB, 0, 16)); // LREAL var shape = CipTemplateObjectDecoder.Decode(bytes); shape.ShouldNotBeNull(); shape.Members.Count.ShouldBe(6); shape.Members.Select(m => m.DataType).ShouldBe( [AbCipDataType.Bool, AbCipDataType.SInt, AbCipDataType.Int, AbCipDataType.DInt, AbCipDataType.Real, AbCipDataType.LReal]); shape.Members.Select(m => m.Offset).ShouldBe([0, 1, 2, 4, 8, 16]); } [Fact] public void Unknown_atomic_type_code_falls_back_to_Structure() { var bytes = BuildTemplate("WeirdUdt", instanceSize: 4, ("Unknown", info: 0xFF, 0, 0)); var shape = CipTemplateObjectDecoder.Decode(bytes); shape.ShouldNotBeNull(); shape.Members.Single().DataType.ShouldBe(AbCipDataType.Structure); } [Fact] public void Zero_member_count_returns_null() { var buf = new byte[12]; BinaryPrimitives.WriteUInt16LittleEndian(buf.AsSpan(0), 0); CipTemplateObjectDecoder.Decode(buf).ShouldBeNull(); } [Fact] public void Short_buffer_returns_null() { CipTemplateObjectDecoder.Decode([0x01, 0x00]).ShouldBeNull(); // only 2 bytes — less than header } [Fact] public void Missing_member_name_surfaces_placeholder() { // Header says 3 members but strings list has only UDT name + 2 member names. var memberCount = (ushort)3; var buf = new byte[12 + 8 * 3]; BinaryPrimitives.WriteUInt16LittleEndian(buf.AsSpan(0), memberCount); BinaryPrimitives.WriteUInt32LittleEndian(buf.AsSpan(4), 12); for (var i = 0; i < 3; i++) { var o = 12 + i * 8; BinaryPrimitives.WriteUInt16LittleEndian(buf.AsSpan(o), 0xC4); BinaryPrimitives.WriteUInt32LittleEndian(buf.AsSpan(o + 4), (uint)(i * 4)); } // strings: only UDT + 2 members, missing the third. var strings = Encoding.ASCII.GetBytes("MyUdt;\0A;\0B;\0"); var combined = buf.Concat(strings).ToArray(); var shape = CipTemplateObjectDecoder.Decode(combined); shape.ShouldNotBeNull(); shape.Members.Count.ShouldBe(3); shape.Members[2].Name.ShouldBe(""); } [Theory] [InlineData("Foo;\0Bar;\0", new[] { "Foo", "Bar" })] [InlineData("Foo;Bar;", new[] { "Foo", "Bar" })] // no nulls [InlineData("Only;\0", new[] { "Only" })] [InlineData(";\0", new string[] { })] // empty [InlineData("", new string[] { })] public void ParseSemicolonTerminatedStrings_handles_shapes(string input, string[] expected) { var bytes = Encoding.ASCII.GetBytes(input); var result = CipTemplateObjectDecoder.ParseSemicolonTerminatedStrings(bytes); result.ShouldBe(expected); } }