fix(abcip): decode real @udt template buffers; browse STRINGs as String, hide BOOL hosts
Live read-only check against production CompactLogix 5069-L320ERS2 rev 34.11 presses (Z-35162 / 10.209.70.160): every @udt/<id> template the controller returned decoded to null, so controller-discovered UDTs never fanned out. The decoder assumed a 12-byte header led by the member count and [info][array][offset] blocks with ';'-terminated names; libplctag actually returns a 14-byte header (u16 id, u32 def words, u32 instance size, u16 member count, u16 handle), [info][type][offset] blocks, and NUL-terminated names after a 'Name;<tail>' type name. All three test builders restated the old assumption, so the unit suite agreed with itself (the #468 trap again); they now share a TemplateBufferBuilder pinned by real captures (CipTemplateObjectWireCaptureTests). Discovery follow-ons that the working decoder exposed: - STRING (and user string types, LEN DINT + DATA SINT[n]) browse as one String variable, top-level and nested, not as a LEN/DATA folder. - Controller-generated members (ZZZZZZZZZZ* BOOL hosts, __* AOI backing) are not browsed. - A UDT array is not fanned out into element-free Tag.Member paths.
This commit is contained in:
+22
-59
@@ -10,45 +10,15 @@ namespace ZB.MOM.WW.OtOpcUa.Driver.AbCip.Tests;
|
||||
public sealed class CipTemplateObjectDecoderTests
|
||||
{
|
||||
/// <summary>
|
||||
/// Construct a Template Object blob — header + member blocks + semicolon-delimited
|
||||
/// strings (UDT name first, then member names).
|
||||
/// Construct a template buffer in libplctag's real layout (see <see cref="TemplateBufferBuilder"/>).
|
||||
/// <c>info</c> here is the member's CIP type word; <c>arraySize</c> > 0 marks an array member.
|
||||
/// </summary>
|
||||
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;
|
||||
}
|
||||
=> TemplateBufferBuilder.Build(udtName, instanceSize,
|
||||
[.. members.Select(m => (m.name, m.info, m.arraySize, m.offset))]);
|
||||
|
||||
/// <summary>Verifies that a simple UDT with two members decodes correctly.</summary>
|
||||
[Fact]
|
||||
@@ -161,8 +131,9 @@ public sealed class CipTemplateObjectDecoderTests
|
||||
[Fact]
|
||||
public void Zero_member_count_returns_null()
|
||||
{
|
||||
var buf = new byte[12];
|
||||
BinaryPrimitives.WriteUInt16LittleEndian(buf.AsSpan(0), 0);
|
||||
var buf = new byte[14];
|
||||
BinaryPrimitives.WriteUInt32LittleEndian(buf.AsSpan(6), 8); // instance size
|
||||
BinaryPrimitives.WriteUInt16LittleEndian(buf.AsSpan(10), 0); // member count
|
||||
CipTemplateObjectDecoder.Decode(buf).ShouldBeNull();
|
||||
}
|
||||
|
||||
@@ -178,19 +149,10 @@ public sealed class CipTemplateObjectDecoderTests
|
||||
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 full = TemplateBufferBuilder.Build("MyUdt", 12,
|
||||
("A", 0xC4, 0, 0), ("B", 0xC4, 0, 4), ("C", 0xC4, 0, 8));
|
||||
// Drop the trailing "C\0" so only two member names remain.
|
||||
var combined = full[..^2];
|
||||
|
||||
var shape = CipTemplateObjectDecoder.Decode(combined);
|
||||
shape.ShouldNotBeNull();
|
||||
@@ -198,19 +160,20 @@ public sealed class CipTemplateObjectDecoderTests
|
||||
shape.Members[2].Name.ShouldBe("<member_2>");
|
||||
}
|
||||
|
||||
/// <summary>Verifies that semicolon-terminated string parsing handles various input shapes.</summary>
|
||||
/// <param name="input">The raw semicolon-terminated string input.</param>
|
||||
/// <param name="expected">The expected array of parsed strings.</param>
|
||||
/// <summary>Verifies the template string section splits into the type name + NUL-terminated member names.</summary>
|
||||
/// <param name="input">The raw string section.</param>
|
||||
/// <param name="memberCount">Member names to read after the type name.</param>
|
||||
/// <param name="expected">The expected type name followed by member names.</param>
|
||||
[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)
|
||||
[InlineData("STRING;nEAIA\0LEN\0DATA\0", 2, new[] { "STRING", "LEN", "DATA" })]
|
||||
[InlineData("Udt;n\0A\0B\0\0\u0001junk", 2, new[] { "Udt", "A", "B" })] // trailing bytes ignored
|
||||
[InlineData("Udt\0A\0", 1, new[] { "Udt", "A" })] // no ';' tail
|
||||
[InlineData("Udt;nEA\0A\0", 3, new[] { "Udt", "A" })] // fewer names than members
|
||||
[InlineData("", 1, new string[] { })]
|
||||
public void ParseTemplateStrings_handles_shapes(string input, int memberCount, string[] expected)
|
||||
{
|
||||
var bytes = Encoding.ASCII.GetBytes(input);
|
||||
var result = CipTemplateObjectDecoder.ParseSemicolonTerminatedStrings(bytes);
|
||||
var result = CipTemplateObjectDecoder.ParseTemplateStrings(bytes, memberCount);
|
||||
result.ShouldBe(expected);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user