using System.Buffers.Binary;
using System.Text;
namespace ZB.MOM.WW.OtOpcUa.Driver.S7;
///
/// Byte-level codecs for the four Siemens S7 string-shaped types: STRING, WSTRING,
/// CHAR, WCHAR. Pulled out of so the encoding rules are
/// unit-testable against golden byte vectors without standing up a Plc instance.
///
///
/// Wire formats (all big-endian, matching S7's native byte order):
///
/// -
/// STRING: 2-byte header (maxLen byte, actualLen byte) +
/// N ASCII bytes. Total slot size on the PLC = 2 + maxLen. Bytes past
/// actualLen are unspecified — the codec ignores them on read.
///
/// -
/// WSTRING: 4-byte header (maxLen UInt16 BE, actualLen
/// UInt16 BE) + N × 2 UTF-16BE bytes. Total slot size on the PLC =
/// 4 + 2 × maxLen.
///
/// -
/// CHAR: 1 ASCII byte.
///
/// -
/// WCHAR: 2 UTF-16BE bytes.
///
///
///
/// Header-bug clamp: certain S7 firmware revisions write
/// actualLen > maxLen (observed with NULL-padded buffers from older
/// CP-modules). On read the codec clamps the effective length so it never
/// walks past the wire buffer. On write the codec rejects the input
/// outright — silently truncating produces silent data loss.
///
///
public static class S7StringCodec
{
/// Buffer size for a STRING tag with the given declared .
public static int StringBufferSize(int maxLen) => 2 + maxLen;
/// Buffer size for a WSTRING tag with the given declared .
public static int WStringBufferSize(int maxLen) => 4 + (2 * maxLen);
///
/// Decode an S7 STRING wire buffer into a .NET string.
/// must be exactly 2 + maxLen long. actualLen is clamped to the
/// declared if the firmware reported an out-of-spec
/// value (header-bug tolerance).
///
public static string DecodeString(ReadOnlySpan bytes, int maxLen)
{
if (maxLen is < 1 or > 254)
throw new ArgumentOutOfRangeException(nameof(maxLen), maxLen, "S7 STRING max length must be 1-254");
var expected = StringBufferSize(maxLen);
if (bytes.Length != expected)
throw new InvalidDataException($"S7 STRING expected {expected} bytes, got {bytes.Length}");
// bytes[0] = declared max-length (advisory; we trust the caller-provided maxLen).
// bytes[1] = actual length. Clamp on read — firmware bug fallback.
int actual = bytes[1];
if (actual > maxLen) actual = maxLen;
if (actual == 0) return string.Empty;
return Encoding.ASCII.GetString(bytes.Slice(2, actual));
}
///
/// Encode a .NET string into an S7 STRING wire buffer of length
/// 2 + maxLen. ASCII only — non-ASCII characters are encoded as ?
/// by . Throws if is longer
/// than .
///
public static byte[] EncodeString(string value, int maxLen)
{
ArgumentNullException.ThrowIfNull(value);
if (maxLen is < 1 or > 254)
throw new ArgumentOutOfRangeException(nameof(maxLen), maxLen, "S7 STRING max length must be 1-254");
if (value.Length > maxLen)
throw new ArgumentException(
$"S7 STRING value of length {value.Length} exceeds declared max {maxLen}", nameof(value));
var buf = new byte[StringBufferSize(maxLen)];
buf[0] = (byte)maxLen;
buf[1] = (byte)value.Length;
Encoding.ASCII.GetBytes(value, 0, value.Length, buf, 2);
// Trailing bytes [2 + value.Length .. end] left as 0x00; S7 PLCs treat them as
// don't-care because actualLen bounds the readable region.
return buf;
}
///
/// Decode an S7 WSTRING wire buffer into a .NET string.
/// must be exactly 4 + 2 × maxLen long. actualLen is clamped to
/// on read.
///
public static string DecodeWString(ReadOnlySpan bytes, int maxLen)
{
if (maxLen < 1)
throw new ArgumentOutOfRangeException(nameof(maxLen), maxLen, "S7 WSTRING max length must be >= 1");
var expected = WStringBufferSize(maxLen);
if (bytes.Length != expected)
throw new InvalidDataException($"S7 WSTRING expected {expected} bytes, got {bytes.Length}");
// Header is two UInt16 BE: declared max-len and actual-len (both in characters).
int actual = BinaryPrimitives.ReadUInt16BigEndian(bytes.Slice(2, 2));
if (actual > maxLen) actual = maxLen;
if (actual == 0) return string.Empty;
return Encoding.BigEndianUnicode.GetString(bytes.Slice(4, actual * 2));
}
///
/// Encode a .NET string into an S7 WSTRING wire buffer of length
/// 4 + 2 × maxLen. Throws if has more than
/// UTF-16 code units.
///
public static byte[] EncodeWString(string value, int maxLen)
{
ArgumentNullException.ThrowIfNull(value);
if (maxLen < 1)
throw new ArgumentOutOfRangeException(nameof(maxLen), maxLen, "S7 WSTRING max length must be >= 1");
if (value.Length > maxLen)
throw new ArgumentException(
$"S7 WSTRING value of length {value.Length} exceeds declared max {maxLen}", nameof(value));
var buf = new byte[WStringBufferSize(maxLen)];
BinaryPrimitives.WriteUInt16BigEndian(buf.AsSpan(0, 2), (ushort)maxLen);
BinaryPrimitives.WriteUInt16BigEndian(buf.AsSpan(2, 2), (ushort)value.Length);
if (value.Length > 0)
Encoding.BigEndianUnicode.GetBytes(value, 0, value.Length, buf, 4);
return buf;
}
/// Decode a single S7 CHAR (one ASCII byte).
public static char DecodeChar(ReadOnlySpan bytes)
{
if (bytes.Length != 1)
throw new InvalidDataException($"S7 CHAR expected 1 byte, got {bytes.Length}");
return (char)bytes[0];
}
/// Encode a single ASCII char into an S7 CHAR (one byte). Non-ASCII rejected.
public static byte[] EncodeChar(char value)
{
if (value > 0x7F)
throw new ArgumentException($"S7 CHAR value '{value}' (U+{(int)value:X4}) is not ASCII", nameof(value));
return [(byte)value];
}
/// Decode a single S7 WCHAR (two bytes UTF-16 big-endian).
public static char DecodeWChar(ReadOnlySpan bytes)
{
if (bytes.Length != 2)
throw new InvalidDataException($"S7 WCHAR expected 2 bytes, got {bytes.Length}");
return (char)BinaryPrimitives.ReadUInt16BigEndian(bytes);
}
/// Encode a single char into an S7 WCHAR (two bytes UTF-16 big-endian).
public static byte[] EncodeWChar(char value)
{
var buf = new byte[2];
BinaryPrimitives.WriteUInt16BigEndian(buf, value);
return buf;
}
}