7892eb13a8
CIP codec (#564): PatchU16 bounds guard; CipReply content equality; CipPath port/length/ASCII/odd/oversize validation with ArgumentException for malformed tag names and routes; CipPath.MessageRouter/ConnectionManager are now ReadOnlySpan<byte>; refused Forward Open surfaces ForwardOpenRefusedException with structured general/extended status; test CaptureFile uses BinaryPrimitives. EncapsulationClient (#565): frame write bounded by the exchange timeout, which faults (never cancels) the connection; finite positive timeouts enforced at every entry point; gate wait bounded by the timeout; deterministic reorder of the 300 ms test; new dispose-during-drain, gate-wait and RegisterSession zero-context tests. Claude-Session: https://claude.ai/code/session_01Ef2ozxS7QPanGT8uoATbTe
115 lines
4.7 KiB
C#
115 lines
4.7 KiB
C#
using Shouldly;
|
|
using Xunit;
|
|
using ZB.MOM.WW.OtOpcUa.Driver.Ab.Transport.Cip;
|
|
namespace ZB.MOM.WW.OtOpcUa.Driver.Ab.Transport.Tests.Cip;
|
|
|
|
public sealed class CipPathTests
|
|
{
|
|
[Theory]
|
|
[InlineData("1,0", "0100")]
|
|
[InlineData("", "")]
|
|
[InlineData("1,0,2,10.0.0.5", "0100" + "1208" + "31302E302E302E35")]
|
|
[InlineData("2,10.0.0.55", "1209" + "31302E302E302E3535" + "00")]
|
|
[InlineData("15,3", "0F0F0003")]
|
|
[InlineData("16,10.0.0.5", "1F08" + "1000" + "31302E302E302E35")]
|
|
public void Encodes_route(string path, string hex) =>
|
|
Convert.ToHexString(CipPath.EncodeRoute(path)).ShouldBe(hex);
|
|
|
|
[Fact]
|
|
public void Odd_route_parts_throw() =>
|
|
Should.Throw<ArgumentException>(() => CipPath.EncodeRoute("1,0,2"));
|
|
|
|
[Theory]
|
|
[InlineData("TestDINT", "9108546573744449 4E54")]
|
|
[InlineData("Abc", "910341626300")]
|
|
[InlineData("Program:Main.Arr[3].Val", "910C50726F6772616D3A4D61696E" + "910341727200" + "2803" + "910356616C00")]
|
|
[InlineData("Big[300]", "910342696700" + "29002C01")]
|
|
[InlineData("Huge[70000]", "910448756765" + "2A00" + "70110100")]
|
|
[InlineData("Grid[1,2]", "910447726964" + "2801" + "2802")]
|
|
public void Encodes_symbolic(string name, string hex) =>
|
|
Convert.ToHexString(CipPath.EncodeSymbolic(name)).ShouldBe(hex.Replace(" ", ""));
|
|
|
|
[Fact]
|
|
public void Symbolic_rejects_bit_selector() =>
|
|
Should.Throw<ArgumentException>(() => CipPath.EncodeSymbolic("Tag.5"));
|
|
|
|
[Fact]
|
|
public void Splits_bit_selector()
|
|
{
|
|
CipPath.SplitBitSelector("Tag.5").ShouldBe(("Tag", 5));
|
|
CipPath.SplitBitSelector("Tag.Member").ShouldBe(("Tag.Member", null));
|
|
CipPath.SplitBitSelector("Tag").ShouldBe(("Tag", null));
|
|
}
|
|
|
|
[Theory]
|
|
[InlineData(2, 1u, "20022401")]
|
|
[InlineData(0x6B, 0x1234u, "206B25003412")]
|
|
[InlineData(0x300, 1u, "21000003" + "2401")]
|
|
[InlineData(2, 70000u, "2002" + "2600" + "70110100")]
|
|
public void Encodes_class_instance(ushort cls, uint inst, string hex) =>
|
|
Convert.ToHexString(CipPath.EncodeClassInstance(cls, inst)).ShouldBe(hex);
|
|
|
|
[Theory]
|
|
[InlineData("-1,0")] // negative port
|
|
[InlineData("65536,0")] // port does not fit a u16
|
|
[InlineData("x,0")] // non-numeric port
|
|
[InlineData("1,é.example")] // non-ASCII link
|
|
public void Malformed_route_throws_ArgumentException(string path) =>
|
|
Should.Throw<ArgumentException>(() => CipPath.EncodeRoute(path));
|
|
|
|
[Fact]
|
|
public void Route_link_over_255_characters_throws()
|
|
{
|
|
Should.Throw<ArgumentException>(() => CipPath.EncodeRoute("1," + new string('a', 256)));
|
|
CipPath.EncodeRoute("1," + new string('a', 255)).Length.ShouldBe(2 + 255 + 1);
|
|
}
|
|
|
|
[Fact]
|
|
public void Route_over_255_words_throws() =>
|
|
Should.Throw<ArgumentException>(() => CipPath.EncodeRoute(string.Join(",", Enumerable.Repeat("1,0", 256))));
|
|
|
|
[Theory]
|
|
[InlineData("")]
|
|
[InlineData("A..B")] // empty member
|
|
[InlineData(".A")]
|
|
[InlineData("A[1")] // unterminated subscript
|
|
[InlineData("A[x]")] // non-numeric subscript
|
|
[InlineData("A[-1]")]
|
|
[InlineData("A[]")]
|
|
[InlineData("A[1]B")] // junk after the subscript
|
|
[InlineData("Tä")] // non-ASCII
|
|
[InlineData("A[99999999999]")] // subscript overflows u32
|
|
[InlineData("Tag.99999999999")] // bit selector overflows int
|
|
public void Malformed_tag_names_throw_ArgumentException(string name) =>
|
|
Should.Throw<ArgumentException>(() => CipPath.EncodeSymbolic(name));
|
|
|
|
[Fact]
|
|
public void Tag_member_over_255_characters_throws()
|
|
{
|
|
Should.Throw<ArgumentException>(() => CipPath.EncodeSymbolic(new string('a', 256)));
|
|
CipPath.EncodeSymbolic(new string('a', 255)).Length.ShouldBe(2 + 255 + 1);
|
|
}
|
|
|
|
[Theory]
|
|
[InlineData(1)] // odd
|
|
[InlineData(3)]
|
|
[InlineData(512)] // 256 words: size byte would wrap
|
|
public void WithPathSize_rejects_odd_and_oversize_paths(int length) =>
|
|
Should.Throw<ArgumentException>(() => CipPath.WithPathSize(new byte[length]));
|
|
|
|
[Fact]
|
|
public void WithPathSize_accepts_the_255_word_maximum() =>
|
|
CipPath.WithPathSize(new byte[510])[0].ShouldBe((byte)255);
|
|
|
|
[Fact]
|
|
public void Well_known_paths_are_immutable_spans()
|
|
{
|
|
CipPath.MessageRouter.ToArray().ShouldBe(new byte[] { 0x20, 0x02, 0x24, 0x01 });
|
|
CipPath.ConnectionManager.ToArray().ShouldBe(new byte[] { 0x20, 0x06, 0x24, 0x01 });
|
|
}
|
|
|
|
[Fact]
|
|
public void Prefixes_path_size_in_words() =>
|
|
Convert.ToHexString(CipPath.WithPathSize([0x20, 0x02, 0x24, 0x01])).ShouldBe("0220022401");
|
|
}
|