fe2a6db786
rust / build / test / clippy / fmt (push) Has been cancelled
Layout:
- src/ .NET 10 x64 reference: MxNativeCodec, MxNativeClient,
MxAsbClient, probes, tests, harnesses. Executable spec.
- design/ Architectural plan for the Rust port (M0–M6), error
model, protocol invariants, risks (R1–R16), adversarial
review log (review.md).
- rust/ Rust workspace. M0 skeleton + M1 codec parity.
mxaccess-codec: 215 unit tests + 2 cross-implementation
parity tests (byte-identical against .NET reference).
Other crates are M0 stubs awaiting M2+.
- captures/ Frida + netsh + pcap evidence per CLAUDE.md
("captures are evidence, not throwaway logs").
- analysis/ Decompiled C# (frida/proxy/decompiled-*),
Ghidra exports for native DLLs (`exports/` only —
working state at `projects/` and AVEVA's input
binaries at `input/` are gitignored).
- docs/ Reverse-engineering reference docs.
- tools/ Setup-LiveProbeEnv.ps1 (Infisical credential fetcher),
Compute-Crc.ps1 (.NET parity helper).
- .github/workflows/ Rust CI: fmt + build + test + clippy on Windows.
- LICENSE MIT (Joseph Doherty, 2026).
Verified:
- cargo test --workspace → 217 passed (215 unit + 2 .NET parity), 0 failed
- cargo clippy --workspace -- -D warnings → clean
- cargo fmt --all -- --check → clean
- cargo publish --dry-run -p mxaccess-codec → packages cleanly
Excluded from history (see .gitignore):
- **/bin, **/obj, **/target — build artifacts
- analysis/ghidra/projects/ — Ghidra working state (regenerable)
- analysis/ghidra/input/ — AVEVA proprietary DLLs (vendor IP)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
395 lines
18 KiB
C#
395 lines
18 KiB
C#
using System.Buffers.Binary;
|
|
using System.Globalization;
|
|
using System.Text;
|
|
|
|
namespace MxNativeCodec;
|
|
|
|
public static class NmxWriteMessage
|
|
{
|
|
public const byte Command = 0x37;
|
|
public const ushort Version = 1;
|
|
public const int HandleProjectionOffset = 3;
|
|
public const int HandleProjectionLength = 14;
|
|
public const int KindOffset = 17;
|
|
|
|
public static byte[] Encode(
|
|
MxReferenceHandle referenceHandle,
|
|
MxValueKind valueKind,
|
|
object value,
|
|
int writeIndex = 1,
|
|
uint clientToken = 0)
|
|
{
|
|
byte[] valueBytes = EncodeValue(valueKind, value);
|
|
byte[] body = valueKind switch
|
|
{
|
|
MxValueKind.Boolean => CreateBoolean(referenceHandle, valueKind, valueBytes, writeIndex, clientToken),
|
|
MxValueKind.Int32 or MxValueKind.Float32 => CreateFixed(referenceHandle, valueKind, valueBytes, writeIndex, clientToken),
|
|
MxValueKind.Float64 => CreateFixed(referenceHandle, valueKind, valueBytes, writeIndex, clientToken),
|
|
MxValueKind.String or MxValueKind.DateTime => CreateVariable(referenceHandle, valueKind, valueBytes, writeIndex, clientToken),
|
|
MxValueKind.BooleanArray or MxValueKind.Int32Array or MxValueKind.Float32Array or MxValueKind.Float64Array => CreateArray(referenceHandle, valueKind, valueBytes, GetArrayCount(value), GetArrayElementWidth(valueKind), writeIndex, clientToken),
|
|
MxValueKind.StringArray or MxValueKind.DateTimeArray => CreateArray(referenceHandle, valueKind, valueBytes, GetArrayCount(value), elementWidth: 4, writeIndex, clientToken),
|
|
_ => throw new ArgumentOutOfRangeException(nameof(valueKind), valueKind, null),
|
|
};
|
|
return body;
|
|
}
|
|
|
|
public static byte[] EncodeTimestamped(
|
|
MxReferenceHandle referenceHandle,
|
|
MxValueKind valueKind,
|
|
object value,
|
|
DateTime timestamp,
|
|
int writeIndex = 1,
|
|
uint clientToken = 0)
|
|
{
|
|
byte[] valueBytes = EncodeValue(valueKind, value);
|
|
byte[] body = valueKind switch
|
|
{
|
|
MxValueKind.Boolean => CreateBooleanTimestamped(referenceHandle, value, timestamp, writeIndex, clientToken),
|
|
MxValueKind.Int32 or MxValueKind.Float32 => CreateFixedTimestamped(referenceHandle, valueKind, valueBytes, timestamp, writeIndex, clientToken),
|
|
MxValueKind.Float64 => CreateFixedTimestamped(referenceHandle, valueKind, valueBytes, timestamp, writeIndex, clientToken),
|
|
MxValueKind.String or MxValueKind.DateTime => CreateVariableTimestamped(referenceHandle, valueKind, valueBytes, timestamp, writeIndex, clientToken),
|
|
MxValueKind.BooleanArray or MxValueKind.Int32Array or MxValueKind.Float32Array or MxValueKind.Float64Array => CreateArrayTimestamped(referenceHandle, valueKind, valueBytes, GetArrayCount(value), GetArrayElementWidth(valueKind), timestamp, writeIndex, clientToken),
|
|
MxValueKind.StringArray or MxValueKind.DateTimeArray => CreateArrayTimestamped(referenceHandle, valueKind, valueBytes, GetArrayCount(value), elementWidth: 4, timestamp, writeIndex, clientToken),
|
|
_ => throw new ArgumentOutOfRangeException(nameof(valueKind), valueKind, null),
|
|
};
|
|
return body;
|
|
}
|
|
|
|
public static MxValueKind GetValueKind(short mxDataType, bool isArray)
|
|
{
|
|
if (TryGetValueKind(mxDataType, isArray, out MxValueKind valueKind))
|
|
{
|
|
return valueKind;
|
|
}
|
|
|
|
throw new ArgumentOutOfRangeException(nameof(mxDataType), $"Unsupported MX data type {mxDataType} with isArray={isArray}.");
|
|
}
|
|
|
|
public static bool TryGetValueKind(short mxDataType, bool isArray, out MxValueKind valueKind)
|
|
{
|
|
return (mxDataType, isArray) switch
|
|
{
|
|
(1, false) => Return(MxValueKind.Boolean, out valueKind),
|
|
(2, false) => Return(MxValueKind.Int32, out valueKind),
|
|
(3, false) => Return(MxValueKind.Float32, out valueKind),
|
|
(4, false) => Return(MxValueKind.Float64, out valueKind),
|
|
(5, false) => Return(MxValueKind.String, out valueKind),
|
|
(6, false) => Return(MxValueKind.DateTime, out valueKind),
|
|
(1, true) => Return(MxValueKind.BooleanArray, out valueKind),
|
|
(2, true) => Return(MxValueKind.Int32Array, out valueKind),
|
|
(3, true) => Return(MxValueKind.Float32Array, out valueKind),
|
|
(4, true) => Return(MxValueKind.Float64Array, out valueKind),
|
|
(5, true) => Return(MxValueKind.StringArray, out valueKind),
|
|
(6, true) => Return(MxValueKind.DateTimeArray, out valueKind),
|
|
_ => Return(default, out valueKind, success: false),
|
|
};
|
|
}
|
|
|
|
private static bool Return(MxValueKind source, out MxValueKind target, bool success = true)
|
|
{
|
|
target = source;
|
|
return success;
|
|
}
|
|
|
|
public static byte GetWireKind(MxValueKind valueKind)
|
|
{
|
|
return valueKind switch
|
|
{
|
|
MxValueKind.Boolean => 0x01,
|
|
MxValueKind.Int32 => 0x02,
|
|
MxValueKind.Float32 => 0x03,
|
|
MxValueKind.Float64 => 0x04,
|
|
MxValueKind.String or MxValueKind.DateTime => 0x05,
|
|
MxValueKind.BooleanArray => 0x41,
|
|
MxValueKind.Int32Array => 0x42,
|
|
MxValueKind.Float32Array => 0x43,
|
|
MxValueKind.Float64Array => 0x44,
|
|
MxValueKind.StringArray or MxValueKind.DateTimeArray => 0x45,
|
|
_ => throw new ArgumentOutOfRangeException(nameof(valueKind), valueKind, null),
|
|
};
|
|
}
|
|
|
|
private static byte[] CreateFixed(MxReferenceHandle referenceHandle, MxValueKind valueKind, byte[] valueBytes, int writeIndex, uint clientToken)
|
|
{
|
|
byte[] body = new byte[KindOffset + 1 + valueBytes.Length + 14 + sizeof(int)];
|
|
WriteCommonPrefix(body, referenceHandle, valueKind);
|
|
valueBytes.CopyTo(body.AsSpan(KindOffset + 1));
|
|
WriteNormalSuffix(body.AsSpan(KindOffset + 1 + valueBytes.Length), writeIndex, clientToken);
|
|
return body;
|
|
}
|
|
|
|
private static byte[] CreateBoolean(MxReferenceHandle referenceHandle, MxValueKind valueKind, byte[] valueBytes, int writeIndex, uint clientToken)
|
|
{
|
|
byte[] body = new byte[KindOffset + 1 + valueBytes.Length + 11 + sizeof(int)];
|
|
WriteCommonPrefix(body, referenceHandle, valueKind);
|
|
valueBytes.CopyTo(body.AsSpan(KindOffset + 1));
|
|
WriteBooleanSuffix(body.AsSpan(KindOffset + 1 + valueBytes.Length), writeIndex, clientToken);
|
|
return body;
|
|
}
|
|
|
|
private static byte[] CreateBooleanTimestamped(MxReferenceHandle referenceHandle, object value, DateTime timestamp, int writeIndex, uint clientToken)
|
|
{
|
|
byte[] body = new byte[KindOffset + 1 + sizeof(byte) + 14 + sizeof(int)];
|
|
WriteCommonPrefix(body, referenceHandle, MxValueKind.Boolean);
|
|
body[KindOffset + 1] = Convert.ToBoolean(value, CultureInfo.InvariantCulture) ? (byte)0xff : (byte)0x00;
|
|
WriteTimestampedSuffix(body.AsSpan(KindOffset + 2), timestamp, writeIndex, clientToken);
|
|
return body;
|
|
}
|
|
|
|
private static byte[] CreateFixedTimestamped(MxReferenceHandle referenceHandle, MxValueKind valueKind, byte[] valueBytes, DateTime timestamp, int writeIndex, uint clientToken)
|
|
{
|
|
byte[] body = new byte[KindOffset + 1 + valueBytes.Length + 14 + sizeof(int)];
|
|
WriteCommonPrefix(body, referenceHandle, valueKind);
|
|
valueBytes.CopyTo(body.AsSpan(KindOffset + 1));
|
|
WriteTimestampedSuffix(body.AsSpan(KindOffset + 1 + valueBytes.Length), timestamp, writeIndex, clientToken);
|
|
return body;
|
|
}
|
|
|
|
private static byte[] CreateVariable(MxReferenceHandle referenceHandle, MxValueKind valueKind, byte[] valueBytes, int writeIndex, uint clientToken)
|
|
{
|
|
byte[] body = new byte[KindOffset + 1 + sizeof(int) + sizeof(int) + valueBytes.Length + 14 + sizeof(int)];
|
|
WriteCommonPrefix(body, referenceHandle, valueKind);
|
|
BinaryPrimitives.WriteInt32LittleEndian(body.AsSpan(18, sizeof(int)), valueBytes.Length + sizeof(int));
|
|
BinaryPrimitives.WriteInt32LittleEndian(body.AsSpan(22, sizeof(int)), valueBytes.Length);
|
|
valueBytes.CopyTo(body.AsSpan(26));
|
|
WriteNormalSuffix(body.AsSpan(26 + valueBytes.Length), writeIndex, clientToken);
|
|
return body;
|
|
}
|
|
|
|
private static byte[] CreateVariableTimestamped(MxReferenceHandle referenceHandle, MxValueKind valueKind, byte[] valueBytes, DateTime timestamp, int writeIndex, uint clientToken)
|
|
{
|
|
byte[] body = new byte[KindOffset + 1 + sizeof(int) + sizeof(int) + valueBytes.Length + 14 + sizeof(int)];
|
|
WriteCommonPrefix(body, referenceHandle, valueKind);
|
|
BinaryPrimitives.WriteInt32LittleEndian(body.AsSpan(18, sizeof(int)), valueBytes.Length + sizeof(int));
|
|
BinaryPrimitives.WriteInt32LittleEndian(body.AsSpan(22, sizeof(int)), valueBytes.Length);
|
|
valueBytes.CopyTo(body.AsSpan(26));
|
|
WriteTimestampedSuffix(body.AsSpan(26 + valueBytes.Length), timestamp, writeIndex, clientToken);
|
|
return body;
|
|
}
|
|
|
|
private static byte[] CreateArray(
|
|
MxReferenceHandle referenceHandle,
|
|
MxValueKind valueKind,
|
|
byte[] valueBytes,
|
|
int count,
|
|
ushort elementWidth,
|
|
int writeIndex,
|
|
uint clientToken)
|
|
{
|
|
byte[] body = new byte[KindOffset + 1 + 10 + valueBytes.Length + 14 + sizeof(int)];
|
|
WriteCommonPrefix(body, referenceHandle, valueKind);
|
|
BinaryPrimitives.WriteUInt16LittleEndian(body.AsSpan(22, sizeof(ushort)), checked((ushort)count));
|
|
BinaryPrimitives.WriteUInt16LittleEndian(body.AsSpan(24, sizeof(ushort)), elementWidth);
|
|
valueBytes.CopyTo(body.AsSpan(28));
|
|
WriteNormalSuffix(body.AsSpan(28 + valueBytes.Length), writeIndex, clientToken);
|
|
return body;
|
|
}
|
|
|
|
private static byte[] CreateArrayTimestamped(
|
|
MxReferenceHandle referenceHandle,
|
|
MxValueKind valueKind,
|
|
byte[] valueBytes,
|
|
int count,
|
|
ushort elementWidth,
|
|
DateTime timestamp,
|
|
int writeIndex,
|
|
uint clientToken)
|
|
{
|
|
byte[] body = new byte[KindOffset + 1 + 10 + valueBytes.Length + 14 + sizeof(int)];
|
|
WriteCommonPrefix(body, referenceHandle, valueKind);
|
|
BinaryPrimitives.WriteUInt16LittleEndian(body.AsSpan(22, sizeof(ushort)), checked((ushort)count));
|
|
BinaryPrimitives.WriteUInt16LittleEndian(body.AsSpan(24, sizeof(ushort)), elementWidth);
|
|
valueBytes.CopyTo(body.AsSpan(28));
|
|
WriteTimestampedSuffix(body.AsSpan(28 + valueBytes.Length), timestamp, writeIndex, clientToken);
|
|
return body;
|
|
}
|
|
|
|
private static void WriteCommonPrefix(byte[] body, MxReferenceHandle referenceHandle, MxValueKind valueKind)
|
|
{
|
|
body[0] = Command;
|
|
BinaryPrimitives.WriteUInt16LittleEndian(body.AsSpan(1, sizeof(ushort)), Version);
|
|
referenceHandle.Encode().AsSpan(6, HandleProjectionLength).CopyTo(body.AsSpan(HandleProjectionOffset, HandleProjectionLength));
|
|
body[KindOffset] = GetWireKind(valueKind);
|
|
}
|
|
|
|
private static void WriteNormalSuffix(Span<byte> suffixAndIndex, int writeIndex, uint clientToken)
|
|
{
|
|
if (suffixAndIndex.Length != 18)
|
|
{
|
|
throw new ArgumentException("Normal write suffix span must include the 14-byte suffix and 4-byte write index.", nameof(suffixAndIndex));
|
|
}
|
|
|
|
BinaryPrimitives.WriteInt16LittleEndian(suffixAndIndex[..sizeof(short)], -1);
|
|
BinaryPrimitives.WriteUInt64LittleEndian(suffixAndIndex.Slice(2, sizeof(ulong)), 0);
|
|
BinaryPrimitives.WriteUInt32LittleEndian(suffixAndIndex.Slice(10, sizeof(uint)), clientToken);
|
|
BinaryPrimitives.WriteInt32LittleEndian(suffixAndIndex.Slice(14, sizeof(int)), writeIndex);
|
|
}
|
|
|
|
private static void WriteBooleanSuffix(Span<byte> suffixAndIndex, int writeIndex, uint clientToken)
|
|
{
|
|
if (suffixAndIndex.Length != 15)
|
|
{
|
|
throw new ArgumentException("Boolean write suffix span must include the 11-byte suffix and 4-byte write index.", nameof(suffixAndIndex));
|
|
}
|
|
|
|
suffixAndIndex[..7].Clear();
|
|
BinaryPrimitives.WriteUInt32LittleEndian(suffixAndIndex.Slice(7, sizeof(uint)), clientToken);
|
|
BinaryPrimitives.WriteInt32LittleEndian(suffixAndIndex.Slice(11, sizeof(int)), writeIndex);
|
|
}
|
|
|
|
private static void WriteTimestampedSuffix(Span<byte> suffixAndIndex, DateTime timestamp, int writeIndex, uint clientToken)
|
|
{
|
|
if (suffixAndIndex.Length != 18)
|
|
{
|
|
throw new ArgumentException("Timestamped write suffix span must include the 14-byte suffix and 4-byte write index.", nameof(suffixAndIndex));
|
|
}
|
|
|
|
BinaryPrimitives.WriteInt16LittleEndian(suffixAndIndex[..sizeof(short)], 0);
|
|
BinaryPrimitives.WriteInt64LittleEndian(suffixAndIndex.Slice(2, sizeof(long)), timestamp.ToFileTime());
|
|
BinaryPrimitives.WriteUInt32LittleEndian(suffixAndIndex.Slice(10, sizeof(uint)), clientToken);
|
|
BinaryPrimitives.WriteInt32LittleEndian(suffixAndIndex.Slice(14, sizeof(int)), writeIndex);
|
|
}
|
|
|
|
private static byte[] EncodeValue(MxValueKind valueKind, object value)
|
|
{
|
|
return valueKind switch
|
|
{
|
|
MxValueKind.Boolean => Convert.ToBoolean(value, CultureInfo.InvariantCulture) ? [0xff, 0xff, 0xff, 0x00] : [0x00, 0xff, 0xff, 0x00],
|
|
MxValueKind.Int32 => EncodeInt32(Convert.ToInt32(value, CultureInfo.InvariantCulture)),
|
|
MxValueKind.Float32 => EncodeFloat32(Convert.ToSingle(value, CultureInfo.InvariantCulture)),
|
|
MxValueKind.Float64 => EncodeFloat64(Convert.ToDouble(value, CultureInfo.InvariantCulture)),
|
|
MxValueKind.String => EncodeUtf16String(Convert.ToString(value, CultureInfo.InvariantCulture) ?? string.Empty),
|
|
MxValueKind.DateTime => EncodeUtf16String(FormatDateTime((DateTime)value)),
|
|
MxValueKind.BooleanArray => EncodeBooleanArray((bool[])value),
|
|
MxValueKind.Int32Array => EncodeInt32Array((int[])value),
|
|
MxValueKind.Float32Array => EncodeFloat32Array((float[])value),
|
|
MxValueKind.Float64Array => EncodeFloat64Array((double[])value),
|
|
MxValueKind.StringArray => EncodeVariableArray(((string[])value).Select(static value => value ?? string.Empty)),
|
|
MxValueKind.DateTimeArray => EncodeVariableArray(((DateTime[])value).Select(FormatDateTime)),
|
|
_ => throw new ArgumentOutOfRangeException(nameof(valueKind), valueKind, null),
|
|
};
|
|
}
|
|
|
|
private static byte[] EncodeInt32(int value)
|
|
{
|
|
byte[] bytes = new byte[sizeof(int)];
|
|
BinaryPrimitives.WriteInt32LittleEndian(bytes, value);
|
|
return bytes;
|
|
}
|
|
|
|
private static byte[] EncodeFloat32(float value)
|
|
{
|
|
byte[] bytes = new byte[sizeof(float)];
|
|
BinaryPrimitives.WriteInt32LittleEndian(bytes, BitConverter.SingleToInt32Bits(value));
|
|
return bytes;
|
|
}
|
|
|
|
private static byte[] EncodeFloat64(double value)
|
|
{
|
|
byte[] bytes = new byte[sizeof(double)];
|
|
BinaryPrimitives.WriteInt64LittleEndian(bytes, BitConverter.DoubleToInt64Bits(value));
|
|
return bytes;
|
|
}
|
|
|
|
private static byte[] EncodeUtf16String(string value)
|
|
{
|
|
byte[] textBytes = Encoding.Unicode.GetBytes(value);
|
|
byte[] bytes = new byte[textBytes.Length + 2];
|
|
textBytes.CopyTo(bytes, 0);
|
|
return bytes;
|
|
}
|
|
|
|
private static byte[] EncodeBooleanArray(bool[] values)
|
|
{
|
|
byte[] bytes = new byte[values.Length * sizeof(short)];
|
|
for (int i = 0; i < values.Length; i++)
|
|
{
|
|
BinaryPrimitives.WriteInt16LittleEndian(bytes.AsSpan(i * sizeof(short), sizeof(short)), values[i] ? (short)-1 : (short)0);
|
|
}
|
|
|
|
return bytes;
|
|
}
|
|
|
|
private static byte[] EncodeInt32Array(int[] values)
|
|
{
|
|
byte[] bytes = new byte[values.Length * sizeof(int)];
|
|
for (int i = 0; i < values.Length; i++)
|
|
{
|
|
BinaryPrimitives.WriteInt32LittleEndian(bytes.AsSpan(i * sizeof(int), sizeof(int)), values[i]);
|
|
}
|
|
|
|
return bytes;
|
|
}
|
|
|
|
private static byte[] EncodeFloat32Array(float[] values)
|
|
{
|
|
byte[] bytes = new byte[values.Length * sizeof(float)];
|
|
for (int i = 0; i < values.Length; i++)
|
|
{
|
|
BinaryPrimitives.WriteInt32LittleEndian(bytes.AsSpan(i * sizeof(float), sizeof(float)), BitConverter.SingleToInt32Bits(values[i]));
|
|
}
|
|
|
|
return bytes;
|
|
}
|
|
|
|
private static byte[] EncodeFloat64Array(double[] values)
|
|
{
|
|
byte[] bytes = new byte[values.Length * sizeof(double)];
|
|
for (int i = 0; i < values.Length; i++)
|
|
{
|
|
BinaryPrimitives.WriteInt64LittleEndian(bytes.AsSpan(i * sizeof(double), sizeof(double)), BitConverter.DoubleToInt64Bits(values[i]));
|
|
}
|
|
|
|
return bytes;
|
|
}
|
|
|
|
private static byte[] EncodeVariableArray(IEnumerable<string> values)
|
|
{
|
|
using var stream = new MemoryStream();
|
|
foreach (string value in values)
|
|
{
|
|
byte[] textBytes = EncodeUtf16String(value);
|
|
byte[] header = new byte[13];
|
|
BinaryPrimitives.WriteInt32LittleEndian(header.AsSpan(0, 4), 1 + sizeof(int) + sizeof(int) + textBytes.Length);
|
|
header[4] = 0x05;
|
|
BinaryPrimitives.WriteInt32LittleEndian(header.AsSpan(5, 4), textBytes.Length + sizeof(int));
|
|
BinaryPrimitives.WriteInt32LittleEndian(header.AsSpan(9, 4), textBytes.Length);
|
|
stream.Write(header);
|
|
stream.Write(textBytes);
|
|
}
|
|
|
|
return stream.ToArray();
|
|
}
|
|
|
|
private static int GetArrayCount(object value)
|
|
{
|
|
return value switch
|
|
{
|
|
bool[] values => values.Length,
|
|
int[] values => values.Length,
|
|
float[] values => values.Length,
|
|
double[] values => values.Length,
|
|
string[] values => values.Length,
|
|
DateTime[] values => values.Length,
|
|
_ => throw new ArgumentException("Value is not a supported MX array.", nameof(value)),
|
|
};
|
|
}
|
|
|
|
private static ushort GetArrayElementWidth(MxValueKind valueKind)
|
|
{
|
|
return valueKind switch
|
|
{
|
|
MxValueKind.BooleanArray => sizeof(short),
|
|
MxValueKind.Int32Array => sizeof(int),
|
|
MxValueKind.Float32Array => sizeof(float),
|
|
MxValueKind.Float64Array => sizeof(double),
|
|
_ => throw new ArgumentOutOfRangeException(nameof(valueKind), valueKind, null),
|
|
};
|
|
}
|
|
|
|
private static string FormatDateTime(DateTime value)
|
|
{
|
|
return value.ToString("M/d/yyyy h:mm:ss tt", CultureInfo.InvariantCulture);
|
|
}
|
|
}
|