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;
}
}