diff --git a/NModbus.UnitTests/Extensions/ModbusMasterEnhancedFixture.cs b/NModbus.UnitTests/Extensions/ModbusMasterEnhancedFixture.cs new file mode 100644 index 0000000..96d96fc --- /dev/null +++ b/NModbus.UnitTests/Extensions/ModbusMasterEnhancedFixture.cs @@ -0,0 +1,100 @@ +using System; +using Moq; +using NModbus.Extensions; +using NModbus.Extensions.Functions; +using Xunit; + +namespace NModbus.UnitTests.Extensions +{ + /// + /// Regression coverage for issue #38: + /// (and the other Read*HoldingRegisters helpers) crashed with a confusing + /// "Destination array is not long enough" exception when the configured + /// wordSize was smaller than the requested numeric type. + /// + public class ModbusMasterEnhancedFixture + { + private static Mock BuildMaster(ushort[] response) + { + var master = new Mock(MockBehavior.Strict); + master + .Setup(m => m.ReadHoldingRegisters(It.IsAny(), It.IsAny(), It.IsAny())) + .Returns(response); + return master; + } + + [Fact] + public void ReadIntHoldingRegisters_WordSize16_ThrowsClearArgumentException() + { + // Arrange: wordSize=16 means a "value" is a single 16-bit register, + // which is too small to hold a 32-bit int. The library should reject + // the call with an actionable error rather than crashing inside + // BitConverter with "Destination array is not long enough" (issue #38). + var master = BuildMaster(new ushort[] { 0x1234 }); + var enhanced = new ModbusMasterEnhanced(master.Object, wordSize: 16); + + // Act + Assert + var ex = Assert.Throws(() => enhanced.ReadIntHoldingRegisters(1, 10, 1)); + Assert.Contains("wordSize", ex.Message, StringComparison.OrdinalIgnoreCase); + Assert.Contains("32", ex.Message); + } + + [Fact] + public void ReadUintHoldingRegisters_WordSize16_ThrowsClearArgumentException() + { + var master = BuildMaster(new ushort[] { 0x1234 }); + var enhanced = new ModbusMasterEnhanced(master.Object, wordSize: 16); + + var ex = Assert.Throws(() => enhanced.ReadUintHoldingRegisters(1, 10, 1)); + Assert.Contains("wordSize", ex.Message, StringComparison.OrdinalIgnoreCase); + } + + [Fact] + public void ReadFloatHoldingRegisters_WordSize16_ThrowsClearArgumentException() + { + var master = BuildMaster(new ushort[] { 0x1234 }); + var enhanced = new ModbusMasterEnhanced(master.Object, wordSize: 16); + + var ex = Assert.Throws(() => enhanced.ReadFloatHoldingRegisters(1, 10, 1)); + Assert.Contains("wordSize", ex.Message, StringComparison.OrdinalIgnoreCase); + } + + [Fact] + public void ReadShortHoldingRegisters_WordSize16_StillWorks() + { + // 16-bit values fit in a 16-bit word, so this should succeed and not regress. + var master = BuildMaster(new ushort[] { 0x1234 }); + var enhanced = new ModbusMasterEnhanced(master.Object, wordSize: 16); + + short[] result = enhanced.ReadShortHoldingRegisters(1, 10, 1); + Assert.Single(result); + Assert.Equal(unchecked((short)0x1234), result[0]); + } + + [Fact] + public void ReadIntHoldingRegisters_WordSize32_StillWorks() + { + // Sanity check that the wordSize=32 path is unchanged by the validation guard. + // We don't pin the exact byte order here, that's already covered by the + // existing extension tests and depends on host endianness. + var master = BuildMaster(new ushort[] { 0x1234, 0x5678 }); + var enhanced = new ModbusMasterEnhanced(master.Object, wordSize: 32); + + int[] result = enhanced.ReadIntHoldingRegisters(1, 10, 1); + Assert.Single(result); + } + + [Fact] + public void ByteValueArraysToInts_FrontPaddingWithSmallArray_ThrowsClearArgumentException() + { + // Direct coverage on the low-level helper: passing 2-byte arrays + // would previously throw ArgumentOutOfRangeException from BitConverter + // with "index" name and a confusing "Destination array is not long enough" + // chain. We now throw ArgumentException up front with a clearer message. + byte[][] tooSmall = { new byte[] { 0x12, 0x34 } }; + + var ex = Assert.Throws(() => RegisterFunctions.ByteValueArraysToInts(tooSmall)); + Assert.Contains("4 bytes", ex.Message); + } + } +} diff --git a/NModbus.UnitTests/NModbus.UnitTests.csproj b/NModbus.UnitTests/NModbus.UnitTests.csproj index 77fb2fa..223ec4f 100644 --- a/NModbus.UnitTests/NModbus.UnitTests.csproj +++ b/NModbus.UnitTests/NModbus.UnitTests.csproj @@ -1,12 +1,11 @@ - net4.6 + net4.6;net6.0;net8.0 true NModbus.UnitTests NModbus.UnitTests true - 1.0.4 false diff --git a/NModbus/Extensions/Functions/RegisterFunctions.cs b/NModbus/Extensions/Functions/RegisterFunctions.cs index aa71f58..1c19f6a 100644 --- a/NModbus/Extensions/Functions/RegisterFunctions.cs +++ b/NModbus/Extensions/Functions/RegisterFunctions.cs @@ -67,6 +67,7 @@ public static ushort[] ByteValueArraysToUShorts(byte[][] data, bool frontPadding public static int[] ByteValueArraysToInts(byte[][] data, bool frontPadding = true) { + RequireMinimumByteLength(data, 4, nameof(ByteValueArraysToInts)); return frontPadding ? data.Select(e => BitConverter.ToInt32(e, e.Length - 4)).ToArray() : data.Select(e => BitConverter.ToInt32(e, 0)).ToArray(); @@ -74,6 +75,7 @@ public static int[] ByteValueArraysToInts(byte[][] data, bool frontPadding = tru public static uint[] ByteValueArraysToUInts(byte[][] data, bool frontPadding = true) { + RequireMinimumByteLength(data, 4, nameof(ByteValueArraysToUInts)); return frontPadding ? data.Select(e => BitConverter.ToUInt32(e, e.Length - 4)).ToArray() : data.Select(e => BitConverter.ToUInt32(e, 0)).ToArray(); @@ -81,11 +83,32 @@ public static uint[] ByteValueArraysToUInts(byte[][] data, bool frontPadding = t public static float[] ByteValueArraysToFloats(byte[][] data, bool frontPadding = true) { + RequireMinimumByteLength(data, 4, nameof(ByteValueArraysToFloats)); return frontPadding ? data.Select(e => BitConverter.ToSingle(e, e.Length - 4)).ToArray() : data.Select(e => BitConverter.ToSingle(e, 0)).ToArray(); } + private static void RequireMinimumByteLength(byte[][] data, int minBytes, string operation) + { + // Issue #38: previously, supplying byte arrays narrower than the target + // numeric type leaked through to BitConverter and raised an opaque + // ArgumentOutOfRangeException about "startIndex". Validate up front so + // callers configuring ModbusMasterEnhanced with too small a wordSize + // see an actionable message instead. + if (data == null) throw new ArgumentNullException(nameof(data)); + for (int i = 0; i < data.Length; i++) + { + if (data[i] == null || data[i].Length < minBytes) + { + throw new ArgumentException( + $"{operation} requires each byte array to be at least {minBytes} bytes; element {i} has {data[i]?.Length ?? 0}. " + + "When using ModbusMasterEnhanced, set wordSize to at least the size of the value being read (e.g. 32 for int/uint/float).", + nameof(data)); + } + } + } + public static byte[][] CharsToByteValueArrays(char[] data, uint wordSize, bool frontPadding = true, bool singleCharPerRegister = true) {