Closes the NotSupportedException cliff for S7 string-shaped types.
- S7DataType gains WString, Char, WChar members alongside the existing
String entry.
- New S7StringCodec encodes/decodes the four wire formats:
STRING : 2-byte header (max-len + actual-len bytes) + N ASCII bytes
-> total 2 + max_len.
WSTRING : 4-byte header (max-len + actual-len UInt16 BE) + N×2
UTF-16BE bytes -> total 4 + 2 × max_len.
CHAR : 1 ASCII byte (rejects non-ASCII on encode).
WCHAR : 2 UTF-16BE bytes.
Header-bug clamp: actualLen > maxLen is silently clamped on read so
firmware quirks don't walk past the wire buffer; rejected on write
to avoid silent truncation.
- S7Driver.ReadOneAsync / WriteOneAsync issue ReadBytesAsync /
WriteBytesAsync against the parsed Area / DbNumber / ByteOffset and
honour S7TagDefinition.StringLength (default 254 = S7 STRING max).
- MapDataType returns DriverDataType.String for the three new enum
members so OPC UA discovery surfaces them as scalar strings.
Tests: 21 new cases on S7StringCodec covering golden-byte vectors,
encode/decode round-trips, the firmware-bug header-clamp, ASCII-only
guard on CHAR, and the StringLength default. 85/85 passing.
Closes #288
229 lines
7.4 KiB
C#
229 lines
7.4 KiB
C#
using Shouldly;
|
||
using Xunit;
|
||
|
||
namespace ZB.MOM.WW.OtOpcUa.Driver.S7.Tests;
|
||
|
||
/// <summary>
|
||
/// Golden-byte unit tests for <see cref="S7StringCodec"/>: STRING / WSTRING / CHAR /
|
||
/// WCHAR encode + decode round-trips and the firmware-bug header-clamp on read.
|
||
/// These tests intentionally don't touch S7.Net — the codec operates on raw byte
|
||
/// spans so reproducing the wire format here is sufficient to lock the contract.
|
||
/// </summary>
|
||
[Trait("Category", "Unit")]
|
||
public sealed class S7StringCodecTests
|
||
{
|
||
// -------- STRING --------
|
||
|
||
[Fact]
|
||
public void EncodeString_emits_two_byte_header_and_ascii_payload()
|
||
{
|
||
var bytes = S7StringCodec.EncodeString("HELLO", maxLen: 10);
|
||
|
||
bytes.Length.ShouldBe(2 + 10); // 2-byte header + max-len slot
|
||
bytes[0].ShouldBe<byte>(10); // declared max
|
||
bytes[1].ShouldBe<byte>(5); // actual length
|
||
// ASCII payload
|
||
bytes[2].ShouldBe<byte>((byte)'H');
|
||
bytes[3].ShouldBe<byte>((byte)'E');
|
||
bytes[4].ShouldBe<byte>((byte)'L');
|
||
bytes[5].ShouldBe<byte>((byte)'L');
|
||
bytes[6].ShouldBe<byte>((byte)'O');
|
||
// Padding bytes left as 0x00.
|
||
for (var i = 7; i < bytes.Length; i++) bytes[i].ShouldBe<byte>(0);
|
||
}
|
||
|
||
[Fact]
|
||
public void DecodeString_round_trips_encode()
|
||
{
|
||
var bytes = S7StringCodec.EncodeString("ABC", maxLen: 16);
|
||
var decoded = S7StringCodec.DecodeString(bytes, maxLen: 16);
|
||
decoded.ShouldBe("ABC");
|
||
}
|
||
|
||
[Fact]
|
||
public void DecodeString_clamps_when_actualLen_exceeds_maxLen_firmware_bug()
|
||
{
|
||
// Hand-craft a buffer where actualLen (255) > maxLen (10). Real firmware bug
|
||
// observed on legacy CP modules. Codec must clamp to maxLen rather than walk
|
||
// off the end of the wire buffer.
|
||
var max = 10;
|
||
var buf = new byte[2 + max];
|
||
buf[0] = (byte)max;
|
||
buf[1] = 255; // out-of-spec actual
|
||
for (var i = 0; i < max; i++) buf[2 + i] = (byte)('A' + i);
|
||
|
||
var s = S7StringCodec.DecodeString(buf, max);
|
||
s.Length.ShouldBe(max);
|
||
s.ShouldBe("ABCDEFGHIJ");
|
||
}
|
||
|
||
[Fact]
|
||
public void DecodeString_empty_actual_len_returns_empty_string()
|
||
{
|
||
var buf = new byte[2 + 8];
|
||
buf[0] = 8;
|
||
buf[1] = 0;
|
||
S7StringCodec.DecodeString(buf, 8).ShouldBe(string.Empty);
|
||
}
|
||
|
||
[Fact]
|
||
public void EncodeString_rejects_value_longer_than_maxLen()
|
||
{
|
||
Should.Throw<ArgumentException>(() => S7StringCodec.EncodeString("TOO-LONG", maxLen: 4));
|
||
}
|
||
|
||
[Fact]
|
||
public void DecodeString_rejects_wrong_length_buffer()
|
||
{
|
||
// 2 + 5 expected, give 3 — must throw rather than silently read.
|
||
var buf = new byte[3];
|
||
Should.Throw<System.IO.InvalidDataException>(() => S7StringCodec.DecodeString(buf, 5));
|
||
}
|
||
|
||
// -------- WSTRING --------
|
||
|
||
[Fact]
|
||
public void EncodeWString_emits_four_byte_header_and_utf16be_payload()
|
||
{
|
||
// "Hi" -> H = 0x0048, i = 0x0069. UTF-16 BE wire bytes 00 48 00 69.
|
||
var bytes = S7StringCodec.EncodeWString("Hi", maxLen: 4);
|
||
|
||
bytes.Length.ShouldBe(4 + 2 * 4); // 4-byte header + 2 × max-len bytes
|
||
bytes[0].ShouldBe<byte>(0x00); // maxLen high
|
||
bytes[1].ShouldBe<byte>(0x04); // maxLen low
|
||
bytes[2].ShouldBe<byte>(0x00); // actualLen high
|
||
bytes[3].ShouldBe<byte>(0x02); // actualLen low
|
||
bytes[4].ShouldBe<byte>(0x00); // 'H' high (BE)
|
||
bytes[5].ShouldBe<byte>(0x48); // 'H' low
|
||
bytes[6].ShouldBe<byte>(0x00); // 'i' high
|
||
bytes[7].ShouldBe<byte>(0x69); // 'i' low
|
||
// Padding bytes [8..11] left as 0x00.
|
||
bytes[8].ShouldBe<byte>(0);
|
||
bytes[9].ShouldBe<byte>(0);
|
||
bytes[10].ShouldBe<byte>(0);
|
||
bytes[11].ShouldBe<byte>(0);
|
||
}
|
||
|
||
[Fact]
|
||
public void DecodeWString_round_trips_unicode()
|
||
{
|
||
// U+00E9 (é) — non-ASCII, exercises the BE encoding.
|
||
var input = "café";
|
||
var bytes = S7StringCodec.EncodeWString(input, maxLen: 8);
|
||
var decoded = S7StringCodec.DecodeWString(bytes, maxLen: 8);
|
||
decoded.ShouldBe(input);
|
||
}
|
||
|
||
[Fact]
|
||
public void DecodeWString_clamps_when_actualLen_exceeds_maxLen_firmware_bug()
|
||
{
|
||
var max = 4;
|
||
var buf = new byte[4 + 2 * max];
|
||
// Header: max=4, actual=0xFFFF (firmware-bug).
|
||
buf[0] = 0x00; buf[1] = (byte)max;
|
||
buf[2] = 0xFF; buf[3] = 0xFF;
|
||
// Payload: 'A','B','C','D' (BE).
|
||
buf[4] = 0x00; buf[5] = (byte)'A';
|
||
buf[6] = 0x00; buf[7] = (byte)'B';
|
||
buf[8] = 0x00; buf[9] = (byte)'C';
|
||
buf[10] = 0x00; buf[11] = (byte)'D';
|
||
|
||
var s = S7StringCodec.DecodeWString(buf, max);
|
||
s.ShouldBe("ABCD"); // clamped to maxLen × 2 bytes
|
||
}
|
||
|
||
[Fact]
|
||
public void EncodeWString_rejects_value_longer_than_maxLen()
|
||
{
|
||
Should.Throw<ArgumentException>(() => S7StringCodec.EncodeWString("TOO-LONG", maxLen: 4));
|
||
}
|
||
|
||
[Fact]
|
||
public void DecodeWString_rejects_wrong_length_buffer()
|
||
{
|
||
Should.Throw<System.IO.InvalidDataException>(() => S7StringCodec.DecodeWString(new byte[5], maxLen: 4));
|
||
}
|
||
|
||
// -------- CHAR --------
|
||
|
||
[Fact]
|
||
public void EncodeChar_emits_single_ascii_byte()
|
||
{
|
||
var b = S7StringCodec.EncodeChar('A');
|
||
b.Length.ShouldBe(1);
|
||
b[0].ShouldBe<byte>(0x41);
|
||
}
|
||
|
||
[Fact]
|
||
public void DecodeChar_round_trips()
|
||
{
|
||
S7StringCodec.DecodeChar(new byte[] { 0x5A }).ShouldBe('Z');
|
||
}
|
||
|
||
[Fact]
|
||
public void EncodeChar_rejects_non_ascii()
|
||
{
|
||
Should.Throw<ArgumentException>(() => S7StringCodec.EncodeChar('é'));
|
||
}
|
||
|
||
[Fact]
|
||
public void DecodeChar_rejects_wrong_length()
|
||
{
|
||
Should.Throw<System.IO.InvalidDataException>(() => S7StringCodec.DecodeChar(new byte[2]));
|
||
}
|
||
|
||
// -------- WCHAR --------
|
||
|
||
[Fact]
|
||
public void EncodeWChar_emits_two_bytes_big_endian()
|
||
{
|
||
var b = S7StringCodec.EncodeWChar('Z');
|
||
b.Length.ShouldBe(2);
|
||
b[0].ShouldBe<byte>(0x00);
|
||
b[1].ShouldBe<byte>(0x5A);
|
||
}
|
||
|
||
[Fact]
|
||
public void EncodeWChar_handles_unicode_codepoint()
|
||
{
|
||
// U+00E9 (é) -> 00 E9 BE
|
||
var b = S7StringCodec.EncodeWChar('é');
|
||
b[0].ShouldBe<byte>(0x00);
|
||
b[1].ShouldBe<byte>(0xE9);
|
||
}
|
||
|
||
[Fact]
|
||
public void DecodeWChar_round_trips()
|
||
{
|
||
S7StringCodec.DecodeWChar(new byte[] { 0x00, 0x5A }).ShouldBe('Z');
|
||
S7StringCodec.DecodeWChar(new byte[] { 0x00, 0xE9 }).ShouldBe('é');
|
||
}
|
||
|
||
[Fact]
|
||
public void DecodeWChar_rejects_wrong_length()
|
||
{
|
||
Should.Throw<System.IO.InvalidDataException>(() => S7StringCodec.DecodeWChar(new byte[1]));
|
||
Should.Throw<System.IO.InvalidDataException>(() => S7StringCodec.DecodeWChar(new byte[3]));
|
||
}
|
||
|
||
// -------- StringLength default + range --------
|
||
|
||
[Fact]
|
||
public void EncodeString_default_max_length_254_round_trips()
|
||
{
|
||
// Default S7TagDefinition.StringLength is 254; codec must accept that.
|
||
var s = new string('x', 100);
|
||
var bytes = S7StringCodec.EncodeString(s, 254);
|
||
bytes.Length.ShouldBe(2 + 254);
|
||
bytes[0].ShouldBe<byte>(254);
|
||
bytes[1].ShouldBe<byte>(100);
|
||
S7StringCodec.DecodeString(bytes, 254).ShouldBe(s);
|
||
}
|
||
|
||
[Fact]
|
||
public void EncodeString_rejects_max_length_above_254()
|
||
{
|
||
Should.Throw<ArgumentOutOfRangeException>(() => S7StringCodec.EncodeString("x", maxLen: 255));
|
||
}
|
||
}
|