11 Commits
1.7.5 ... 1.7.6

Author SHA1 Message Date
Matt Nadareski
b12d122721 Bump version 2025-10-07 09:28:17 -04:00
Matt Nadareski
20f1679557 Update Hashing to 1.5.1 2025-10-07 09:23:21 -04:00
Matt Nadareski
7ccedbeac5 Move Compare to better namespace 2025-09-30 21:25:56 -04:00
Matt Nadareski
72910cc1c0 Add AES/CTR encryption helpers 2025-09-30 19:33:28 -04:00
Matt Nadareski
8f4ea0da16 Add BouncyCastle as a dependency 2025-09-30 19:30:23 -04:00
Matt Nadareski
eb4975b261 Fix namespace in readme 2025-09-30 18:29:30 -04:00
Matt Nadareski
995c19d903 Add byte array math operations from NDecrypt; fix issues and add tests 2025-09-30 18:02:05 -04:00
Matt Nadareski
f0fe9af467 Require exact versions for build 2025-09-30 11:06:54 -04:00
Matt Nadareski
d33b47d15a Revert "Start allowing larger numeric types for reads"
This reverts commit e4a0a08d13.
2025-09-25 12:24:06 -04:00
Matt Nadareski
e4a0a08d13 Start allowing larger numeric types for reads 2025-09-25 12:08:22 -04:00
Matt Nadareski
24a69166f0 Add more info about namespaces to the readme 2025-09-25 09:43:38 -04:00
13 changed files with 417 additions and 10 deletions

View File

