using System.Buffers.Binary; using System.Text; namespace ZB.MOM.WW.OtOpcUa.Driver.AbCip; /// /// Decoder for the CIP Template Object (class 0x6C) blob returned by a Read Template /// service. Produces an describing the UDT's name, total size, /// + ordered member list with per-member offset + type + array length. /// /// /// Wire format per Rockwell CIP Vol 1 §5A + Logix 5000 CIP Programming Manual /// 1756-PM019 §"Template Object", cross-checked against libplctag's ab/cip.c /// handle_read_template_reply: /// /// Header (fixed-size, little-endian): /// /// u16Member count. /// u16Struct handle (opaque id). /// u32Instance size — bytes per structure instance. /// u32Member-definition total size — not used here. /// /// /// Then member_count member blocks (8 bytes each): /// /// u16Member info — type code + flags (same encoding /// as Symbol Object: bit 15 = struct, lower 12 = CIP type code). /// u16Array size — 0 for scalar members. /// u32Struct offset — byte offset from struct start. /// /// /// Then strings: UDT name followed by each member name, each terminated by a /// semicolon ; followed by a null \0. The UDT name may itself contain the /// sequence UDTName;0\0 where 0 after the semicolon is an ASCII flag byte. /// Decoder trims to the first semicolon. /// public static class CipTemplateObjectDecoder { private const int HeaderSize = 12; // u16 + u16 + u32 + u32 private const int MemberBlockSize = 8; // u16 + u16 + u32 private const ushort MemberInfoStructFlag = 0x8000; private const ushort MemberInfoTypeCodeMask = 0x0FFF; /// /// Decode the raw Template Object blob. Returns null when the header indicates /// zero members or the buffer is too short to hold the fixed header. /// public static AbCipUdtShape? Decode(byte[] buffer) { ArgumentNullException.ThrowIfNull(buffer); if (buffer.Length < HeaderSize) return null; var memberCount = BinaryPrimitives.ReadUInt16LittleEndian(buffer.AsSpan(0)); // bytes 2-3: struct handle — opaque, not needed for the shape record var instanceSize = BinaryPrimitives.ReadUInt32LittleEndian(buffer.AsSpan(4)); // bytes 8-11: member-definition total size — inferred from names list instead if (memberCount == 0) return null; var memberBlocksOffset = HeaderSize; var namesOffset = memberBlocksOffset + MemberBlockSize * memberCount; if (namesOffset > buffer.Length) return null; var stringsSpan = buffer.AsSpan(namesOffset); var names = ParseSemicolonTerminatedStrings(stringsSpan); if (names.Count == 0) return null; // Strings layout: UDT name first, then one per member (in the same order as the // member-info blocks). Always consume the first entry as the UDT name; missing // trailing member names get placeholders below. var udtName = names[0]; var memberNames = names.Skip(1).ToArray(); var members = new List(memberCount); for (var i = 0; i < memberCount; i++) { var blockOffset = memberBlocksOffset + (i * MemberBlockSize); var info = BinaryPrimitives.ReadUInt16LittleEndian(buffer.AsSpan(blockOffset)); var arraySize = BinaryPrimitives.ReadUInt16LittleEndian(buffer.AsSpan(blockOffset + 2)); var offset = (int)BinaryPrimitives.ReadUInt32LittleEndian(buffer.AsSpan(blockOffset + 4)); var isStruct = (info & MemberInfoStructFlag) != 0; var typeCode = (byte)(info & MemberInfoTypeCodeMask); var dataType = isStruct ? AbCipDataType.Structure : (CipSymbolObjectDecoder.MapTypeCode(typeCode) ?? AbCipDataType.Structure); var memberName = i < memberNames.Length ? memberNames[i] : $""; members.Add(new AbCipUdtMember( Name: memberName, Offset: offset, DataType: dataType, ArrayLength: arraySize == 0 ? 1 : arraySize)); } return new AbCipUdtShape( TypeName: udtName, TotalSize: (int)instanceSize, Members: members); } /// /// Walk a span of NAME;\0NAME;\0… byte sequences. Splits at each semicolon — /// the null byte after each semicolon is optional padding per Rockwell's string /// encoding convention. Stops at a trailing null / end of buffer. /// internal static List ParseSemicolonTerminatedStrings(ReadOnlySpan span) { var result = new List(); var start = 0; for (var i = 0; i < span.Length; i++) { var b = span[i]; if (b == ';') { if (i > start) result.Add(Encoding.ASCII.GetString(span[start..i])); // Skip the optional null/space padding following the semicolon. while (i + 1 < span.Length && (span[i + 1] == '\0' || span[i + 1] == ' ')) i++; start = i + 1; } else if (b == 0 && start == i) { // Trailing null at a string boundary — done. break; } } // Trailing name without a semicolon (unlikely but observed on some firmwares). if (start < span.Length) { var zeroAt = span[start..].IndexOf((byte)0); var end = zeroAt < 0 ? span.Length : start + zeroAt; if (end > start) result.Add(Encoding.ASCII.GetString(span[start..end])); } return result; } }