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