@@ -39,16 +39,40 @@ Various compression implementations that are used across multiple projects. Most
| [DotNetZip](https://github.com/DinoChiesa/DotNetZip) | BZip2 and DEFLATE implementations; minor edits have been made |
| [ZLibPort](https://github.com/Nanook/zlib-C-To-CSharp-Port) | Adds zlib code for internal and external use; minor edits have been made |
### `SabreTools.IO.Encryption`
Various encryption implementations that are used across multiple projects. Most of the implementations are be ports of existing C and C++ code.
#### Supported Encryption Schemes
| Encryption Scheme | Encrypt | Decrypt | Notes |
| --- | --- | --- | --- |
| AES/CTR | Yes | Yes | Subset of functionality exposed from [The Bouncy Castle Cryptography Library For .NET](https://github.com/bcgit/bc-csharp) |
| MoPaQ | No | Yes | Used to encrypt and decrypt MoPaQ tables for processing |
### `SabreTools.IO.Extensions`
Extensions for `BinaryReader`, `byte[]`, and `Stream` to help with reading and writing various data types. Some data types are locked behind .NET version support.
This namespace also contains other various extensions that help with common functionality and safe access.
### `SabreTools.IO.Interfaces`
Common interfaces used mainly internal to the library.
| Interface | Notes |
| --- | --- |
| `IMatch<T>` | Represents a matcher for a generic type |
| `IMatchSet<T, U>` | Represents a set of `IMatch<T>` types |
### `SabreTools.IO.Logging`
Logic for a logging system, including writing to console and textfile outputs. There are 4 possible log levels for logging statements to be invoked with. There is also a stopwatch implementation included for logging statements with automatic timespan tracking.
### `SabreTools.IO.Matching`
Classes designed to make matching contents and paths easier. These classes allow for both grouped and single matching as well as post-processing of matched information.
### `SabreTools.IO.Readers` and `SabreTools.IO.Writers`
Reading and writing support for the following file types:
@@ -63,8 +87,14 @@ For a generic INI implementation, see `SabreTools.IO.IniFile`.
Custom `Stream` implementations that are required for specialized use:
- `BufferedStream`: A format that is not a true stream implementation used for buffered, single-byte reads
- `ReadOnlyBitStream`: A readonly stream implementation allowing bitwise reading
- `ReadOnlyCompositeStream`: A readonly stream implementation that wraps multiple source streams in a set order
- `ViewStream`: A readonly stream implementation representing a view into source data
### `SabreTools.Text.Compare`
Classes focused on string comparison by natural sorting. For example, "5" would be sorted before "100".
## Releases

View File

@@ -1,6 +1,6 @@
using System;
using System.Linq;
using SabreTools.IO.Compare;
using SabreTools.Text.Compare;
using Xunit;
namespace SabreTools.IO.Test.Compare

View File

@@ -1,4 +1,4 @@
using SabreTools.IO.Compare;
using SabreTools.Text.Compare;
using Xunit;
namespace SabreTools.IO.Test.Compare

View File

@@ -1,6 +1,6 @@
using System;
using System.Linq;
using SabreTools.IO.Compare;
using SabreTools.Text.Compare;
using Xunit;
namespace SabreTools.IO.Test.Compare

View File

@@ -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]

View File

@@ -26,7 +26,7 @@
</PackageReference>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.14.1" />
<PackageReference Include="xunit" Version="2.9.3" />
<PackageReference Include="xunit.runner.visualstudio" Version="3.1.4">
<PackageReference Include="xunit.runner.visualstudio" Version="3.1.5">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>

View File

@@ -13,7 +13,7 @@ using System;
using System.Collections.Generic;
using System.Text.RegularExpressions;
namespace SabreTools.IO.Compare
namespace SabreTools.Text.Compare
{
public class NaturalComparer : Comparer<string>, IDisposable
{

View File

@@ -1,4 +1,4 @@
namespace SabreTools.IO.Compare
namespace SabreTools.Text.Compare
{
internal static class NaturalComparerUtil
{

View File

@@ -13,7 +13,7 @@ using System;
using System.Collections.Generic;
using System.Text.RegularExpressions;
namespace SabreTools.IO.Compare
namespace SabreTools.Text.Compare
{
public class NaturalReversedComparer : Comparer<string>, IDisposable
{

View File

@@ -0,0 +1,125 @@
using System;
using System.IO;
using Org.BouncyCastle.Crypto;
using Org.BouncyCastle.Crypto.Parameters;
using Org.BouncyCastle.Security;
using SabreTools.IO.Extensions;
namespace SabreTools.IO.Encryption
{
public static class AESCTR
{
/// <summary>
/// Create AES decryption cipher and intialize
/// </summary>
/// <param name="key">Byte array representation of 128-bit encryption key</param>
/// <param name="iv">AES initial value for counter</param>
/// <returns>Initialized AES cipher</returns>
public static IBufferedCipher CreateDecryptionCipher(byte[] key, byte[] iv)
{
if (key.Length != 16)
throw new ArgumentOutOfRangeException(nameof(key));
var keyParam = new KeyParameter(key);
var cipher = CipherUtilities.GetCipher("AES/CTR");
cipher.Init(forEncryption: false, new ParametersWithIV(keyParam, iv));
return cipher;
}
/// <summary>
/// Create AES encryption cipher and intialize
/// </summary>
/// <param name="key">Byte array representation of 128-bit encryption key</param>
/// <param name="iv">AES initial value for counter</param>
/// <returns>Initialized AES cipher</returns>
public static IBufferedCipher CreateEncryptionCipher(byte[] key, byte[] iv)
{
if (key.Length != 16)
throw new ArgumentOutOfRangeException(nameof(key));
var keyParam = new KeyParameter(key);
var cipher = CipherUtilities.GetCipher("AES/CTR");
cipher.Init(forEncryption: true, new ParametersWithIV(keyParam, iv));
return cipher;
}
/// <summary>
/// Perform an AES operation using an existing cipher
/// </summary>
public static void PerformOperation(uint size,
IBufferedCipher cipher,
Stream input,
Stream output,
Action<string>? progress = null)
{
// Get MiB-aligned block count and extra byte count
int blockCount = (int)((long)size / (1024 * 1024));
int extraBytes = (int)((long)size % (1024 * 1024));
// Process MiB-aligned data
if (blockCount > 0)
{
for (int i = 0; i < blockCount; i++)
{
byte[] readBytes = input.ReadBytes(1024 * 1024);
byte[] processedBytes = cipher.ProcessBytes(readBytes);
output.Write(processedBytes);
output.Flush();
progress?.Invoke($"{i} / {blockCount + 1} MB");
}
}
// Process additional data
if (extraBytes > 0)
{
byte[] readBytes = input.ReadBytes(extraBytes);
byte[] finalBytes = cipher.DoFinal(readBytes);
output.Write(finalBytes);
output.Flush();
}
progress?.Invoke($"{blockCount + 1} / {blockCount + 1} MB... Done!\r\n");
}
/// <summary>
/// Perform an AES operation using two existing ciphers
/// </summary>
public static void PerformOperation(uint size,
IBufferedCipher firstCipher,
IBufferedCipher secondCipher,
Stream input,
Stream output,
Action<string>? progress = null)
{
// Get MiB-aligned block count and extra byte count
int blockCount = (int)((long)size / (1024 * 1024));
int extraBytes = (int)((long)size % (1024 * 1024));
// Process MiB-aligned data
if (blockCount > 0)
{
for (int i = 0; i < blockCount; i++)
{
byte[] readBytes = input.ReadBytes(1024 * 1024);
byte[] firstProcessedBytes = firstCipher.ProcessBytes(readBytes);
byte[] secondProcessedBytes = secondCipher.ProcessBytes(firstProcessedBytes);
output.Write(secondProcessedBytes);
output.Flush();
progress?.Invoke($"{i} / {blockCount + 1} MB");
}
}
// Process additional data
if (extraBytes > 0)
{
byte[] readBytes = input.ReadBytes(extraBytes);
byte[] firstFinalBytes = firstCipher.DoFinal(readBytes);
byte[] secondFinalBytes = secondCipher.DoFinal(firstFinalBytes);
output.Write(secondFinalBytes);
output.Flush();
}
progress?.Invoke($"{blockCount + 1} / {blockCount + 1} MB... Done!\r\n");
}
}
}

View File

@@ -237,6 +237,176 @@ namespace SabreTools.IO.Extensions
#endregion
#region Math
/// <summary>
/// Add an integer value to a number represented by a byte array
/// </summary>
/// <param name="self">Byte array to add to</param>
/// <param name="add">Amount to add</param>
/// <returns>Byte array representing the new value</returns>
/// <remarks>Assumes array values are in big-endian format</remarks>
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);
}
/// <summary>
/// Add two numbers represented by byte arrays
/// </summary>
/// <param name="self">Byte array to add to</param>
/// <param name="add">Amount to add</param>
/// <returns>Byte array representing the new value</returns>
/// <remarks>Assumes array values are in big-endian format</remarks>
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;
}
/// <summary>
/// Perform a rotate left on a byte array
/// </summary>
/// <param name="self">Byte array value to rotate</param>
/// <param name="numBits">Number of bits to rotate</param>
/// <returns>Rotated byte array value</returns>
/// <remarks>Assumes array values are in big-endian format</remarks>
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;
}
/// <summary>
/// XOR two numbers represented by byte arrays
/// </summary>
/// <param name="self">Byte array to XOR to</param>
/// <param name="xor">Amount to XOR</param>
/// <returns>Byte array representing the new value</returns>
/// <remarks>Assumes array values are in big-endian format</remarks>
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
/// <summary>

View File

@@ -2,8 +2,8 @@ using System;
using System.Collections.Generic;
using System.IO;
using System.Text.RegularExpressions;
using SabreTools.IO.Compare;
using SabreTools.IO.Extensions;
using SabreTools.Text.Compare;
namespace SabreTools.IO
{

View File

@@ -11,7 +11,7 @@
<SuppressTfmSupportBuildWarnings>true</SuppressTfmSupportBuildWarnings>
<SymbolPackageFormat>snupkg</SymbolPackageFormat>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
<Version>1.7.5</Version>
<Version>1.7.6</Version>
<!-- Package Properties -->
<Authors>Matt Nadareski</Authors>
@@ -34,7 +34,9 @@
</ItemGroup>
<ItemGroup>
<PackageReference Include="SabreTools.Hashing" Version="1.5.0" />
<PackageReference Include="BouncyCastle.NetCore" Version="1.9.0" Condition="$(TargetFramework.StartsWith(`net2`)) OR $(TargetFramework.StartsWith(`net3`)) OR $(TargetFramework.StartsWith(`net40`))" />
<PackageReference Include="BouncyCastle.NetCore" Version="2.2.1" Condition="!$(TargetFramework.StartsWith(`net2`)) AND !$(TargetFramework.StartsWith(`net3`)) AND !$(TargetFramework.StartsWith(`net40`))" />
<PackageReference Include="SabreTools.Hashing" Version="[1.5.1]" />
</ItemGroup>
</Project>