From 995c19d903c7354e89f7398e5ff4b6761482920b Mon Sep 17 00:00:00 2001 From: Matt Nadareski Date: Tue, 30 Sep 2025 18:02:05 -0400 Subject: [PATCH] Add byte array math operations from NDecrypt; fix issues and add tests --- .../Extensions/ByteArrayExtensionsTests.cs | 80 +++++++++ .../Extensions/ByteArrayExtensions.cs | 170 ++++++++++++++++++ 2 files changed, 250 insertions(+) diff --git a/SabreTools.IO.Test/Extensions/ByteArrayExtensionsTests.cs b/SabreTools.IO.Test/Extensions/ByteArrayExtensionsTests.cs index b742d9a..d7c5f0c 100644 --- a/SabreTools.IO.Test/Extensions/ByteArrayExtensionsTests.cs +++ b/SabreTools.IO.Test/Extensions/ByteArrayExtensionsTests.cs @@ -424,6 +424,86 @@ namespace SabreTools.IO.Test.Extensions #endregion + #region Add + + [Theory] + [InlineData(new byte[0], 0, new byte[0])] + [InlineData(new byte[0], 1234, new byte[] { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x04, 0xD2 })] + [InlineData(new byte[] { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x04, 0xD2 }, 0, new byte[] { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x04, 0xD2 })] + [InlineData(new byte[] { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x04, 0xD2 }, 1234, new byte[] { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x09, 0xA4 })] + public void Add_NumericInput(byte[] self, uint add, byte[] expected) + { + byte[] actual = self.Add(add); + + Assert.Equal(expected.Length, actual.Length); + if (actual.Length > 0) + Assert.True(actual.EqualsExactly(expected)); + } + + [Theory] + [InlineData(new byte[0], new byte[0], new byte[0])] + [InlineData(new byte[0], new byte[] { 0x04, 0xD2 }, new byte[] { 0x04, 0xD2 })] + [InlineData(new byte[] { 0x04, 0xD2 }, new byte[0], new byte[] { 0x04, 0xD2 })] + [InlineData(new byte[] { 0x04, 0xD2 }, new byte[] { 0x00, 0x00 }, new byte[] { 0x04, 0xD2 })] + [InlineData(new byte[] { 0x00, 0x00 }, new byte[] { 0x04, 0xD2 }, new byte[] { 0x04, 0xD2 })] + [InlineData(new byte[] { 0x04, 0xD2 }, new byte[] { 0x04, 0xD2 }, new byte[] { 0x09, 0xA4 })] + [InlineData(new byte[] { 0xAB, 0x04, 0xD2 }, new byte[] { 0x04, 0xD2 }, new byte[] { 0xAB, 0x09, 0xA4 })] + [InlineData(new byte[] { 0x04, 0xD2 }, new byte[] { 0xAB, 0x04, 0xD2 }, new byte[] { 0xAB, 0x09, 0xA4 })] + public void Add_ArrayInput(byte[] self, byte[] add, byte[] expected) + { + byte[] actual = self.Add(add); + + Assert.Equal(expected.Length, actual.Length); + if (actual.Length > 0) + Assert.True(actual.EqualsExactly(expected)); + } + + #endregion + + #region RotateLeft + + [Theory] + [InlineData(new byte[0], 0, new byte[0])] + [InlineData(new byte[] { 0x01 }, 0, new byte[] { 0x01 })] + [InlineData(new byte[] { 0x01 }, 1, new byte[] { 0x02 })] + [InlineData(new byte[] { 0x80 }, 1, new byte[] { 0x01 })] + [InlineData(new byte[] { 0x00, 0x01 }, 0, new byte[] { 0x00, 0x01 })] + [InlineData(new byte[] { 0x00, 0x01 }, 1, new byte[] { 0x00, 0x02 })] + [InlineData(new byte[] { 0x00, 0x80 }, 1, new byte[] { 0x01, 0x00 })] + [InlineData(new byte[] { 0x80, 0x00 }, 1, new byte[] { 0x00, 0x01 })] + public void RotateLeftTest(byte[] self, int numBits, byte[] expected) + { + byte[] actual = self.RotateLeft(numBits); + + Assert.Equal(expected.Length, actual.Length); + if (actual.Length > 0) + Assert.True(actual.EqualsExactly(expected)); + } + + #endregion + + #region Xor + + [Theory] + [InlineData(new byte[0], new byte[0], new byte[0])] + [InlineData(new byte[0], new byte[] { 0x04, 0xD2 }, new byte[] { 0x04, 0xD2 })] + [InlineData(new byte[] { 0x04, 0xD2 }, new byte[0], new byte[] { 0x04, 0xD2 })] + [InlineData(new byte[] { 0x04, 0xD2 }, new byte[] { 0x00, 0x00 }, new byte[] { 0x04, 0xD2 })] + [InlineData(new byte[] { 0x00, 0x00 }, new byte[] { 0x04, 0xD2 }, new byte[] { 0x04, 0xD2 })] + [InlineData(new byte[] { 0x04, 0xD2 }, new byte[] { 0x04, 0xD2 }, new byte[] { 0x00, 0x00 })] + [InlineData(new byte[] { 0xAB, 0x04, 0xD2 }, new byte[] { 0x04, 0xD2 }, new byte[] { 0xAB, 0x00, 0x00 })] + [InlineData(new byte[] { 0x04, 0xD2 }, new byte[] { 0xAB, 0x04, 0xD2 }, new byte[] { 0xAB, 0x00, 0x00 })] + public void XorTest(byte[] self, byte[] add, byte[] expected) + { + byte[] actual = self.Xor(add); + + Assert.Equal(expected.Length, actual.Length); + if (actual.Length > 0) + Assert.True(actual.EqualsExactly(expected)); + } + + #endregion + #region ToHexString [Fact] diff --git a/SabreTools.IO/Extensions/ByteArrayExtensions.cs b/SabreTools.IO/Extensions/ByteArrayExtensions.cs index 8de4f2a..3b3d89e 100644 --- a/SabreTools.IO/Extensions/ByteArrayExtensions.cs +++ b/SabreTools.IO/Extensions/ByteArrayExtensions.cs @@ -237,6 +237,176 @@ namespace SabreTools.IO.Extensions #endregion + #region Math + + /// + /// Add an integer value to a number represented by a byte array + /// + /// Byte array to add to + /// Amount to add + /// Byte array representing the new value + /// Assumes array values are in big-endian format + public static byte[] Add(this byte[] self, uint add) + { + // If nothing is being added, just return + if (add == 0) + return self; + + // Get the big-endian representation of the value + byte[] addBytes = BitConverter.GetBytes(add); + Array.Reverse(addBytes); + + // Pad the array out to 16 bytes + byte[] paddedBytes = new byte[16]; + Array.Copy(addBytes, 0, paddedBytes, 12, 4); + + // If the input is empty, just return the added value + if (self.Length == 0) + return paddedBytes; + + return self.Add(paddedBytes); + } + + /// + /// Add two numbers represented by byte arrays + /// + /// Byte array to add to + /// Amount to add + /// Byte array representing the new value + /// Assumes array values are in big-endian format + public static byte[] Add(this byte[] self, byte[] add) + { + // If either input is empty + if (self.Length == 0 && add.Length == 0) + return []; + else if (self.Length > 0 && add.Length == 0) + return self; + else if (self.Length == 0 && add.Length > 0) + return add; + + // Setup the output array + int outLength = Math.Max(self.Length, add.Length); + byte[] output = new byte[outLength]; + + // Loop adding with carry + uint carry = 0; + for (int i = 0; i < outLength; i++) + { + int selfIndex = self.Length - i - 1; + uint selfValue = selfIndex >= 0 ? self[selfIndex] : 0u; + + int addIndex = add.Length - i - 1; + uint addValue = addIndex >= 0 ? add[addIndex] : 0u; + + uint next = selfValue + addValue + carry; + carry = next >> 8; + + int outputIndex = output.Length - i - 1; + output[outputIndex] = (byte)(next & 0xFF); + } + + return output; + } + + /// + /// Perform a rotate left on a byte array + /// + /// Byte array value to rotate + /// Number of bits to rotate + /// Rotated byte array value + /// Assumes array values are in big-endian format + public static byte[] RotateLeft(this byte[] self, int numBits) + { + // If either input is empty + if (self.Length == 0) + return []; + else if (numBits == 0) + return self; + + byte[] output = new byte[self.Length]; + Array.Copy(self, output, output.Length); + + // Shift by bytes + while (numBits >= 8) + { + byte temp = output[0]; + for (int i = 0; i < output.Length - 1; i++) + { + output[i] = output[i + 1]; + } + + output[output.Length - 1] = temp; + numBits -= 8; + } + + // Shift by bits + if (numBits > 0) + { + byte bitMask = (byte)(8 - numBits), carry, wrap = 0; + for (int i = 0; i < output.Length; i++) + { + carry = (byte)((255 << bitMask & output[i]) >> bitMask); + + // Make sure the first byte carries to the end + if (i == 0) + wrap = carry; + + // Otherwise, move to the last byte + else + output[i - 1] |= carry; + + // Shift the current bits + output[i] <<= numBits; + } + + // Make sure the wrap happens + output[output.Length - 1] |= wrap; + } + + return output; + } + + /// + /// XOR two numbers represented by byte arrays + /// + /// Byte array to XOR to + /// Amount to XOR + /// Byte array representing the new value + /// Assumes array values are in big-endian format + public static byte[] Xor(this byte[] self, byte[] xor) + { + // If either input is empty + if (self.Length == 0 && xor.Length == 0) + return []; + else if (self.Length > 0 && xor.Length == 0) + return self; + else if (self.Length == 0 && xor.Length > 0) + return xor; + + // Setup the output array + int outLength = Math.Max(self.Length, xor.Length); + byte[] output = new byte[outLength]; + + // Loop XOR + for (int i = 0; i < outLength; i++) + { + int selfIndex = self.Length - i - 1; + uint selfValue = selfIndex >= 0 ? self[selfIndex] : 0u; + + int xorIndex = xor.Length - i - 1; + uint xorValue = xorIndex >= 0 ? xor[xorIndex] : 0u; + + uint next = selfValue ^ xorValue; + + int outputIndex = output.Length - i - 1; + output[outputIndex] = (byte)(next & 0xFF); + } + + return output; + } + + #endregion + #region Strings ///