14 Commits
1.7.4 ... 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
Matt Nadareski
6c13cdcf31 Bump version 2025-09-24 08:07:49 -04:00
Matt Nadareski
4138c271e5 This should be internal to the Compare namespace 2025-09-23 11:06:40 -04:00
Matt Nadareski
f80d31597b Reintegrate Matching and reorganize as needed 2025-09-23 10:59:01 -04:00
36 changed files with 4014 additions and 69 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

@@ -0,0 +1,38 @@
using System;
using System.Linq;
using SabreTools.Text.Compare;
using Xunit;
namespace SabreTools.IO.Test.Compare
{
public class NaturalComparerTests
{
[Fact]
public void ListSort_Numeric()
{
// Setup arrays
string[] sortable = ["0", "100", "5", "2", "1000"];
string[] expected = ["0", "2", "5", "100", "1000"];
// Run sorting on array
Array.Sort(sortable, new NaturalComparer());
// Check the output
Assert.True(sortable.SequenceEqual(expected));
}
[Fact]
public void ListSort_Mixed()
{
// Setup arrays
string[] sortable = ["b3b", "c", "b", "a", "a1"];
string[] expected = ["a", "a1", "b", "b3b", "c"];
// Run sorting on array
Array.Sort(sortable, new NaturalComparer());
// Check the output
Assert.True(sortable.SequenceEqual(expected));
}
}
}

View File

@@ -0,0 +1,66 @@
using SabreTools.Text.Compare;
using Xunit;
namespace SabreTools.IO.Test.Compare
{
public class NaturalComparerUtilTests
{
[Fact]
public void CompareNumeric_BothNull_Equal()
{
int actual = NaturalComparerUtil.ComparePaths(null, null);
Assert.Equal(0, actual);
}
[Fact]
public void CompareNumeric_SingleNull_Ordered()
{
int actual = NaturalComparerUtil.ComparePaths(null, "notnull");
Assert.Equal(-1, actual);
actual = NaturalComparerUtil.ComparePaths("notnull", null);
Assert.Equal(1, actual);
}
[Fact]
public void CompareNumeric_BothEqual_Equal()
{
int actual = NaturalComparerUtil.ComparePaths("notnull", "notnull");
Assert.Equal(0, actual);
}
[Fact]
public void CompareNumeric_BothEqualWithPath_Equal()
{
int actual = NaturalComparerUtil.ComparePaths("notnull/file.ext", "notnull/file.ext");
Assert.Equal(0, actual);
}
[Fact]
public void CompareNumeric_BothEqualWithAltPath_Equal()
{
int actual = NaturalComparerUtil.ComparePaths("notnull/file.ext", "notnull\\file.ext");
Assert.Equal(0, actual);
}
[Fact]
public void CompareNumeric_NumericNonDecimalString_Ordered()
{
int actual = NaturalComparerUtil.ComparePaths("100", "10");
Assert.Equal(1, actual);
actual = NaturalComparerUtil.ComparePaths("10", "100");
Assert.Equal(-1, actual);
}
[Fact]
public void CompareNumeric_NumericDecimalString_Ordered()
{
int actual = NaturalComparerUtil.ComparePaths("100.100", "100.10");
Assert.Equal(1, actual);
actual = NaturalComparerUtil.ComparePaths("100.10", "100.100");
Assert.Equal(-1, actual);
}
}
}

View File

@@ -0,0 +1,38 @@
using System;
using System.Linq;
using SabreTools.Text.Compare;
using Xunit;
namespace SabreTools.IO.Test.Compare
{
public class NaturalReversedComparerTests
{
[Fact]
public void ListSort_Numeric()
{
// Setup arrays
string[] sortable = ["0", "100", "5", "2", "1000"];
string[] expected = ["1000", "100", "5", "2", "0"];
// Run sorting on array
Array.Sort(sortable, new NaturalReversedComparer());
// Check the output
Assert.True(sortable.SequenceEqual(expected));
}
[Fact]
public void ListSort_Mixed()
{
// Setup arrays
string[] sortable = ["b3b", "c", "b", "a", "a1"];
string[] expected = ["c", "b3b", "b", "a1", "a"];
// Run sorting on array
Array.Sort(sortable, new NaturalReversedComparer());
// Check the output
Assert.True(sortable.SequenceEqual(expected));
}
}
}

View File

@@ -1,9 +1,7 @@
using System;
using System.IO;
using System.Linq;
#if NET7_0_OR_GREATER
using System.Numerics;
#endif
using System.Text;
using SabreTools.IO.Extensions;
using Xunit;
@@ -226,7 +224,6 @@ namespace SabreTools.IO.Test.Extensions
Assert.Equal(0x0100, read);
}
#if NET6_0_OR_GREATER
[Fact]
public void ReadHalfTest()
{
@@ -246,7 +243,6 @@ namespace SabreTools.IO.Test.Extensions
Half read = br.ReadHalfBigEndian();
Assert.Equal(expected, read);
}
#endif
[Fact]
public void ReadInt24Test()
@@ -598,7 +594,6 @@ namespace SabreTools.IO.Test.Extensions
Assert.Equal(expected, read);
}
#if NET7_0_OR_GREATER
[Fact]
public void ReadInt128Test()
{
@@ -640,7 +635,6 @@ namespace SabreTools.IO.Test.Extensions
UInt128 read = br.ReadUInt128BigEndian();
Assert.Equal(expected, read);
}
#endif
[Fact]
public void ReadNullTerminatedStringTest()
@@ -691,16 +685,13 @@ namespace SabreTools.IO.Test.Extensions
Guid actualGuid = br.ReadType<Guid>();
Assert.Equal(expectedGuid, actualGuid);
#if NET6_0_OR_GREATER
// Half
stream = new MemoryStream(_bytes);
br = new BinaryReader(stream);
Half expectedHalf = BitConverter.Int16BitsToHalf(0x0100);
Half actualHalf = br.ReadType<Half>();
Assert.Equal(expectedHalf, actualHalf);
#endif
#if NET7_0_OR_GREATER
// Int128
stream = new MemoryStream(_bytes);
br = new BinaryReader(stream);
@@ -714,7 +705,6 @@ namespace SabreTools.IO.Test.Extensions
UInt128 expectedUInt128 = (UInt128)new BigInteger(_bytes);
UInt128 actualUInt128 = br.ReadType<UInt128>();
Assert.Equal(expectedHalf, actualHalf);
#endif
// Enum
stream = new MemoryStream(_bytes);

View File

@@ -1,9 +1,7 @@
using System;
using System.IO;
using System.Linq;
#if NET7_0_OR_GREATER
using System.Numerics;
#endif
using System.Text;
using SabreTools.IO.Extensions;
using Xunit;
@@ -132,7 +130,6 @@ namespace SabreTools.IO.Test.Extensions
ValidateBytes(expected, stream.GetBuffer());
}
#if NET6_0_OR_GREATER
[Fact]
public void WriteHalfTest()
{
@@ -153,7 +150,6 @@ namespace SabreTools.IO.Test.Extensions
Assert.True(write);
ValidateBytes(expected, stream.GetBuffer());
}
#endif
[Fact]
public void WriteInt24Test()
@@ -408,7 +404,6 @@ namespace SabreTools.IO.Test.Extensions
ValidateBytes(expected, stream.GetBuffer());
}
#if NET7_0_OR_GREATER
[Fact]
public void WriteInt128Test()
{
@@ -452,7 +447,6 @@ namespace SabreTools.IO.Test.Extensions
Assert.True(write);
ValidateBytes(expected, stream.GetBuffer());
}
#endif
[Fact]
public void WriteNullTerminatedAnsiStringTest()
@@ -536,16 +530,13 @@ namespace SabreTools.IO.Test.Extensions
Assert.True(actual);
ValidateBytes(_bytes, stream.GetBuffer());
#if NET6_0_OR_GREATER
// Half
stream = new MemoryStream(new byte[2], 0, 2, true, true);
bw = new BinaryWriter(stream);
actual = bw.WriteType<Half>(BitConverter.Int16BitsToHalf(0x0100));
Assert.True(actual);
ValidateBytes([.. _bytes.Take(2)], stream.GetBuffer());
#endif
#if NET7_0_OR_GREATER
// Int128
stream = new MemoryStream(new byte[16], 0, 16, true, true);
bw = new BinaryWriter(stream);
@@ -559,7 +550,6 @@ namespace SabreTools.IO.Test.Extensions
actual = bw.WriteType<UInt128>((UInt128)new BigInteger(_bytes));
Assert.True(actual);
ValidateBytes(_bytes, stream.GetBuffer());
#endif
// Enum
stream = new MemoryStream(new byte[4], 0, 4, true, true);

View File

@@ -36,6 +36,474 @@ namespace SabreTools.IO.Test.Extensions
#endregion
#region FindAllPositions
[Fact]
public void FindAllPositions_EmptyStack_NoMatches()
{
byte[] stack = [];
var positions = stack.FindAllPositions([0x01]);
Assert.Empty(positions);
}
[Fact]
public void FindAllPositions_EmptyNeedle_NoMatches()
{
byte[] stack = [0x01];
var positions = stack.FindAllPositions(Array.Empty<byte>());
Assert.Empty(positions);
}
[Fact]
public void FindAllPositions_LongerNeedle_NoMatches()
{
byte[] stack = [0x01];
var positions = stack.FindAllPositions([0x01, 0x02]);
Assert.Empty(positions);
}
[Fact]
public void FindAllPositions_InvalidStart_NoMatches()
{
byte[] stack = [0x01];
var positions = stack.FindAllPositions([0x01, 0x02], start: -1);
Assert.Empty(positions);
positions = stack.FindAllPositions([0x01, 0x02], start: 2);
Assert.Empty(positions);
}
[Fact]
public void FindAllPositions_InvalidEnd_NoMatches()
{
byte[] stack = [0x01];
var positions = stack.FindAllPositions([0x01, 0x02], end: -2);
Assert.Empty(positions);
positions = stack.FindAllPositions([0x01, 0x02], end: 0);
Assert.Empty(positions);
positions = stack.FindAllPositions([0x01, 0x02], end: 2);
Assert.Empty(positions);
}
[Fact]
public void FindAllPositions_Matching_Matches()
{
byte[] stack = [0x01, 0x02];
var positions = stack.FindAllPositions([0x01, 0x02]);
int position = Assert.Single(positions);
Assert.Equal(0, position);
}
[Fact]
public void FindAllPositions_Mismatch_NoMatches()
{
byte[] stack = [0x01, 0x03];
var positions = stack.FindAllPositions([0x01, 0x02]);
Assert.Empty(positions);
}
[Fact]
public void FindAllPositions_Multiple_Matches()
{
byte[] stack = [0x01, 0x01];
var positions = stack.FindAllPositions([0x01]);
Assert.Equal(2, positions.Count);
}
#endregion
#region FirstPosition
[Fact]
public void FirstPosition_EmptyStack_NoMatches()
{
byte[] stack = [];
int position = stack.FirstPosition([0x01]);
Assert.Equal(-1, position);
}
[Fact]
public void FirstPosition_EmptyNeedle_NoMatches()
{
byte[] stack = [0x01];
int position = stack.FirstPosition(Array.Empty<byte>());
Assert.Equal(-1, position);
}
[Fact]
public void FirstPosition_LongerNeedle_NoMatches()
{
byte[] stack = [0x01];
int position = stack.FirstPosition([0x01, 0x02]);
Assert.Equal(-1, position);
}
[Fact]
public void FirstPosition_InvalidStart_NoMatches()
{
byte[] stack = [0x01];
int position = stack.FirstPosition([0x01, 0x02], start: -1);
Assert.Equal(-1, position);
position = stack.FirstPosition([0x01, 0x02], start: 2);
Assert.Equal(-1, position);
}
[Fact]
public void FirstPosition_InvalidEnd_NoMatches()
{
byte[] stack = [0x01];
int position = stack.FirstPosition([0x01, 0x02], end: -2);
Assert.Equal(-1, position);
position = stack.FirstPosition([0x01, 0x02], end: 0);
Assert.Equal(-1, position);
position = stack.FirstPosition([0x01, 0x02], end: 2);
Assert.Equal(-1, position);
}
[Fact]
public void FirstPosition_Matching_Matches()
{
byte[] stack = [0x01, 0x02];
int position = stack.FirstPosition([0x01, 0x02]);
Assert.Equal(0, position);
}
[Fact]
public void FirstPosition_Mismatch_NoMatches()
{
byte[] stack = [0x01, 0x03];
int position = stack.FirstPosition([0x01, 0x02]);
Assert.Equal(-1, position);
}
[Fact]
public void FirstPosition_Multiple_Matches()
{
byte[] stack = [0x01, 0x01];
int position = stack.FirstPosition([0x01]);
Assert.Equal(0, position);
}
#endregion
#region LastPosition
[Fact]
public void LastPosition_EmptyStack_NoMatches()
{
byte[] stack = [];
int position = stack.LastPosition([0x01]);
Assert.Equal(-1, position);
}
[Fact]
public void LastPosition_EmptyNeedle_NoMatches()
{
byte[] stack = [0x01];
int position = stack.LastPosition(Array.Empty<byte>());
Assert.Equal(-1, position);
}
[Fact]
public void LastPosition_LongerNeedle_NoMatches()
{
byte[] stack = [0x01];
int position = stack.LastPosition([0x01, 0x02]);
Assert.Equal(-1, position);
}
[Fact]
public void LastPosition_InvalidStart_NoMatches()
{
byte[] stack = [0x01];
int position = stack.LastPosition([0x01, 0x02], start: -1);
Assert.Equal(-1, position);
position = stack.LastPosition([0x01, 0x02], start: 2);
Assert.Equal(-1, position);
}
[Fact]
public void LastPosition_InvalidEnd_NoMatches()
{
byte[] stack = [0x01];
int position = stack.LastPosition([0x01, 0x02], end: -2);
Assert.Equal(-1, position);
position = stack.LastPosition([0x01, 0x02], end: 0);
Assert.Equal(-1, position);
position = stack.LastPosition([0x01, 0x02], end: 2);
Assert.Equal(-1, position);
}
[Fact]
public void LastPosition_Matching_Matches()
{
byte[] stack = [0x01, 0x02];
int position = stack.LastPosition([0x01, 0x02]);
Assert.Equal(0, position);
}
[Fact]
public void LastPosition_Mismatch_NoMatches()
{
byte[] stack = [0x01, 0x03];
int position = stack.LastPosition([0x01, 0x02]);
Assert.Equal(-1, position);
}
[Fact]
public void LastPosition_Multiple_Matches()
{
byte[] stack = [0x01, 0x01];
int position = stack.LastPosition([0x01]);
Assert.Equal(1, position);
}
#endregion
#region EqualsExactly
[Fact]
public void EqualsExactly_EmptyStack_NoMatches()
{
byte[] stack = [];
bool found = stack.EqualsExactly([0x01]);
Assert.False(found);
}
[Fact]
public void EqualsExactly_EmptyNeedle_NoMatches()
{
byte[] stack = [0x01];
bool found = stack.EqualsExactly(Array.Empty<byte>());
Assert.False(found);
}
[Fact]
public void EqualsExactly_ShorterNeedle_NoMatches()
{
byte[] stack = [0x01, 0x02];
bool found = stack.EqualsExactly([0x01]);
Assert.False(found);
}
[Fact]
public void EqualsExactly_LongerNeedle_NoMatches()
{
byte[] stack = [0x01];
bool found = stack.EqualsExactly([0x01, 0x02]);
Assert.False(found);
}
[Fact]
public void EqualsExactly_Matching_Matches()
{
byte[] stack = [0x01, 0x02];
bool found = stack.EqualsExactly([0x01, 0x02]);
Assert.True(found);
}
[Fact]
public void EqualsExactly_Mismatch_NoMatches()
{
byte[] stack = [0x01, 0x03];
bool found = stack.EqualsExactly([0x01, 0x02]);
Assert.False(found);
}
#endregion
#region StartsWith
[Fact]
public void StartsWith_EmptyStack_NoMatches()
{
byte[] stack = [];
bool found = stack.StartsWith([0x01]);
Assert.False(found);
}
[Fact]
public void StartsWith_EmptyNeedle_NoMatches()
{
byte[] stack = [0x01];
bool found = stack.StartsWith(Array.Empty<byte>());
Assert.False(found);
}
[Fact]
public void StartsWith_LongerNeedle_NoMatches()
{
byte[] stack = [0x01];
bool found = stack.StartsWith([0x01, 0x02]);
Assert.False(found);
}
[Fact]
public void StartsWith_Matching_Matches()
{
byte[] stack = [0x01, 0x02];
bool found = stack.StartsWith([0x01, 0x02]);
Assert.True(found);
}
[Fact]
public void StartsWith_Mismatch_NoMatches()
{
byte[] stack = [0x01, 0x03];
bool found = stack.StartsWith([0x01, 0x02]);
Assert.False(found);
}
[Fact]
public void StartsWith_Multiple_Matches()
{
byte[] stack = [0x01, 0x01];
bool found = stack.StartsWith([0x01]);
Assert.True(found);
}
#endregion
#region EndsWith
[Fact]
public void EndsWith_EmptyStack_NoMatches()
{
byte[] stack = [];
bool found = stack.EndsWith([0x01]);
Assert.False(found);
}
[Fact]
public void EndsWith_EmptyNeedle_NoMatches()
{
byte[] stack = [0x01];
bool found = stack.EndsWith(Array.Empty<byte>());
Assert.False(found);
}
[Fact]
public void EndsWith_LongerNeedle_NoMatches()
{
byte[] stack = [0x01];
bool found = stack.StartsWith([0x01, 0x02]);
Assert.False(found);
}
[Fact]
public void EndsWith_Matching_Matches()
{
byte[] stack = [0x01, 0x02];
bool found = stack.EndsWith([0x01, 0x02]);
Assert.True(found);
}
[Fact]
public void EndsWith_Mismatch_NoMatches()
{
byte[] stack = [0x01, 0x03];
bool found = stack.EndsWith([0x01, 0x02]);
Assert.False(found);
}
[Fact]
public void EndsWith_Multiple_Matches()
{
byte[] stack = [0x01, 0x01];
bool found = stack.EndsWith([0x01]);
Assert.True(found);
}
#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

@@ -1,8 +1,6 @@
using System;
using System.Linq;
#if NET7_0_OR_GREATER
using System.Numerics;
#endif
using System.Text;
using SabreTools.IO.Extensions;
using Xunit;
@@ -142,7 +140,6 @@ namespace SabreTools.IO.Test.Extensions
Assert.Equal(0x0100, read);
}
#if NET6_0_OR_GREATER
[Fact]
public void ReadHalfTest()
{
@@ -160,7 +157,6 @@ namespace SabreTools.IO.Test.Extensions
Half read = _bytes.ReadHalfBigEndian(ref offset);
Assert.Equal(expected, read);
}
#endif
[Fact]
public void ReadInt24Test()
@@ -474,7 +470,6 @@ namespace SabreTools.IO.Test.Extensions
Assert.Equal(expected, read);
}
#if NET7_0_OR_GREATER
[Fact]
public void ReadInt128Test()
{
@@ -512,7 +507,6 @@ namespace SabreTools.IO.Test.Extensions
UInt128 read = _bytes.ReadUInt128BigEndian(ref offset);
Assert.Equal(expected, read);
}
#endif
[Fact]
public void ReadNullTerminatedStringTest()
@@ -557,15 +551,12 @@ namespace SabreTools.IO.Test.Extensions
Guid actualGuid = _bytes.ReadType<Guid>(ref offset);
Assert.Equal(expectedGuid, actualGuid);
#if NET6_0_OR_GREATER
// Half
offset = 0;
Half expectedHalf = BitConverter.Int16BitsToHalf(0x0100);
Half actualHalf = _bytes.ReadType<Half>(ref offset);
Assert.Equal(expectedHalf, actualHalf);
#endif
#if NET7_0_OR_GREATER
// Int128
offset = 0;
Int128 expectedInt128 = (Int128)new BigInteger(_bytes);
@@ -577,7 +568,6 @@ namespace SabreTools.IO.Test.Extensions
UInt128 expectedUInt128 = (UInt128)new BigInteger(_bytes);
UInt128 actualUInt128 = _bytes.ReadType<UInt128>(ref offset);
Assert.Equal(expectedHalf, actualHalf);
#endif
// Enum
offset = 0;

View File

@@ -1,8 +1,6 @@
using System;
using System.Linq;
#if NET7_0_OR_GREATER
using System.Numerics;
#endif
using System.Text;
using SabreTools.IO.Extensions;
using Xunit;
@@ -139,7 +137,6 @@ namespace SabreTools.IO.Test.Extensions
ValidateBytes(expected, buffer);
}
#if NET6_0_OR_GREATER
[Fact]
public void WriteHalfTest()
{
@@ -161,7 +158,6 @@ namespace SabreTools.IO.Test.Extensions
Assert.True(write);
ValidateBytes(expected, buffer);
}
#endif
[Fact]
public void WriteInt24Test()
@@ -427,7 +423,6 @@ namespace SabreTools.IO.Test.Extensions
ValidateBytes(expected, buffer);
}
#if NET7_0_OR_GREATER
[Fact]
public void WriteInt128Test()
{
@@ -471,7 +466,6 @@ namespace SabreTools.IO.Test.Extensions
Assert.True(write);
ValidateBytes(expected, buffer);
}
#endif
[Fact]
public void WriteNullTerminatedAnsiStringTest()
@@ -555,16 +549,13 @@ namespace SabreTools.IO.Test.Extensions
Assert.True(actual);
ValidateBytes(_bytes, buffer);
#if NET6_0_OR_GREATER
// Half
offset = 0;
buffer = new byte[2];
actual = buffer.WriteType<Half>(ref offset, BitConverter.Int16BitsToHalf(0x0100));
Assert.True(actual);
ValidateBytes([.. _bytes.Take(2)], buffer);
#endif
#if NET7_0_OR_GREATER
// Int128
offset = 0;
buffer = new byte[16];
@@ -578,7 +569,6 @@ namespace SabreTools.IO.Test.Extensions
actual = buffer.WriteType<UInt128>(ref offset, (UInt128)new BigInteger(_bytes));
Assert.True(actual);
ValidateBytes(_bytes, buffer);
#endif
// Enum
offset = 0;

View File

@@ -1,9 +1,7 @@
using System;
using System.IO;
using System.Linq;
#if NET7_0_OR_GREATER
using System.Numerics;
#endif
using System.Text;
using SabreTools.IO.Extensions;
using Xunit;
@@ -146,7 +144,6 @@ namespace SabreTools.IO.Test.Extensions
Assert.Equal(0x0100, read);
}
#if NET6_0_OR_GREATER
[Fact]
public void ReadHalfTest()
{
@@ -164,7 +161,6 @@ namespace SabreTools.IO.Test.Extensions
Half read = stream.ReadHalfBigEndian();
Assert.Equal(expected, read);
}
#endif
[Fact]
public void ReadInt24Test()
@@ -478,7 +474,6 @@ namespace SabreTools.IO.Test.Extensions
Assert.Equal(expected, read);
}
#if NET7_0_OR_GREATER
[Fact]
public void ReadInt128Test()
{
@@ -516,7 +511,6 @@ namespace SabreTools.IO.Test.Extensions
UInt128 read = stream.ReadUInt128BigEndian();
Assert.Equal(expected, read);
}
#endif
[Fact]
public void ReadNullTerminatedStringTest()
@@ -561,15 +555,12 @@ namespace SabreTools.IO.Test.Extensions
Guid actualGuid = stream.ReadType<Guid>();
Assert.Equal(expectedGuid, actualGuid);
#if NET6_0_OR_GREATER
// Half
stream = new MemoryStream(_bytes);
Half expectedHalf = BitConverter.Int16BitsToHalf(0x0100);
Half actualHalf = stream.ReadType<Half>();
Assert.Equal(expectedHalf, actualHalf);
#endif
#if NET7_0_OR_GREATER
// Int128
stream = new MemoryStream(_bytes);
Int128 expectedInt128 = (Int128)new BigInteger(_bytes);
@@ -581,7 +572,6 @@ namespace SabreTools.IO.Test.Extensions
UInt128 expectedUInt128 = (UInt128)new BigInteger(_bytes);
UInt128 actualUInt128 = stream.ReadType<UInt128>();
Assert.Equal(expectedHalf, actualHalf);
#endif
// Enum
stream = new MemoryStream(_bytes);

View File

@@ -1,9 +1,7 @@
using System;
using System.IO;
using System.Linq;
#if NET7_0_OR_GREATER
using System.Numerics;
#endif
using System.Text;
using SabreTools.IO.Extensions;
using Xunit;
@@ -128,7 +126,6 @@ namespace SabreTools.IO.Test.Extensions
ValidateBytes(expected, stream.GetBuffer());
}
#if NET6_0_OR_GREATER
[Fact]
public void WriteHalfTest()
{
@@ -148,7 +145,6 @@ namespace SabreTools.IO.Test.Extensions
Assert.True(write);
ValidateBytes(expected, stream.GetBuffer());
}
#endif
[Fact]
public void WriteInt24Test()
@@ -390,7 +386,6 @@ namespace SabreTools.IO.Test.Extensions
ValidateBytes(expected, stream.GetBuffer());
}
#if NET7_0_OR_GREATER
[Fact]
public void WriteInt128Test()
{
@@ -430,7 +425,6 @@ namespace SabreTools.IO.Test.Extensions
Assert.True(write);
ValidateBytes(expected, stream.GetBuffer());
}
#endif
[Fact]
public void WriteNullTerminatedAnsiStringTest()
@@ -507,15 +501,12 @@ namespace SabreTools.IO.Test.Extensions
Assert.True(actual);
ValidateBytes(_bytes, stream.GetBuffer());
#if NET6_0_OR_GREATER
// Half
stream = new MemoryStream(new byte[2], 0, 2, true, true);
actual = stream.WriteType<Half>(BitConverter.Int16BitsToHalf(0x0100));
Assert.True(actual);
ValidateBytes([.. _bytes.Take(2)], stream.GetBuffer());
#endif
#if NET7_0_OR_GREATER
// Int128
stream = new MemoryStream(new byte[16], 0, 16, true, true);
actual = stream.WriteType<Int128>((Int128)new BigInteger(_bytes));
@@ -527,7 +518,6 @@ namespace SabreTools.IO.Test.Extensions
actual = stream.WriteType<UInt128>((UInt128)new BigInteger(_bytes));
Assert.True(actual);
ValidateBytes(_bytes, stream.GetBuffer());
#endif
// Enum
stream = new MemoryStream(new byte[4], 0, 4, true, true);

View File

@@ -0,0 +1,324 @@
using System.Collections.Generic;
using System.IO;
using SabreTools.IO.Matching;
using Xunit;
namespace SabreTools.IO.Test
{
public class MatchUtilTests
{
#region Array
[Fact]
public void ArrayGetAllMatches_NullStack_NoMatches()
{
byte[]? stack = null;
List<ContentMatchSet> matchSets = [new ContentMatchSet(new byte[1], "name")];
var matches = MatchUtil.GetAllMatches("file", stack, matchSets);
Assert.Empty(matches);
}
[Fact]
public void ArrayGetAllMatches_EmptyStack_NoMatches()
{
byte[] stack = [];
List<ContentMatchSet> matchSets = [new ContentMatchSet(new byte[1], "name")];
var matches = MatchUtil.GetAllMatches("file", stack, matchSets);
Assert.Empty(matches);
}
[Fact]
public void ArrayGetAllMatches_EmptyMatchSets_NoMatches()
{
byte[] stack = [0x01];
List<ContentMatchSet> matchSets = [];
var matches = MatchUtil.GetAllMatches("file", stack, matchSets);
Assert.Empty(matches);
}
[Fact]
public void ArrayGetAllMatches_Matching_Matches()
{
byte[] stack = [0x01];
List<ContentMatchSet> matchSets = [new ContentMatchSet(new byte[] { 0x01 }, "name")];
var matches = MatchUtil.GetAllMatches("file", stack, matchSets);
string setName = Assert.Single(matches);
Assert.Equal("name", setName);
}
[Fact]
public void ArrayGetAllMatches_PartialMatchingAny_Matches()
{
byte[] stack = [0x01];
List<ContentMatchSet> matchSets =
[
new ContentMatchSet([
new byte[] { 0x00 },
new ContentMatch([0x01]),
], "name")
];
var matches = MatchUtil.GetAllMatches("file", stack, matchSets, any: true);
string setName = Assert.Single(matches);
Assert.Equal("name", setName);
}
[Fact]
public void ArrayGetAllMatches_PartialMatchingAll_NoMatches()
{
byte[] stack = [0x01];
List<ContentMatchSet> matchSets =
[
new ContentMatchSet([
new byte[] { 0x00 },
new ContentMatch([0x01]),
], "name")
];
var matches = MatchUtil.GetAllMatches("file", stack, matchSets, any: false);
Assert.Empty(matches);
}
[Fact]
public void ArrayGetFirstMatch_NullStack_NoMatches()
{
byte[]? stack = null;
List<ContentMatchSet> matchSets = [new ContentMatchSet(new byte[1], "name")];
string? match = MatchUtil.GetFirstMatch("file", stack, matchSets);
Assert.Null(match);
}
[Fact]
public void ArrayGetFirstMatch_EmptyStack_NoMatches()
{
byte[] stack = [];
List<ContentMatchSet> matchSets = [new ContentMatchSet(new byte[1], "name")];
string? match = MatchUtil.GetFirstMatch("file", stack, matchSets);
Assert.Null(match);
}
[Fact]
public void ArrayGetFirstMatch_EmptyMatchSets_NoMatches()
{
byte[] stack = [0x01];
List<ContentMatchSet> matchSets = [];
string? match = MatchUtil.GetFirstMatch("file", stack, matchSets);
Assert.Null(match);
}
[Fact]
public void ArrayGetFirstMatch_Matching_Matches()
{
byte[] stack = [0x01];
List<ContentMatchSet> matchSets = [new ContentMatchSet(new byte[] { 0x01 }, "name")];
string? setName = MatchUtil.GetFirstMatch("file", stack, matchSets);
Assert.Equal("name", setName);
}
[Fact]
public void ArrayGetFirstMatch_PartialMatchingAny_Matches()
{
byte[] stack = [0x01];
List<ContentMatchSet> matchSets =
[
new ContentMatchSet([
new byte[] { 0x00 },
new ContentMatch([0x01]),
], "name")
];
string? setName = MatchUtil.GetFirstMatch("file", stack, matchSets, any: true);
Assert.Equal("name", setName);
}
[Fact]
public void ArrayGetFirstMatch_PartialMatchingAll_NoMatches()
{
byte[] stack = [0x01];
List<ContentMatchSet> matchSets =
[
new ContentMatchSet([
new byte[] { 0x00 },
new ContentMatch([0x01]),
], "name")
];
string? setName = MatchUtil.GetFirstMatch("file", stack, matchSets, any: false);
Assert.Null(setName);
}
[Fact]
public void ExactSizeArrayMatch()
{
byte[] source = [0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07];
byte?[] check = [0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07];
string expected = "match";
var matchers = new List<ContentMatchSet>
{
new(check, expected),
};
string? actual = MatchUtil.GetFirstMatch("testfile", source, matchers, any: false);
Assert.Equal(expected, actual);
}
#endregion
#region Stream
[Fact]
public void StreamGetAllMatches_NullStack_NoMatches()
{
Stream? stack = null;
List<ContentMatchSet> matchSets = [new ContentMatchSet(new byte[1], "name")];
var matches = MatchUtil.GetAllMatches("file", stack, matchSets);
Assert.Empty(matches);
}
[Fact]
public void StreamGetAllMatches_EmptyStack_NoMatches()
{
Stream stack = new MemoryStream();
List<ContentMatchSet> matchSets = [new ContentMatchSet(new byte[1], "name")];
var matches = MatchUtil.GetAllMatches("file", stack, matchSets);
Assert.Empty(matches);
}
[Fact]
public void StreamGetAllMatches_EmptyMatchSets_NoMatches()
{
Stream stack = new MemoryStream([0x01]);
List<ContentMatchSet> matchSets = [];
var matches = MatchUtil.GetAllMatches("file", stack, matchSets);
Assert.Empty(matches);
}
[Fact]
public void StreamGetAllMatches_Matching_Matches()
{
Stream stack = new MemoryStream([0x01]);
List<ContentMatchSet> matchSets = [new ContentMatchSet(new byte[] { 0x01 }, "name")];
var matches = MatchUtil.GetAllMatches("file", stack, matchSets);
string setName = Assert.Single(matches);
Assert.Equal("name", setName);
}
[Fact]
public void StreamGetAllMatches_PartialMatchingAny_Matches()
{
Stream stack = new MemoryStream([0x01]);
List<ContentMatchSet> matchSets =
[
new ContentMatchSet([
new byte[] { 0x00 },
new ContentMatch([0x01]),
], "name")
];
var matches = MatchUtil.GetAllMatches("file", stack, matchSets, any: true);
string setName = Assert.Single(matches);
Assert.Equal("name", setName);
}
[Fact]
public void StreamGetAllMatches_PartialMatchingAll_NoMatches()
{
Stream stack = new MemoryStream([0x01]);
List<ContentMatchSet> matchSets =
[
new ContentMatchSet([
new byte[] { 0x00 },
new ContentMatch([0x01]),
], "name")
];
var matches = MatchUtil.GetAllMatches("file", stack, matchSets, any: false);
Assert.Empty(matches);
}
[Fact]
public void StreamGetFirstMatch_NullStack_NoMatches()
{
Stream? stack = null;
List<ContentMatchSet> matchSets = [new ContentMatchSet(new byte[1], "name")];
string? match = MatchUtil.GetFirstMatch("file", stack, matchSets);
Assert.Null(match);
}
[Fact]
public void StreamGetFirstMatch_EmptyStack_NoMatches()
{
Stream stack = new MemoryStream();
List<ContentMatchSet> matchSets = [new ContentMatchSet(new byte[1], "name")];
string? match = MatchUtil.GetFirstMatch("file", stack, matchSets);
Assert.Null(match);
}
[Fact]
public void StreamGetFirstMatch_EmptyMatchSets_NoMatches()
{
Stream stack = new MemoryStream([0x01]);
List<ContentMatchSet> matchSets = [];
string? match = MatchUtil.GetFirstMatch("file", stack, matchSets);
Assert.Null(match);
}
[Fact]
public void StreamGetFirstMatch_Matching_Matches()
{
Stream stack = new MemoryStream([0x01]);
List<ContentMatchSet> matchSets = [new ContentMatchSet(new byte[] { 0x01 }, "name")];
string? setName = MatchUtil.GetFirstMatch("file", stack, matchSets);
Assert.Equal("name", setName);
}
[Fact]
public void StreamGetFirstMatch_PartialMatchingAny_Matches()
{
Stream stack = new MemoryStream([0x01]);
List<ContentMatchSet> matchSets =
[
new ContentMatchSet([
new byte[] { 0x00 },
new ContentMatch([0x01]),
], "name")
];
string? setName = MatchUtil.GetFirstMatch("file", stack, matchSets, any: true);
Assert.Equal("name", setName);
}
[Fact]
public void StreamGetFirstMatch_PartialMatchingAll_NoMatches()
{
Stream stack = new MemoryStream([0x01]);
List<ContentMatchSet> matchSets =
[
new ContentMatchSet([
new byte[] { 0x00 },
new ContentMatch([0x01]),
], "name")
];
string? setName = MatchUtil.GetFirstMatch("file", stack, matchSets, any: false);
Assert.Null(setName);
}
[Fact]
public void ExactSizeStreamMatch()
{
byte[] source = [0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07];
var stream = new MemoryStream(source);
byte?[] check = [0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07];
string expected = "match";
var matchers = new List<ContentMatchSet>
{
new(check, expected),
};
string? actual = MatchUtil.GetFirstMatch("testfile", stream, matchers, any: false);
Assert.Equal(expected, actual);
}
#endregion
#region Path
#endregion
}
}

View File

@@ -0,0 +1,202 @@
using System;
using System.Collections.Generic;
using System.IO;
using SabreTools.IO.Matching;
using Xunit;
namespace SabreTools.IO.Test.Matching
{
public class ContentMatchSetTests
{
[Fact]
public void InvalidNeedle_ThrowsException()
{
Assert.Throws<InvalidDataException>(() => new ContentMatchSet(Array.Empty<byte>(), "name"));
Assert.Throws<InvalidDataException>(() => new ContentMatchSet(Array.Empty<byte>(), ArrayVersionMock, "name"));
Assert.Throws<InvalidDataException>(() => new ContentMatchSet(Array.Empty<byte>(), StreamVersionMock, "name"));
}
[Fact]
public void InvalidNeedles_ThrowsException()
{
Assert.Throws<InvalidDataException>(() => new ContentMatchSet([], "name"));
Assert.Throws<InvalidDataException>(() => new ContentMatchSet([], ArrayVersionMock, "name"));
Assert.Throws<InvalidDataException>(() => new ContentMatchSet([], StreamVersionMock, "name"));
}
[Fact]
public void GenericConstructor_NoDelegates()
{
var needles = new List<ContentMatch> { new byte[] { 0x01, 0x02, 0x03, 0x04 } };
var cms = new ContentMatchSet(needles, "name");
Assert.Null(cms.GetArrayVersion);
Assert.Null(cms.GetStreamVersion);
}
[Fact]
public void ArrayConstructor_SingleDelegate()
{
var needles = new List<ContentMatch> { new byte[] { 0x01, 0x02, 0x03, 0x04 } };
var cms = new ContentMatchSet(needles, ArrayVersionMock, "name");
Assert.NotNull(cms.GetArrayVersion);
Assert.Null(cms.GetStreamVersion);
}
[Fact]
public void StreamConstructor_SingleDelegate()
{
var needles = new List<ContentMatch> { new byte[] { 0x01, 0x02, 0x03, 0x04 } };
var cms = new ContentMatchSet(needles, StreamVersionMock, "name");
Assert.Null(cms.GetArrayVersion);
Assert.NotNull(cms.GetStreamVersion);
}
#region Array
[Fact]
public void MatchesAll_NullArray_NoMatches()
{
var cms = new ContentMatchSet(new byte[] { 0x01, 0x02, 0x03, 0x04 }, "name");
var actual = cms.MatchesAll((byte[]?)null);
Assert.Empty(actual);
}
[Fact]
public void MatchesAll_EmptyArray_NoMatches()
{
var cms = new ContentMatchSet(new byte[] { 0x01, 0x02, 0x03, 0x04 }, "name");
var actual = cms.MatchesAll([]);
Assert.Empty(actual);
}
[Fact]
public void MatchesAll_MatchingArray_Matches()
{
var cms = new ContentMatchSet(new byte[] { 0x01, 0x02, 0x03, 0x04 }, "name");
var actual = cms.MatchesAll([0x01, 0x02, 0x03, 0x04]);
int position = Assert.Single(actual);
Assert.Equal(0, position);
}
[Fact]
public void MatchesAll_MismatchedArray_NoMatches()
{
var cms = new ContentMatchSet(new byte[] { 0x01, 0x02, 0x03, 0x04 }, "name");
var actual = cms.MatchesAll([0x01, 0x03]);
Assert.Empty(actual);
}
[Fact]
public void MatchesAny_NullArray_NoMatches()
{
var cms = new ContentMatchSet(new byte[] { 0x01, 0x02, 0x03, 0x04 }, "name");
int actual = cms.MatchesAny((byte[]?)null);
Assert.Equal(-1, actual);
}
[Fact]
public void MatchesAny_EmptyArray_NoMatches()
{
var cms = new ContentMatchSet(new byte[] { 0x01, 0x02, 0x03, 0x04 }, "name");
int actual = cms.MatchesAny([]);
Assert.Equal(-1, actual);
}
[Fact]
public void MatchesAny_MatchingArray_Matches()
{
var cms = new ContentMatchSet(new byte[] { 0x01, 0x02, 0x03, 0x04 }, "name");
int actual = cms.MatchesAny([0x01, 0x02, 0x03, 0x04]);
Assert.Equal(0, actual);
}
[Fact]
public void MatchesAny_MismatchedArray_NoMatches()
{
var cms = new ContentMatchSet(new byte[] { 0x01, 0x02, 0x03, 0x04 }, "name");
int actual = cms.MatchesAny([0x01, 0x03]);
Assert.Equal(-1, actual);
}
#endregion
#region Stream
[Fact]
public void MatchesAll_NullStream_NoMatches()
{
var cms = new ContentMatchSet(new byte[] { 0x01, 0x02, 0x03, 0x04 }, "name");
var actual = cms.MatchesAll((Stream?)null);
Assert.Empty(actual);
}
[Fact]
public void MatchesAll_EmptyStream_NoMatches()
{
var cms = new ContentMatchSet(new byte[] { 0x01, 0x02, 0x03, 0x04 }, "name");
var actual = cms.MatchesAll(new MemoryStream());
Assert.Empty(actual);
}
[Fact]
public void MatchesAll_MatchingStream_Matches()
{
var cms = new ContentMatchSet(new byte[] { 0x01, 0x02, 0x03, 0x04 }, "name");
var actual = cms.MatchesAll(new MemoryStream([0x01, 0x02, 0x03, 0x04]));
int position = Assert.Single(actual);
Assert.Equal(0, position);
}
[Fact]
public void MatchesAll_MismatchedStream_NoMatches()
{
var cms = new ContentMatchSet(new byte[] { 0x01, 0x02, 0x03, 0x04 }, "name");
var actual = cms.MatchesAll([0x01, 0x03]);
Assert.Empty(actual);
}
[Fact]
public void MatchesAny_NullStream_NoMatches()
{
var cms = new ContentMatchSet(new byte[] { 0x01, 0x02, 0x03, 0x04 }, "name");
int actual = cms.MatchesAny((Stream?)null);
Assert.Equal(-1, actual);
}
[Fact]
public void MatchesAny_EmptyStream_NoMatches()
{
var cms = new ContentMatchSet(new byte[] { 0x01, 0x02, 0x03, 0x04 }, "name");
int actual = cms.MatchesAny(new MemoryStream());
Assert.Equal(-1, actual);
}
[Fact]
public void MatchesAny_MatchingStream_Matches()
{
var cms = new ContentMatchSet(new byte[] { 0x01, 0x02, 0x03, 0x04 }, "name");
int actual = cms.MatchesAny(new MemoryStream([0x01, 0x02, 0x03, 0x04]));
Assert.Equal(0, actual);
}
[Fact]
public void MatchesAny_MismatchedStream_NoMatches()
{
var cms = new ContentMatchSet(new byte[] { 0x01, 0x02, 0x03, 0x04 }, "name");
int actual = cms.MatchesAny([0x01, 0x03]);
Assert.Equal(-1, actual);
}
#endregion
#region Mock Delegates
/// <inheritdoc cref="GetArrayVersion"/>
private static string? ArrayVersionMock(string path, byte[]? content, List<int> positions) => null;
/// <inheritdoc cref="GetStreamVersion"/>
private static string? StreamVersionMock(string path, Stream? content, List<int> positions) => null;
#endregion
}
}

View File

@@ -0,0 +1,271 @@
using System;
using System.IO;
using SabreTools.IO.Matching;
using Xunit;
namespace SabreTools.IO.Test.Matching
{
public class ContentMatchTests
{
[Fact]
public void InvalidNeedle_ThrowsException()
{
Assert.Throws<InvalidDataException>(() => new ContentMatch(Array.Empty<byte>()));
Assert.Throws<InvalidDataException>(() => new ContentMatch(Array.Empty<byte?>()));
}
[Fact]
public void InvalidStart_ThrowsException()
{
Assert.Throws<ArgumentOutOfRangeException>(() => new ContentMatch(new byte[1], start: -1));
Assert.Throws<ArgumentOutOfRangeException>(() => new ContentMatch(new byte?[1], start: -1));
}
[Fact]
public void InvalidEnd_ThrowsException()
{
Assert.Throws<ArgumentOutOfRangeException>(() => new ContentMatch(new byte[1], end: -2));
Assert.Throws<ArgumentOutOfRangeException>(() => new ContentMatch(new byte?[1], end: -2));
}
[Fact]
public void ImplicitOperatorArray_Success()
{
byte[] needle = [0x01, 0x02, 0x03, 0x04];
var cm = (ContentMatch)needle;
Assert.NotNull(cm);
}
[Fact]
public void ImplicitOperatorNullableArray_Success()
{
byte?[] needle = [0x01, 0x02, 0x03, 0x04];
var cm = (ContentMatch)needle;
Assert.NotNull(cm);
}
#region Byte Array
[Fact]
public void NullArray_NoMatch()
{
var cm = new ContentMatch(new byte?[1]);
int actual = cm.Match((byte[]?)null);
Assert.Equal(-1, actual);
}
[Fact]
public void EmptyArray_NoMatch()
{
var cm = new ContentMatch(new byte?[1]);
int actual = cm.Match([]);
Assert.Equal(-1, actual);
}
[Fact]
public void LargerNeedleArray_NoMatch()
{
var cm = new ContentMatch(new byte?[2]);
int actual = cm.Match(new byte[1]);
Assert.Equal(-1, actual);
}
[Fact]
public void EqualLengthMatchingArray_Match()
{
byte[] needle = [0x01, 0x02, 0x03, 0x04];
var cm = new ContentMatch(needle);
int actual = cm.Match(needle);
Assert.Equal(0, actual);
}
[Fact]
public void EqualLengthMatchingArrayReverse_Match()
{
byte[] needle = [0x01, 0x02, 0x03, 0x04];
var cm = new ContentMatch(needle);
int actual = cm.Match(needle, reverse: true);
Assert.Equal(0, actual);
}
[Fact]
public void EqualLengthMismatchedArray_NoMatch()
{
byte[] needle = [0x01, 0x02, 0x03, 0x04];
var cm = new ContentMatch(needle);
int actual = cm.Match(new byte[4]);
Assert.Equal(-1, actual);
}
[Fact]
public void EqualLengthMismatchedArrayReverse_NoMatch()
{
byte[] needle = [0x01, 0x02, 0x03, 0x04];
var cm = new ContentMatch(needle);
int actual = cm.Match(new byte[4], reverse: true);
Assert.Equal(-1, actual);
}
[Fact]
public void InequalLengthMatchingArray_Match()
{
byte[] stack = [0x01, 0x02, 0x03, 0x04];
byte[] needle = [0x02, 0x03];
var cm = new ContentMatch(needle);
int actual = cm.Match(stack);
Assert.Equal(1, actual);
}
[Fact]
public void InequalLengthMatchingArrayReverse_Match()
{
byte[] stack = [0x01, 0x02, 0x03, 0x04];
byte[] needle = [0x02, 0x03];
var cm = new ContentMatch(needle);
int actual = cm.Match(stack, reverse: true);
Assert.Equal(1, actual);
}
[Fact]
public void InequalLengthMismatchedArray_NoMatch()
{
byte[] stack = [0x01, 0x02, 0x03, 0x04];
byte[] needle = [0x02, 0x04];
var cm = new ContentMatch(needle);
int actual = cm.Match(stack);
Assert.Equal(-1, actual);
}
[Fact]
public void InequalLengthMismatchedArrayReverse_NoMatch()
{
byte[] stack = [0x01, 0x02, 0x03, 0x04];
byte[] needle = [0x02, 0x04];
var cm = new ContentMatch(needle);
int actual = cm.Match(stack, reverse: true);
Assert.Equal(-1, actual);
}
#endregion
#region Stream
[Fact]
public void NullStream_NoMatch()
{
var cm = new ContentMatch(new byte?[1]);
int actual = cm.Match((Stream?)null);
Assert.Equal(-1, actual);
}
[Fact]
public void EmptyStream_NoMatch()
{
var cm = new ContentMatch(new byte?[1]);
int actual = cm.Match(new MemoryStream());
Assert.Equal(-1, actual);
}
[Fact]
public void LargerNeedleStream_NoMatch()
{
var cm = new ContentMatch(new byte?[2]);
int actual = cm.Match(new MemoryStream(new byte[1]));
Assert.Equal(-1, actual);
}
[Fact]
public void EqualLengthMatchingStream_Match()
{
byte[] needle = [0x01, 0x02, 0x03, 0x04];
var cm = new ContentMatch(needle);
int actual = cm.Match(new MemoryStream(needle));
Assert.Equal(0, actual);
}
[Fact]
public void EqualLengthMatchingStreamReverse_Match()
{
byte[] needle = [0x01, 0x02, 0x03, 0x04];
var cm = new ContentMatch(needle);
int actual = cm.Match(new MemoryStream(needle), reverse: true);
Assert.Equal(0, actual);
}
[Fact]
public void EqualLengthMismatchedStream_NoMatch()
{
byte[] needle = [0x01, 0x02, 0x03, 0x04];
var cm = new ContentMatch(needle);
int actual = cm.Match(new MemoryStream(new byte[4]));
Assert.Equal(-1, actual);
}
[Fact]
public void EqualLengthMismatchedStreamReverse_NoMatch()
{
byte[] needle = [0x01, 0x02, 0x03, 0x04];
var cm = new ContentMatch(needle);
int actual = cm.Match(new MemoryStream(new byte[4]), reverse: true);
Assert.Equal(-1, actual);
}
[Fact]
public void InequalLengthMatchingStream_Match()
{
Stream stack = new MemoryStream([0x01, 0x02, 0x03, 0x04]);
byte[] needle = [0x02, 0x03];
var cm = new ContentMatch(needle);
int actual = cm.Match(stack);
Assert.Equal(1, actual);
}
[Fact]
public void InequalLengthMatchingStreamReverse_Match()
{
Stream stack = new MemoryStream([0x01, 0x02, 0x03, 0x04]);
byte[] needle = [0x02, 0x03];
var cm = new ContentMatch(needle);
int actual = cm.Match(stack, reverse: true);
Assert.Equal(1, actual);
}
[Fact]
public void InequalLengthMismatchedStream_NoMatch()
{
Stream stack = new MemoryStream([0x01, 0x02, 0x03, 0x04]);
byte[] needle = [0x02, 0x04];
var cm = new ContentMatch(needle);
int actual = cm.Match(stack);
Assert.Equal(-1, actual);
}
[Fact]
public void InequalLengthMismatchedStreamReverse_NoMatch()
{
Stream stack = new MemoryStream([0x01, 0x02, 0x03, 0x04]);
byte[] needle = [0x02, 0x04];
var cm = new ContentMatch(needle);
int actual = cm.Match(stack, reverse: true);
Assert.Equal(-1, actual);
}
#endregion
}
}

View File

@@ -0,0 +1,22 @@
using System.IO;
using SabreTools.IO.Matching;
using Xunit;
namespace SabreTools.IO.Test.Matching
{
/// <remarks>
/// All other test cases are covered by <see cref="PathMatchTests"/>
/// </remarks>
public class FilePathMatchTests
{
[Fact]
public void ConstructorFormatsNeedle()
{
string needle = "test";
string expected = $"{Path.DirectorySeparatorChar}{needle}";
var fpm = new FilePathMatch(needle);
Assert.Equal(expected, fpm.Needle);
}
}
}

View File

@@ -0,0 +1,186 @@
using System;
using System.Collections.Generic;
using System.IO;
using SabreTools.IO.Matching;
using Xunit;
namespace SabreTools.IO.Test.Matching
{
public class PathMatchSetTests
{
[Fact]
public void InvalidNeedle_ThrowsException()
{
Assert.Throws<InvalidDataException>(() => new PathMatchSet(string.Empty, "name"));
Assert.Throws<InvalidDataException>(() => new PathMatchSet(string.Empty, PathVersionMock, "name"));
}
[Fact]
public void InvalidNeedles_ThrowsException()
{
Assert.Throws<InvalidDataException>(() => new PathMatchSet([], "name"));
Assert.Throws<InvalidDataException>(() => new PathMatchSet([], PathVersionMock, "name"));
}
[Fact]
public void GenericConstructor_NoDelegates()
{
var needles = new List<PathMatch> { "test" };
var cms = new PathMatchSet(needles, "name");
Assert.Null(cms.GetVersion);
}
[Fact]
public void VersionConstructor_SingleDelegate()
{
var needles = new List<PathMatch> { "test" };
var cms = new PathMatchSet(needles, PathVersionMock, "name");
Assert.NotNull(cms.GetVersion);
}
#region Array
[Fact]
public void MatchesAll_NullArray_NoMatches()
{
var cms = new PathMatchSet("test", "name");
var actual = cms.MatchesAll((string[]?)null);
Assert.Empty(actual);
}
[Fact]
public void MatchesAll_EmptyArray_NoMatches()
{
var cms = new PathMatchSet("test", "name");
var actual = cms.MatchesAll(Array.Empty<string>());
Assert.Empty(actual);
}
[Fact]
public void MatchesAll_MatchingArray_Matches()
{
var cms = new PathMatchSet("test", "name");
var actual = cms.MatchesAll(new string[] { "test" });
string path = Assert.Single(actual);
Assert.Equal("test", path);
}
[Fact]
public void MatchesAll_MismatchedArray_NoMatches()
{
var cms = new PathMatchSet("test", "name");
var actual = cms.MatchesAll(new string[] { "not" });
Assert.Empty(actual);
}
[Fact]
public void MatchesAny_NullArray_NoMatches()
{
var cms = new PathMatchSet("test", "name");
string? actual = cms.MatchesAny((string[]?)null);
Assert.Null(actual);
}
[Fact]
public void MatchesAny_EmptyArray_NoMatches()
{
var cms = new PathMatchSet("test", "name");
string? actual = cms.MatchesAny(Array.Empty<string>());
Assert.Null(actual);
}
[Fact]
public void MatchesAny_MatchingArray_Matches()
{
var cms = new PathMatchSet("test", "name");
string? actual = cms.MatchesAny(new string[] { "test" });
Assert.Equal("test", actual);
}
[Fact]
public void MatchesAny_MismatchedArray_NoMatches()
{
var cms = new PathMatchSet("test", "name");
string? actual = cms.MatchesAny(new string[] { "not" });
Assert.Null(actual);
}
#endregion
#region List
[Fact]
public void MatchesAll_NullList_NoMatches()
{
var cms = new PathMatchSet("test", "name");
var actual = cms.MatchesAll((List<string>?)null);
Assert.Empty(actual);
}
[Fact]
public void MatchesAll_EmptyList_NoMatches()
{
var cms = new PathMatchSet("test", "name");
var actual = cms.MatchesAll(new List<string>());
Assert.Empty(actual);
}
[Fact]
public void MatchesAll_MatchingList_Matches()
{
var cms = new PathMatchSet("test", "name");
var actual = cms.MatchesAll(new List<string> { "test" });
string path = Assert.Single(actual);
Assert.Equal("test", path);
}
[Fact]
public void MatchesAll_MismatchedList_NoMatches()
{
var cms = new PathMatchSet("test", "name");
var actual = cms.MatchesAll(new List<string> { "not" });
Assert.Empty(actual);
}
[Fact]
public void MatchesAny_NullList_NoMatches()
{
var cms = new PathMatchSet("test", "name");
string? actual = cms.MatchesAny((List<string>?)null);
Assert.Null(actual);
}
[Fact]
public void MatchesAny_EmptyList_NoMatches()
{
var cms = new PathMatchSet("test", "name");
string? actual = cms.MatchesAny(new List<string>());
Assert.Null(actual);
}
[Fact]
public void MatchesAny_MatchingList_Matches()
{
var cms = new PathMatchSet("test", "name");
string? actual = cms.MatchesAny(new List<string> { "test" });
Assert.Equal("test", actual);
}
[Fact]
public void MatchesAny_MismatchedList_NoMatches()
{
var cms = new PathMatchSet("test", "name");
string? actual = cms.MatchesAny(new List<string> { "not" });
Assert.Null(actual);
}
#endregion
#region Mock Delegates
/// <inheritdoc cref="GetPathVersion"/>
private static string? PathVersionMock(string path, List<string>? files) => null;
#endregion
}
}

View File

@@ -0,0 +1,293 @@
using System;
using System.Collections.Generic;
using System.IO;
using SabreTools.IO.Matching;
using Xunit;
namespace SabreTools.IO.Test.Matching
{
public class PathMatchTests
{
[Fact]
public void InvalidNeedle_ThrowsException()
{
Assert.Throws<InvalidDataException>(() => new PathMatch(string.Empty));
}
[Fact]
public void ImplicitOperatorArray_Success()
{
string needle = "test";
var pm = (PathMatch)needle;
Assert.NotNull(pm);
}
#region Array
[Fact]
public void NullArray_NoMatch()
{
var pm = new PathMatch("test");
string? actual = pm.Match((string[]?)null);
Assert.Null(actual);
}
[Fact]
public void EmptyArray_NoMatch()
{
var pm = new PathMatch("test");
string? actual = pm.Match(Array.Empty<string>());
Assert.Null(actual);
}
[Fact]
public void SingleItemArrayMatching_Match()
{
string needle = "test";
string[] stack = [needle];
var pm = new PathMatch(needle);
string? actual = pm.Match(stack);
Assert.Equal(needle, actual);
}
[Fact]
public void SingleItemArrayMismatched_NoMatch()
{
string needle = "test";
string[] stack = ["not"];
var pm = new PathMatch(needle);
string? actual = pm.Match(stack);
Assert.Null(actual);
}
[Fact]
public void MultiItemArrayMatching_Match()
{
string needle = "test";
string[] stack = ["not", needle, "far"];
var pm = new PathMatch(needle);
string? actual = pm.Match(stack);
Assert.Equal(needle, actual);
}
[Fact]
public void MultiItemArrayMismatched_NoMatch()
{
string needle = "test";
string[] stack = ["not", "too", "far"];
var pm = new PathMatch(needle);
string? actual = pm.Match(stack);
Assert.Null(actual);
}
#endregion
#region List
[Fact]
public void NullList_NoMatch()
{
var pm = new PathMatch("test");
string? actual = pm.Match((List<string>?)null);
Assert.Null(actual);
}
[Fact]
public void EmptyList_NoMatch()
{
var pm = new PathMatch("test");
string? actual = pm.Match(new List<string>());
Assert.Null(actual);
}
[Fact]
public void SingleItemListMatching_Match()
{
string needle = "test";
List<string> stack = [needle];
var pm = new PathMatch(needle);
string? actual = pm.Match(stack);
Assert.Equal(needle, actual);
}
[Fact]
public void SingleItemListMismatched_NoMatch()
{
string needle = "test";
List<string> stack = ["not"];
var pm = new PathMatch(needle);
string? actual = pm.Match(stack);
Assert.Null(actual);
}
[Fact]
public void MultiItemListMatching_Match()
{
string needle = "test";
List<string> stack = ["not", needle, "far"];
var pm = new PathMatch(needle);
string? actual = pm.Match(stack);
Assert.Equal(needle, actual);
}
[Fact]
public void MultiItemListMismatched_NoMatch()
{
string needle = "test";
List<string> stack = ["not", "too", "far"];
var pm = new PathMatch(needle);
string? actual = pm.Match(stack);
Assert.Null(actual);
}
#endregion
#region Match Case
[Fact]
public void MatchCaseEqual_Match()
{
string needle = "test";
List<string> stack = [needle];
var pm = new PathMatch(needle, matchCase: true);
string? actual = pm.Match(stack);
Assert.Equal(needle, actual);
}
[Fact]
public void NoMatchCaseEqual_Match()
{
string needle = "test";
List<string> stack = [needle];
var pm = new PathMatch(needle, matchCase: false);
string? actual = pm.Match(stack);
Assert.Equal(needle, actual);
}
[Fact]
public void MatchCaseInequal_NoMatch()
{
string needle = "test";
List<string> stack = [needle.ToUpperInvariant()];
var pm = new PathMatch(needle, matchCase: true);
string? actual = pm.Match(stack);
Assert.Null(actual);
}
[Fact]
public void NoMatchCaseInequal_Match()
{
string needle = "test";
List<string> stack = [needle.ToUpperInvariant()];
var pm = new PathMatch(needle, matchCase: false);
string? actual = pm.Match(stack);
Assert.Equal(needle.ToUpperInvariant(), actual);
}
[Fact]
public void MatchCaseContains_Match()
{
string needle = "test";
List<string> stack = [$"prefix_{needle}_postfix"];
var pm = new PathMatch(needle, matchCase: true);
string? actual = pm.Match(stack);
Assert.Equal($"prefix_{needle}_postfix", actual);
}
[Fact]
public void NoMatchCaseContains_Match()
{
string needle = "test";
List<string> stack = [$"prefix_{needle}_postfix"];
var pm = new PathMatch(needle, matchCase: false);
string? actual = pm.Match(stack);
Assert.Equal($"prefix_{needle}_postfix", actual);
}
#endregion
#region Use Ends With
[Fact]
public void EndsWithEqual_Match()
{
string needle = "test";
List<string> stack = [needle];
var pm = new PathMatch(needle, useEndsWith: true);
string? actual = pm.Match(stack);
Assert.Equal(needle, actual);
}
[Fact]
public void NoEndsWithEqual_Match()
{
string needle = "test";
List<string> stack = [needle];
var pm = new PathMatch(needle, useEndsWith: false);
string? actual = pm.Match(stack);
Assert.Equal(needle, actual);
}
[Fact]
public void EndsWithInequal_Match()
{
string needle = "test";
List<string> stack = [needle.ToUpperInvariant()];
var pm = new PathMatch(needle, useEndsWith: true);
string? actual = pm.Match(stack);
Assert.Equal(needle.ToUpperInvariant(), actual);
}
[Fact]
public void NoEndsWithInequal_Match()
{
string needle = "test";
List<string> stack = [needle.ToUpperInvariant()];
var pm = new PathMatch(needle, useEndsWith: false);
string? actual = pm.Match(stack);
Assert.Equal(needle.ToUpperInvariant(), actual);
}
[Fact]
public void EndsWithContains_NoMatch()
{
string needle = "test";
List<string> stack = [$"prefix_{needle}_postfix"];
var pm = new PathMatch(needle, useEndsWith: true);
string? actual = pm.Match(stack);
Assert.Null(actual);
}
[Fact]
public void NoEndsWithContains_Match()
{
string needle = "test";
List<string> stack = [$"prefix_{needle}_postfix"];
var pm = new PathMatch(needle, useEndsWith: false);
string? actual = pm.Match(stack);
Assert.Equal($"prefix_{needle}_postfix", actual);
}
#endregion
}
}

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

@@ -1,6 +1,5 @@
using System;
using System.IO;
using SabreTools.IO.Extensions;
using SabreTools.IO.Streams;
using Xunit;

View File

@@ -0,0 +1,92 @@
/*
*
* Links for info and original source code:
*
* https://blog.codinghorror.com/sorting-for-humans-natural-sort-order/
* http://www.codeproject.com/Articles/22517/Natural-Sort-Comparer
*
* Exact code implementation used with permission, originally by motoschifo
*
*/
using System;
using System.Collections.Generic;
using System.Text.RegularExpressions;
namespace SabreTools.Text.Compare
{
public class NaturalComparer : Comparer<string>, IDisposable
{
private readonly Dictionary<string, string[]> _table;
public NaturalComparer()
{
_table = [];
}
public void Dispose()
{
_table.Clear();
}
public override int Compare(string? x, string? y)
{
if (x == null || y == null)
{
if (x == null && y != null)
return -1;
else if (x != null && y == null)
return 1;
else
return 0;
}
if (x.ToLowerInvariant() == y.ToLowerInvariant())
return x.CompareTo(y);
if (!_table.TryGetValue(x, out string[]? x1))
{
//x1 = Regex.Split(x.Replace(" ", string.Empty), "([0-9]+)");
x1 = Regex.Split(x.ToLowerInvariant(), "([0-9]+)");
x1 = Array.FindAll(x1, s => !string.IsNullOrEmpty(s));
_table.Add(x, x1);
}
if (!_table.TryGetValue(y, out string[]? y1))
{
//y1 = Regex.Split(y.Replace(" ", string.Empty), "([0-9]+)");
y1 = Regex.Split(y.ToLowerInvariant(), "([0-9]+)");
y1 = Array.FindAll(y1, s => !string.IsNullOrEmpty(s));
_table.Add(y, y1);
}
for (int i = 0; i < x1.Length && i < y1.Length; i++)
{
if (x1[i] != y1[i])
return PartCompare(x1[i], y1[i]);
}
if (x1.Length > y1.Length)
return 1;
else if (y1.Length > x1.Length)
return -1;
else
return x.CompareTo(y);
}
private static int PartCompare(string left, string right)
{
if (!long.TryParse(left, out long x))
return NaturalComparerUtil.ComparePaths(left, right);
if (!long.TryParse(right, out long y))
return NaturalComparerUtil.ComparePaths(left, right);
// If we have an equal part, then make sure that "longer" ones are taken into account
if (x.CompareTo(y) == 0)
return left.Length - right.Length;
return x.CompareTo(y);
}
}
}

View File

@@ -0,0 +1,96 @@
namespace SabreTools.Text.Compare
{
internal static class NaturalComparerUtil
{
/// <summary>
/// Compare two strings by path parts
/// </summary>
public static int ComparePaths(string? left, string? right)
{
// If both strings are null, return
if (left == null && right == null)
return 0;
// If one is null, then say that's less than
if (left == null)
return -1;
if (right == null)
return 1;
// Normalize the path seperators
left = left.Replace('\\', '/');
right = right.Replace('\\', '/');
// Save the orginal adjusted strings
string leftOrig = left;
string rightOrig = right;
// Normalize strings by lower-case
left = leftOrig.ToLowerInvariant();
right = rightOrig.ToLowerInvariant();
// If the strings are the same exactly, return
if (left == right)
return leftOrig.CompareTo(rightOrig);
// Now split into path parts
string[] leftParts = left.Split('/');
string[] rightParts = right.Split('/');
// Then compare each part in turn
for (int i = 0; i < leftParts.Length && i < rightParts.Length; i++)
{
int partCompare = ComparePathSegment(leftParts[i], rightParts[i]);
if (partCompare != 0)
return partCompare;
}
// If we got out here, then it looped through at least one of the strings
if (leftParts.Length > rightParts.Length)
return 1;
if (leftParts.Length < rightParts.Length)
return -1;
return leftOrig.CompareTo(rightOrig);
}
/// <summary>
/// Compare two path segments deterministically
/// </summary>
private static int ComparePathSegment(string left, string right)
{
// If the lengths are both zero, they're equal
if (left.Length == 0 && right.Length == 0)
return 0;
// Shorter strings are sorted before
if (left.Length == 0)
return -1;
if (right.Length == 0)
return 1;
// Otherwise, loop through until we have an answer
for (int i = 0; i < left.Length && i < right.Length; i++)
{
// Get the next characters from the inputs as integers
int leftChar = left[i];
int rightChar = right[i];
// If the characters are the same, continue
if (leftChar == rightChar)
continue;
// If they're different, check which one was larger
return leftChar > rightChar ? 1 : -1;
}
// If we got out here, then it looped through at least one of the strings
if (left.Length > right.Length)
return 1;
if (left.Length < right.Length)
return -1;
return 0;
}
}
}

View File

@@ -0,0 +1,92 @@
/*
*
* Links for info and original source code:
*
* https://blog.codinghorror.com/sorting-for-humans-natural-sort-order/
* http://www.codeproject.com/Articles/22517/Natural-Sort-Comparer
*
* Exact code implementation used with permission, originally by motoschifo
*
*/
using System;
using System.Collections.Generic;
using System.Text.RegularExpressions;
namespace SabreTools.Text.Compare
{
public class NaturalReversedComparer : Comparer<string>, IDisposable
{
private readonly Dictionary<string, string[]> _table;
public NaturalReversedComparer()
{
_table = [];
}
public void Dispose()
{
_table.Clear();
}
public override int Compare(string? x, string? y)
{
if (x == null || y == null)
{
if (x == null && y != null)
return -1;
else if (x != null && y == null)
return 1;
else
return 0;
}
if (y.ToLowerInvariant() == x.ToLowerInvariant())
return y.CompareTo(x);
if (!_table.TryGetValue(x, out string[]? x1))
{
//x1 = Regex.Split(x.Replace(" ", string.Empty), "([0-9]+)");
x1 = Regex.Split(x.ToLowerInvariant(), "([0-9]+)");
x1 = Array.FindAll(x1, s => !string.IsNullOrEmpty(s));
_table.Add(x, x1);
}
if (!_table.TryGetValue(y, out string[]? y1))
{
//y1 = Regex.Split(y.Replace(" ", string.Empty), "([0-9]+)");
y1 = Regex.Split(y.ToLowerInvariant(), "([0-9]+)");
y1 = Array.FindAll(y1, s => !string.IsNullOrEmpty(s));
_table.Add(y, y1);
}
for (int i = 0; i < x1.Length && i < y1.Length; i++)
{
if (x1[i] != y1[i])
return PartCompare(x1[i], y1[i]);
}
if (y1.Length > x1.Length)
return 1;
else if (x1.Length > y1.Length)
return -1;
else
return y.CompareTo(x);
}
private static int PartCompare(string left, string right)
{
if (!long.TryParse(left, out long x))
return NaturalComparerUtil.ComparePaths(right, left);
if (!long.TryParse(right, out long y))
return NaturalComparerUtil.ComparePaths(right, left);
// If we have an equal part, then make sure that "longer" ones are taken into account
if (y.CompareTo(x) == 0)
return right.Length - left.Length;
return y.CompareTo(x);
}
}
}

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

@@ -1,7 +1,7 @@
using System;
using System.IO;
using SabreTools.Hashing;
using SabreTools.Matching;
using SabreTools.IO.Extensions;
namespace SabreTools.IO.Encryption
{

View File

@@ -2,6 +2,7 @@ using System;
using System.Collections.Generic;
using System.IO;
using System.Text;
using SabreTools.IO.Matching;
namespace SabreTools.IO.Extensions
{
@@ -15,6 +16,399 @@ namespace SabreTools.IO.Extensions
return array == null || array.Length == 0;
}
#region Matching
/// <summary>
/// Find all positions of one array in another, if possible
/// </summary>
/// <param name="stack">Byte array to search within</param>
/// <param name="needle">Byte array representing the search value</param>
/// <param name="start">Optional starting position in the stack, defaults to 0</param>
/// <param name="end">Optional ending position in the stack, defaults to -1 (length of stack)</param>
public static List<int> FindAllPositions(this byte[] stack, byte[] needle, int start = 0, int end = -1)
{
byte?[] nullableNeedle = Array.ConvertAll(needle, b => (byte?)b);
return FindAllPositions(stack, nullableNeedle, start, end);
}
/// <summary>
/// Find all positions of one array in another, if possible
/// </summary>
/// <param name="stack">Byte array to search within</param>
/// <param name="needle">Byte array representing the search value</param>
/// <param name="start">Optional starting position in the stack, defaults to 0</param>
/// <param name="end">Optional ending position in the stack, defaults to -1 (length of stack)</param>
public static List<int> FindAllPositions(this byte[] stack, byte?[] needle, int start = 0, int end = -1)
{
// Get the outgoing list
List<int> positions = [];
// If either set is null or empty
if (stack.Length == 0 || needle.Length == 0)
return positions;
// If the needle is longer than the stack
if (needle.Length > stack.Length)
return positions;
// Normalize the end value, if necessary
if (end == -1)
end = stack.Length;
// Validate the start and end values
if (start < 0 || start >= stack.Length)
return positions;
if (end < -1 || end < start || end > stack.Length)
return positions;
// Loop while there is data to check
while (start < end)
{
// Create a new matcher for this segment
var matcher = new ContentMatch(needle, start, end);
// Get the next matching position
int position = matcher.Match(stack, reverse: false);
if (position < 0)
break;
// Append the position and reset the start index
positions.Add(position);
start = position + 1;
}
return positions;
}
/// <summary>
/// Find the first position of one array in another, if possible
/// </summary>
/// <param name="stack">Byte array to search within</param>
/// <param name="needle">Byte array representing the search value</param>
/// <param name="start">Optional starting position in the stack, defaults to 0</param>
/// <param name="end">Optional ending position in the stack, defaults to -1 (length of stack)</param>
public static int FirstPosition(this byte[] stack, byte[] needle, int start = 0, int end = -1)
{
byte?[] nullableNeedle = Array.ConvertAll(needle, b => (byte?)b);
return FirstPosition(stack, nullableNeedle, start, end);
}
/// <summary>
/// Find the first position of one array in another, if possible
/// </summary>
/// <param name="stack">Byte array to search within</param>
/// <param name="needle">Byte array representing the search value</param>
/// <param name="start">Optional starting position in the stack, defaults to 0</param>
/// <param name="end">Optional ending position in the stack, defaults to -1 (length of stack)</param>
public static int FirstPosition(this byte[] stack, byte?[] needle, int start = 0, int end = -1)
{
// If either set is null or empty
if (stack.Length == 0 || needle.Length == 0)
return -1;
// If the needle is longer than the stack
if (needle.Length > stack.Length)
return -1;
var matcher = new ContentMatch(needle, start, end);
return matcher.Match(stack, reverse: false);
}
/// <summary>
/// Find the last position of one array in another, if possible
/// </summary>
/// <param name="stack">Byte array to search within</param>
/// <param name="needle">Byte array representing the search value</param>
/// <param name="start">Optional starting position in the stack, defaults to 0</param>
/// <param name="end">Optional ending position in the stack, defaults to -1 (length of stack)</param>
public static int LastPosition(this byte[] stack, byte[] needle, int start = 0, int end = -1)
{
byte?[] nullableNeedle = Array.ConvertAll(needle, b => (byte?)b);
return LastPosition(stack, nullableNeedle, start, end);
}
/// <summary>
/// Find the last position of one array in another, if possible
/// </summary>
/// <param name="stack">Byte array to search within</param>
/// <param name="needle">Byte array representing the search value</param>
/// <param name="start">Optional starting position in the stack, defaults to 0</param>
/// <param name="end">Optional ending position in the stack, defaults to -1 (length of stack)</param>
public static int LastPosition(this byte[] stack, byte?[] needle, int start = 0, int end = -1)
{
// If either set is null or empty
if (stack.Length == 0 || needle.Length == 0)
return -1;
// If the needle is longer than the stack
if (needle.Length > stack.Length)
return -1;
var matcher = new ContentMatch(needle, start, end);
return matcher.Match(stack, reverse: true);
}
/// <summary>
/// Check if a byte array exactly matches another
/// </summary>
/// <param name="stack">Byte array to search within</param>
/// <param name="needle">Byte array representing the search value</param>
public static bool EqualsExactly(this byte[] stack, byte[] needle)
{
byte?[] nullableNeedle = Array.ConvertAll(needle, b => (byte?)b);
return EqualsExactly(stack, nullableNeedle);
}
/// <summary>
/// Check if a byte array exactly matches another
/// </summary>
/// <param name="stack">Byte array to search within</param>
/// <param name="needle">Byte array representing the search value</param>
public static bool EqualsExactly(this byte[] stack, byte?[] needle)
{
// If either set is null or empty
if (stack.Length == 0 || needle.Length == 0)
return false;
// If the needle is not the exact length of the stack
if (needle.Length != stack.Length)
return false;
return FirstPosition(stack, needle, start: 0, end: 1) == 0;
}
/// <summary>
/// Check if a byte array starts with another
/// </summary>
/// <param name="stack">Byte array to search within</param>
/// <param name="needle">Byte array representing the search value</param>
public static bool StartsWith(this byte[] stack, byte[] needle)
{
byte?[] nullableNeedle = Array.ConvertAll(needle, b => (byte?)b);
return StartsWith(stack, nullableNeedle);
}
/// <summary>
/// Check if a byte array starts with another
/// </summary>
/// <param name="stack">Byte array to search within</param>
/// <param name="needle">Byte array representing the search value</param>
public static bool StartsWith(this byte[] stack, byte?[] needle)
{
// If either set is null or empty
if (stack.Length == 0 || needle.Length == 0)
return false;
// If the needle is longer than the stack
if (needle.Length > stack.Length)
return false;
return FirstPosition(stack, needle, start: 0, end: 1) > -1;
}
/// <summary>
/// Check if a byte array ends with another
/// </summary>
/// <param name="stack">Byte array to search within</param>
/// <param name="needle">Byte array representing the search value</param>
public static bool EndsWith(this byte[] stack, byte[] needle)
{
byte?[] nullableNeedle = Array.ConvertAll(needle, b => (byte?)b);
return EndsWith(stack, nullableNeedle);
}
/// <summary>
/// Check if a byte array ends with another
/// </summary>
/// <param name="stack">Byte array to search within</param>
/// <param name="needle">Byte array representing the search value</param>
public static bool EndsWith(this byte[] stack, byte?[] needle)
{
// If either set is null or empty
if (stack.Length == 0 || needle.Length == 0)
return false;
// If the needle is longer than the stack
if (needle.Length > stack.Length)
return false;
return FirstPosition(stack, needle, start: stack.Length - needle.Length) > -1;
}
#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>
/// Convert a byte array to a hex string
/// </summary>
@@ -183,8 +577,6 @@ namespace SabreTools.IO.Extensions
return strings;
}
#region Fixed Byte-Width Encoding Helpers
/// <summary>
/// Read string data from a byte array using an encoding with a fixed width
/// </summary>

View File

@@ -0,0 +1,13 @@
namespace SabreTools.IO.Interfaces
{
/// <summary>
/// Represents a matcher for a particular type
/// </summary>
public interface IMatch<T>
{
/// <summary>
/// Nullable typed data to be matched
/// </summary>
T? Needle { get; }
}
}

View File

@@ -0,0 +1,20 @@
using System.Collections.Generic;
namespace SabreTools.IO.Interfaces
{
/// <summary>
/// Wrapper for a single set of matching criteria
/// </summary>
public interface IMatchSet<T, U> where T : IMatch<U>
{
/// <summary>
/// Set of all matchers
/// </summary>
public List<T> Matchers { get; }
/// <summary>
/// Unique name for the match set
/// </summary>
public string SetName { get; }
}
}

363
SabreTools.IO/MatchUtil.cs Normal file
View File

@@ -0,0 +1,363 @@
using System.Collections.Generic;
using System.IO;
using System.Text;
using SabreTools.IO.Matching;
namespace SabreTools.IO
{
/// <summary>
/// Helper class for matching
/// </summary>
public static class MatchUtil
{
#region Array Content Matching
/// <summary>
/// Get all content matches for a given list of matchers
/// </summary>
/// <param name="file">File to check for matches</param>
/// <param name="stack">Array to search</param>
/// <param name="matchSets">List of ContentMatchSets to be run on the file</param>
/// <param name="any">True if any content match is a success, false if all have to match</param>
/// <param name="includeDebug">True to include positional data, false otherwise</param>
/// <returns>List of strings representing the matches, null or empty otherwise</returns>
public static List<string> GetAllMatches(string file,
byte[]? stack,
List<ContentMatchSet> matchSets,
bool any = false,
bool includeDebug = false)
=> FindAllMatches(file, stack, matchSets, any, includeDebug, false);
/// <summary>
/// Get first content match for a given list of matchers
/// </summary>
/// <param name="file">File to check for matches</param>
/// <param name="stack">Array to search</param>
/// <param name="matchSets">List of ContentMatchSets to be run on the file</param>
/// <param name="any">True if any content match is a success, false if all have to match</param>
/// <param name="includeDebug">True to include positional data, false otherwise</param>
/// <returns>String representing the match, null otherwise</returns>
public static string? GetFirstMatch(string file,
byte[]? stack,
List<ContentMatchSet> matchSets,
bool any = false,
bool includeDebug = false)
{
var contentMatches = FindAllMatches(file, stack, matchSets, any, includeDebug, true);
if (contentMatches == null || contentMatches.Count == 0)
return null;
return contentMatches[0];
}
/// <summary>
/// Get the required set of content matches on a per Matcher basis
/// </summary>
/// <param name="file">File to check for matches</param>
/// <param name="stack">Array to search</param>
/// <param name="matchSets">List of ContentMatchSets to be run on the file</param>
/// <param name="any">True if any content match is a success, false if all have to match</param>
/// <param name="includeDebug">True to include positional data, false otherwise</param>
/// <param name="stopAfterFirst">True to stop after the first match, false otherwise</param>
/// <returns>List of strings representing the matches, empty otherwise</returns>
private static List<string> FindAllMatches(string file,
byte[]? stack,
List<ContentMatchSet> matchSets,
bool any,
bool includeDebug,
bool stopAfterFirst)
{
// If either set is null or empty
if (stack == null || stack.Length == 0 || matchSets.Count == 0)
return [];
// Initialize the list of matches
var matchesList = new List<string>();
// Loop through and try everything otherwise
foreach (var matcher in matchSets)
{
// Determine if the matcher passes
List<int> positions = any
? [matcher.MatchesAny(stack)]
: matcher.MatchesAll(stack);
// If we don't have a pass, just continue
if (positions.Count == 0 || positions[0] == -1)
continue;
// Build the output string
var matchString = new StringBuilder();
matchString.Append(matcher.SetName);
// Invoke the version delegate, if it exists
if (matcher.GetArrayVersion != null)
{
// A null version returned means the check didn't pass at the version step
var version = matcher.GetArrayVersion(file, stack, positions);
if (version == null)
continue;
// Trim and add the version
version = version.Trim();
if (version.Length > 0)
matchString.Append($" {version}");
}
// Append the positional data if required
if (includeDebug)
{
string positionsString = string.Join(", ", [.. positions.ConvertAll(p => p.ToString())]);
matchString.Append($" (Index {positionsString})");
}
// Append the match to the list
matchesList.Add(matchString.ToString());
// If we're stopping after the first match, bail out here
if (stopAfterFirst)
return matchesList;
}
return matchesList;
}
#endregion
#region Stream Content Matching
/// <summary>
/// Get all content matches for a given list of matchers
/// </summary>
/// <param name="file">File to check for matches</param>
/// <param name="stack">Stream to search</param>
/// <param name="matchSets">List of ContentMatchSets to be run on the file</param>
/// <param name="any">True if any content match is a success, false if all have to match</param>
/// <param name="includeDebug">True to include positional data, false otherwise</param>
/// <returns>List of strings representing the matches, null or empty otherwise</returns>
public static List<string> GetAllMatches(string file,
Stream? stack,
List<ContentMatchSet> matchSets,
bool any = false,
bool includeDebug = false)
=> FindAllMatches(file, stack, matchSets, any, includeDebug, false);
/// <summary>
/// Get first content match for a given list of matchers
/// </summary>
/// <param name="file">File to check for matches</param>
/// <param name="stack">Stream to search</param>
/// <param name="matchSets">List of ContentMatchSets to be run on the file</param>
/// <param name="any">True if any content match is a success, false if all have to match</param>
/// <param name="includeDebug">True to include positional data, false otherwise</param>
/// <returns>String representing the match, null otherwise</returns>
public static string? GetFirstMatch(string file,
Stream? stack,
List<ContentMatchSet> matchSets,
bool any = false,
bool includeDebug = false)
{
var contentMatches = FindAllMatches(file, stack, matchSets, any, includeDebug, true);
if (contentMatches == null || contentMatches.Count == 0)
return null;
return contentMatches[0];
}
/// <summary>
/// Get the required set of content matches on a per Matcher basis
/// </summary>
/// <param name="file">File to check for matches</param>
/// <param name="stack">Stream to search</param>
/// <param name="matchSets">List of ContentMatchSets to be run on the file</param>
/// <param name="any">True if any content match is a success, false if all have to match</param>
/// <param name="includeDebug">True to include positional data, false otherwise</param>
/// <param name="stopAfterFirst">True to stop after the first match, false otherwise</param>
/// <returns>List of strings representing the matches, empty otherwise</returns>
private static List<string> FindAllMatches(string file,
Stream? stack,
List<ContentMatchSet> matchSets,
bool any,
bool includeDebug,
bool stopAfterFirst)
{
// If either set is null or empty
if (stack == null || stack.Length == 0 || matchSets.Count == 0)
return [];
// Initialize the list of matches
var matchesList = new List<string>();
// Loop through and try everything otherwise
foreach (var matcher in matchSets)
{
// Determine if the matcher passes
List<int> positions = any
? [matcher.MatchesAny(stack)]
: matcher.MatchesAll(stack);
// If we don't have a pass, just continue
if (positions.Count == 0 || positions[0] == -1)
continue;
// Build the output string
var matchString = new StringBuilder();
matchString.Append(matcher.SetName);
// Invoke the version delegate, if it exists
if (matcher.GetStreamVersion != null)
{
// A null version returned means the check didn't pass at the version step
var version = matcher.GetStreamVersion(file, stack, positions);
if (version == null)
continue;
// Trim and add the version
version = version.Trim();
if (version.Length > 0)
matchString.Append($" {version}");
}
// Append the positional data if required
if (includeDebug)
{
string positionsString = string.Join(", ", [.. positions.ConvertAll(p => p.ToString())]);
matchString.Append($" (Index {positionsString})");
}
// Append the match to the list
matchesList.Add(matchString.ToString());
// If we're stopping after the first match, bail out here
if (stopAfterFirst)
return matchesList;
}
return matchesList;
}
#endregion
#region Path Matching
/// <summary>
/// Get all path matches for a given list of matchers
/// </summary>
/// <param name="stack">File path to check for matches</param>
/// <param name="matchSets">List of PathMatchSets to be run on the file</param>
/// <param name="any">True if any path match is a success, false if all have to match</param>
/// <returns>List of strings representing the matches, null or empty otherwise</returns>
public static List<string> GetAllMatches(string stack, List<PathMatchSet> matchSets, bool any = false)
=> FindAllMatches([stack], matchSets, any, false);
/// <summary>
/// Get all path matches for a given list of matchers
/// </summary>
/// <param name="files">File paths to check for matches</param>
/// <param name="matchSets">List of PathMatchSets to be run on the file</param>
/// <param name="any">True if any path match is a success, false if all have to match</param>
/// <returns>List of strings representing the matches, null or empty otherwise</returns>
public static List<string> GetAllMatches(List<string>? stack, List<PathMatchSet> matchSets, bool any = false)
=> FindAllMatches(stack, matchSets, any, false);
/// <summary>
/// Get first path match for a given list of matchers
/// </summary>
/// <param name="stack">File path to check for matches</param>
/// <param name="matchSets">List of PathMatchSets to be run on the file</param>
/// <param name="any">True if any path match is a success, false if all have to match</param>
/// <returns>String representing the match, null otherwise</returns>
public static string? GetFirstMatch(string stack, List<PathMatchSet> matchSets, bool any = false)
{
var contentMatches = FindAllMatches([stack], matchSets, any, true);
if (contentMatches == null || contentMatches.Count == 0)
return null;
return contentMatches[0];
}
/// <summary>
/// Get first path match for a given list of matchers
/// </summary>
/// <param name="stack">File paths to check for matches</param>
/// <param name="matchSets">List of PathMatchSets to be run on the file</param>
/// <param name="any">True if any path match is a success, false if all have to match</param>
/// <returns>String representing the match, null otherwise</returns>
public static string? GetFirstMatch(List<string> stack, List<PathMatchSet> matchSets, bool any = false)
{
var contentMatches = FindAllMatches(stack, matchSets, any, true);
if (contentMatches == null || contentMatches.Count == 0)
return null;
return contentMatches[0];
}
/// <summary>
/// Get the required set of path matches on a per Matcher basis
/// </summary>
/// <param name="stack">File paths to check for matches</param>
/// <param name="matchSets">List of PathMatchSets to be run on the file</param>
/// <param name="any">True if any path match is a success, false if all have to match</param>
/// <param name="stopAfterFirst">True to stop after the first match, false otherwise</param>
/// <returns>List of strings representing the matches, null or empty otherwise</returns>
private static List<string> FindAllMatches(List<string>? stack, List<PathMatchSet> matchSets, bool any, bool stopAfterFirst)
{
// If either set is null or empty
if (stack == null || stack.Count == 0 || matchSets.Count == 0)
return [];
// Initialize the list of matches
var matchesList = new List<string>();
// Loop through and try everything otherwise
foreach (var matcher in matchSets)
{
// Determine if the matcher passes
List<string> matches = [];
if (any)
{
string? anyMatch = matcher.MatchesAny(stack);
if (anyMatch != null)
matches = [anyMatch];
}
else
{
matches = matcher.MatchesAll(stack);
}
// If we don't have a pass, just continue
if (matches.Count == 0)
continue;
// Build the output string
var matchString = new StringBuilder();
matchString.Append(matcher.SetName);
// Invoke the version delegate, if it exists
if (matcher.GetVersion != null)
{
// A null version returned means the check didn't pass at the version step
var version = matcher.GetVersion(matches[0], stack);
if (version == null)
continue;
// Trim and add the version
version = version.Trim();
if (version.Length > 0)
matchString.Append($" {version}");
}
// Append the match to the list
matchesList.Add(matchString.ToString());
// If we're stopping after the first match, bail out here
if (stopAfterFirst)
return matchesList;
}
return matchesList;
}
#endregion
}
}

View File

@@ -0,0 +1,333 @@
using System;
using System.IO;
using SabreTools.IO.Interfaces;
namespace SabreTools.IO.Matching
{
/// <summary>
/// Content matching criteria
/// </summary>
public class ContentMatch : IMatch<byte?[]>
{
/// <summary>
/// Content to match
/// </summary>
public byte?[] Needle { get; }
/// <summary>
/// Starting index for matching
/// </summary>
private readonly int _start;
/// <summary>
/// Ending index for matching
/// </summary>
private readonly int _end;
/// <summary>
/// Constructor
/// </summary>
/// <param name="needle">Byte array representing the search</param>
/// <param name="start">Optional starting position in the stack, defaults to 0</param>
/// <param name="end">Optional ending position in the stack, defaults to -1 (length of stack)</param>
public ContentMatch(byte[] needle, int start = 0, int end = -1)
{
// Validate the inputs
if (needle.Length == 0)
throw new InvalidDataException(nameof(needle));
if (start < 0)
throw new ArgumentOutOfRangeException(nameof(start));
if (end < -1)
throw new ArgumentOutOfRangeException(nameof(end));
Needle = Array.ConvertAll(needle, b => (byte?)b);
_start = start;
_end = end;
}
/// <summary>
/// Constructor
/// </summary>
/// <param name="needle">Nullable byte array representing the search</param>
/// <param name="start">Optional starting position in the stack, defaults to 0</param>
/// <param name="end">Optional ending position in the stack, defaults to -1 (length of stack)</param>
public ContentMatch(byte?[] needle, int start = 0, int end = -1)
{
// Validate the inputs
if (needle.Length == 0)
throw new InvalidDataException(nameof(needle));
if (start < 0)
throw new ArgumentOutOfRangeException(nameof(start));
if (end < -1)
throw new ArgumentOutOfRangeException(nameof(end));
Needle = needle;
_start = start;
_end = end;
}
#region Conversion
/// <summary>
/// Allow conversion from byte array to ContentMatch
/// </summary>
public static implicit operator ContentMatch(byte[] needle) => new ContentMatch(needle);
/// <summary>
/// Allow conversion from nullable byte array to ContentMatch
/// </summary>
public static implicit operator ContentMatch(byte?[] needle) => new ContentMatch(needle);
#endregion
#region Array Matching
/// <summary>
/// Get if this match can be found in a stack
/// </summary>
/// <param name="stack">Array to search for the given content</param>
/// <param name="reverse">True to search from the end of the array, false from the start</param>
/// <returns>Found position on success, -1 otherwise</returns>
public int Match(byte[]? stack, bool reverse = false)
{
// If either set is null or empty
if (stack == null || stack.Length == 0 || Needle.Length == 0)
return -1;
// Get the adjusted end value for comparison
int end = _end < 0 ? stack.Length : _end;
end = end > stack.Length ? stack.Length : end;
// If the stack window is invalid
if (end < _start)
return -1;
// If the needle is larger than the stack window, it can't be contained within
if (Needle.Length > stack.Length - _start)
return -1;
// If the needle and stack window are identically sized, short-circuit
if (Needle.Length == stack.Length - _start)
return EqualAt(stack, _start) ? _start : -1;
// Return based on the direction of search
return reverse ? MatchReverse(stack) : MatchForward(stack);
}
/// <summary>
/// Match within a stack starting from the smallest index
/// </summary>
/// <param name="stack">Array to search for the given content</param>
/// <returns>Found position on success, -1 otherwise</returns>
private int MatchForward(byte[] stack)
{
// Set the default start and end values
int start = _start < 0 ? 0 : _start;
int end = _end < 0 ? stack.Length - Needle.Length : _end;
// Loop starting from the smallest index
for (int i = start; i < end; i++)
{
// If we somehow have an invalid end and we haven't matched, return
if (i > stack.Length)
return -1;
// Check to see if the values are equal
if (EqualAt(stack, i))
return i;
}
return -1;
}
/// <summary>
/// Match within a stack starting from the largest index
/// </summary>
/// <param name="stack">Array to search for the given content</param>
/// <returns>Found position on success, -1 otherwise</returns>
private int MatchReverse(byte[] stack)
{
// Set the default start and end values
int start = _start < 0 ? 0 : _start;
int end = _end < 0 ? stack.Length - Needle.Length : _end;
// Loop starting from the largest index
for (int i = end; i > start; i--)
{
// If we somehow have an invalid end and we haven't matched, return
if (i > stack.Length)
return -1;
// Check to see if the values are equal
if (EqualAt(stack, i))
return i;
}
return -1;
}
/// <summary>
/// Get if a stack at a certain index is equal to a needle
/// </summary>
/// <param name="stack">Array to search for the given content</param>
/// <param name="index">Starting index to check equality</param>
/// <returns>True if the needle matches the stack at a given index</returns>
private bool EqualAt(byte[] stack, int index)
{
// If the index is invalid, we can't do anything
if (index < 0)
return false;
// If we're too close to the end of the stack, return false
if (Needle.Length > stack.Length - index)
return false;
// Loop through and check the value
for (int i = 0; i < Needle.Length; i++)
{
// A null value is a wildcard
if (Needle[i] == null)
continue;
else if (stack[i + index] != Needle[i])
return false;
}
return true;
}
#endregion
#region Stream Matching
/// <summary>
/// Get if this match can be found in a stack
/// </summary>
/// <param name="stack">Stream to search for the given content</param>
/// <param name="reverse">True to search from the end of the array, false from the start</param>
/// <returns>Found position on success, -1 otherwise</returns>
public int Match(Stream? stack, bool reverse = false)
{
// If either set is null or empty
if (stack == null || stack.Length == 0 || Needle.Length == 0)
return -1;
// Get the adjusted end value for comparison
int end = _end < 0 ? (int)stack.Length : _end;
end = end > (int)stack.Length ? (int)stack.Length : end;
// If the stack window is invalid
if (end < _start)
return -1;
// If the needle is larger than the stack window, it can't be contained within
if (Needle.Length > stack.Length - _start)
return -1;
// If the needle and stack window are identically sized, short-circuit
if (Needle.Length == stack.Length - _start)
return EqualAt(stack, _start) ? _start : -1;
// Return based on the direction of search
return reverse ? MatchReverse(stack) : MatchForward(stack);
}
/// <summary>
/// Match within a stack starting from the smallest index
/// </summary>
/// <param name="stack">Stream to search for the given content</param>
/// <returns>Found position on success, -1 otherwise</returns>
private int MatchForward(Stream stack)
{
// Set the default start and end values
int start = _start < 0 ? 0 : _start;
int end = _end < 0 ? (int)stack.Length - Needle.Length : _end;
// Loop starting from the smallest index
for (int i = start; i < end; i++)
{
// If we somehow have an invalid end and we haven't matched, return
if (i > stack.Length)
return -1;
// Check to see if the values are equal
if (EqualAt(stack, i))
return i;
}
return -1;
}
/// <summary>
/// Match within a stack starting from the largest index
/// </summary>
/// <param name="stack">Stream to search for the given content</param>
/// <returns>Found position on success, -1 otherwise</returns>
private int MatchReverse(Stream stack)
{
// Set the default start and end values
int start = _start < 0 ? 0 : _start;
int end = _end < 0 ? (int)stack.Length - Needle.Length : _end;
// Loop starting from the largest index
for (int i = end; i > start; i--)
{
// If we somehow have an invalid end and we haven't matched, return
if (i > stack.Length)
return -1;
// Check to see if the values are equal
if (EqualAt(stack, i))
return i;
}
return -1;
}
/// <summary>
/// Get if a stack at a certain index is equal to a needle
/// </summary>
/// <param name="stack">Stream to search for the given content</param>
/// <param name="index">Starting index to check equality</param>
/// <returns>True if the needle matches the stack at a given index</returns>
private bool EqualAt(Stream stack, int index)
{
// If the index is invalid, we can't do anything
if (index < 0)
return false;
// If we're too close to the end of the stack, return false
if (Needle.Length > stack.Length - index)
return false;
// Save the current position and move to the index
long currentPosition = stack.Position;
stack.Seek(index, SeekOrigin.Begin);
// Set the return value
bool matched = true;
// Loop through and check the value
for (int i = 0; i < Needle.Length; i++)
{
byte stackValue = (byte)stack.ReadByte();
// A null value is a wildcard
if (Needle[i] == null)
{
continue;
}
else if (stackValue != Needle[i])
{
matched = false;
break;
}
}
// Reset the position and return the value
stack.Seek(currentPosition, SeekOrigin.Begin);
return matched;
}
#endregion
}
}

View File

@@ -0,0 +1,239 @@
using System.Collections.Generic;
using System.IO;
using SabreTools.IO.Interfaces;
namespace SabreTools.IO.Matching
{
/// <summary>
/// A set of content matches that work together
/// </summary>
public class ContentMatchSet : IMatchSet<ContentMatch, byte?[]>
{
/// <inheritdoc/>
public List<ContentMatch> Matchers { get; }
/// <inheritdoc/>
public string SetName { get; }
/// <summary>
/// Function to get a content version
/// </summary>
/// <remarks>
/// A content version method takes the file path, the file contents,
/// and a list of found positions and returns a single string. That
/// string is either a version string, in which case it will be appended
/// to the match name, or `null`, in which case it will cause
/// the match name to be omitted.
/// </remarks>
public GetArrayVersion? GetArrayVersion { get; }
/// <summary>
/// Function to get a content version
/// </summary>
/// <remarks>
/// A content version method takes the file path, the file contents,
/// and a list of found positions and returns a single string. That
/// string is either a version string, in which case it will be appended
/// to the match name, or `null`, in which case it will cause
/// the match name to be omitted.
/// </remarks>
public GetStreamVersion? GetStreamVersion { get; }
#region Generic Constructors
/// <summary>
/// Constructor
/// </summary>
/// <param name="needle">ContentMatch representing the comparisons</param>
/// <param name="setName">Unique name for the set</param>
public ContentMatchSet(ContentMatch needle, string setName)
: this([needle], setName) { }
/// <summary>
/// Constructor
/// </summary>
/// <param name="needles">List of ContentMatch objects representing the comparisons</param>
/// <param name="setName">Unique name for the set</param>
public ContentMatchSet(List<ContentMatch> needles, string setName)
{
// Validate the inputs
if (needles.Count == 0)
throw new InvalidDataException(nameof(needles));
Matchers = needles;
SetName = setName;
GetArrayVersion = null;
GetStreamVersion = null;
}
#endregion
#region Array Constructors
/// <summary>
/// Constructor
/// </summary>
/// <param name="needle">ContentMatch representing the comparisons</param>
/// <param name="getVersion">Delegate for deriving a version on match of an array</param>
/// <param name="setName">Unique name for the set</param>
public ContentMatchSet(ContentMatch needle, GetArrayVersion getVersion, string setName)
: this([needle], getVersion, setName) { }
/// <summary>
/// Constructor
/// </summary>
/// <param name="needles">List of ContentMatch objects representing the comparisons</param>
/// <param name="getVersion">Delegate for deriving a version on match of an array</param>
/// <param name="setName">Unique name for the set</param>
public ContentMatchSet(List<ContentMatch> needles, GetArrayVersion getVersion, string setName)
{
// Validate the inputs
if (needles.Count == 0)
throw new InvalidDataException(nameof(needles));
Matchers = needles;
SetName = setName;
GetArrayVersion = getVersion;
GetStreamVersion = null;
}
#endregion
#region Stream Constructors
/// <summary>
/// Constructor
/// </summary>
/// <param name="needle">ContentMatch representing the comparisons</param>
/// <param name="getVersion">Delegate for deriving a version on match of a Stream</param>
/// <param name="setName">Unique name for the set</param>
public ContentMatchSet(ContentMatch needle, GetStreamVersion getVersion, string setName)
: this([needle], getVersion, setName) { }
/// <summary>
/// Constructor
/// </summary>
/// <param name="needles">List of ContentMatch objects representing the comparisons</param>
/// <param name="getVersion">Delegate for deriving a version on match of a Stream</param>
/// <param name="setName">Unique name for the set</param>
public ContentMatchSet(List<ContentMatch> needles, GetStreamVersion getVersion, string setName)
{
// Validate the inputs
if (needles.Count == 0)
throw new InvalidDataException(nameof(needles));
Matchers = needles;
SetName = setName;
GetArrayVersion = null;
GetStreamVersion = getVersion;
}
#endregion
#region Array Matching
/// <summary>
/// Determine whether all content matches pass
/// </summary>
/// <param name="stack">Array to search</param>
/// <returns>List of matching positions, if any</returns>
public List<int> MatchesAll(byte[]? stack)
{
// If either set is null or empty
if (stack == null || stack.Length == 0 || Matchers.Count == 0)
return [];
// Initialize the position list
var positions = new List<int>();
// Loop through all content matches and make sure all pass
foreach (var contentMatch in Matchers)
{
int position = contentMatch.Match(stack);
if (position < 0)
return [];
positions.Add(position);
}
return positions;
}
/// <summary>
/// Determine whether any content matches pass
/// </summary>
/// <param name="stack">Array to search</param>
/// <returns>First matching position on success, -1 on error</returns>
public int MatchesAny(byte[]? stack)
{
// If either set is null or empty
if (stack == null || stack.Length == 0 || Matchers.Count == 0)
return -1;
// Loop through all content matches and make sure all pass
foreach (var contentMatch in Matchers)
{
int position = contentMatch.Match(stack);
if (position >= 0)
return position;
}
return -1;
}
#endregion
#region Stream Matching
/// <summary>
/// Determine whether all content matches pass
/// </summary>
/// <param name="stack">Stream to search</param>
/// <returns>List of matching positions, if any</returns>
public List<int> MatchesAll(Stream? stack)
{
// If either set is null or empty
if (stack == null || stack.Length == 0 || Matchers.Count == 0)
return [];
// Initialize the position list
var positions = new List<int>();
// Loop through all content matches and make sure all pass
foreach (var contentMatch in Matchers)
{
int position = contentMatch.Match(stack);
if (position < 0)
return [];
positions.Add(position);
}
return positions;
}
/// <summary>
/// Determine whether any content matches pass
/// </summary>
/// <param name="stack">Stream to search</param>
/// <returns>First matching position on success, -1 on error</returns>
public int MatchesAny(Stream? stack)
{
// If either set is null or empty
if (stack == null || stack.Length == 0 || Matchers.Count == 0)
return -1;
// Loop through all content matches and make sure all pass
foreach (var contentMatch in Matchers)
{
int position = contentMatch.Match(stack);
if (position >= 0)
return position;
}
return -1;
}
#endregion
}
}

View File

@@ -0,0 +1,31 @@
using System.Collections.Generic;
using System.IO;
namespace SabreTools.IO.Matching
{
/// <summary>
/// Get a version number from a file
/// </summary>
/// <param name="path">File path to get the version from</param>
/// <param name="content">Optional file contents as a byte array</param>
/// <param name="positions">List of positions in the array that were matched</param>
/// <returns>Version string on success, null on failure</returns>
public delegate string? GetArrayVersion(string path, byte[]? content, List<int> positions);
/// <summary>
/// Get a version number from an input path
/// </summary>
/// <param name="path">File or directory path to get the version from</param>
/// <param name="files">Optional set of files in the directory</param>
/// <returns>Version string on success, null on failure</returns>
public delegate string? GetPathVersion(string path, List<string>? files);
/// <summary>
/// Get a version number from a file
/// </summary>
/// <param name="path">File path to get the version from</param>
/// <param name="content">Optional file contents as a Stream</param>
/// <param name="positions">List of positions in the Stream that were matched</param>
/// <returns>Version string on success, null on failure</returns>
public delegate string? GetStreamVersion(string path, Stream? content, List<int> positions);
}

View File

@@ -0,0 +1,18 @@
using System.IO;
namespace SabreTools.IO.Matching
{
/// <summary>
/// File path matching criteria
/// </summary>
public class FilePathMatch : PathMatch
{
/// <summary>
/// Constructor
/// </summary>
/// <param name="needle">String representing the search</param>
/// <param name="matchCase">True to match exact casing, false otherwise</param>
public FilePathMatch(string needle, bool matchCase = false)
: base($"{Path.DirectorySeparatorChar}{needle}", matchCase, true) { }
}
}

View File

@@ -0,0 +1,93 @@
using System.Collections.Generic;
using System.IO;
using SabreTools.IO.Interfaces;
namespace SabreTools.IO.Matching
{
/// <summary>
/// Path matching criteria
/// </summary>
public class PathMatch : IMatch<string>
{
/// <summary>
/// String to match
/// </summary>
public string Needle { get; }
/// <summary>
/// Match casing instead of invariant
/// </summary>
private readonly bool _matchCase;
/// <summary>
/// Match that values end with the needle and not just contains
/// </summary>
private readonly bool _useEndsWith;
/// <summary>
/// Constructor
/// </summary>
/// <param name="needle">String representing the search</param>
/// <param name="matchCase">True to match exact casing, false otherwise</param>
/// <param name="useEndsWith">True to match the end only, false for contains</param>
public PathMatch(string needle, bool matchCase = false, bool useEndsWith = false)
{
// Validate the inputs
if (needle.Length == 0)
throw new InvalidDataException(nameof(needle));
Needle = needle;
_matchCase = matchCase;
_useEndsWith = useEndsWith;
}
#region Conversion
/// <summary>
/// Allow conversion from string to PathMatch
/// </summary>
public static implicit operator PathMatch(string needle) => new PathMatch(needle);
#endregion
#region Matching
/// <summary>
/// Get if this match can be found in a stack
/// </summary>
/// <param name="stack">Array of strings to search for the given content</param>
/// <returns>Matched item on success, null on error</returns>
public string? Match(string[]? stack)
=> Match(stack == null ? null : new List<string>(stack));
/// <summary>
/// Get if this match can be found in a stack
/// </summary>
/// <param name="stack">List of strings to search for the given content</param>
/// <returns>Matched item on success, null on error</returns>
public string? Match(List<string>? stack)
{
// If either set is null or empty
if (stack == null || stack.Count == 0 || Needle.Length == 0)
return null;
// Preprocess the needle, if necessary
string procNeedle = _matchCase ? Needle : Needle.ToLowerInvariant();
foreach (string stackItem in stack)
{
// Preprocess the stack item, if necessary
string procStackItem = _matchCase ? stackItem : stackItem.ToLowerInvariant();
if (_useEndsWith && procStackItem.EndsWith(procNeedle))
return stackItem;
else if (!_useEndsWith && procStackItem.Contains(procNeedle))
return stackItem;
}
return null;
}
#endregion
}
}

View File

@@ -0,0 +1,156 @@
using System.Collections.Generic;
using System.IO;
using SabreTools.IO.Interfaces;
namespace SabreTools.IO.Matching
{
/// <summary>
/// A set of path matches that work together
/// </summary>
public class PathMatchSet : IMatchSet<PathMatch, string>
{
/// <inheritdoc/>
public List<PathMatch> Matchers { get; }
/// <inheritdoc/>
public string SetName { get; }
/// <summary>
/// Function to get a path version for this Matcher
/// </summary>
/// <remarks>
/// A path version method takes the matched path and an enumerable of files
/// and returns a single string. That string is either a version string,
/// in which case it will be appended to the match name, or `null`,
/// in which case it will cause the match name to be omitted.
/// </remarks>
public GetPathVersion? GetVersion { get; }
#region Generic Constructors
/// <summary>
/// Constructor
/// </summary>
/// <param name="needle">PathMatch representing the comparisons</param>
/// <param name="setName">Unique name for the set</param>
public PathMatchSet(PathMatch needle, string setName)
: this([needle], setName) { }
/// <summary>
/// Constructor
/// </summary>
/// <param name="needles">List of PathMatch objects representing the comparisons</param>
/// <param name="setName">Unique name for the set</param>
public PathMatchSet(List<PathMatch> needles, string setName)
{
// Validate the inputs
if (needles.Count == 0)
throw new InvalidDataException(nameof(needles));
Matchers = needles;
SetName = setName;
GetVersion = null;
}
#endregion
#region Version Constructors
/// <summary>
/// Constructor
/// </summary>
/// <param name="needle">PathMatch representing the comparisons</param>
/// <param name="getVersion">Delegate for deriving a version on match</param>
/// <param name="setName">Unique name for the set</param>
public PathMatchSet(PathMatch needle, GetPathVersion getVersion, string setName)
: this([needle], getVersion, setName) { }
/// <summary>
/// Constructor
/// </summary>
/// <param name="needles">List of PathMatch objects representing the comparisons</param>
/// <param name="getVersion">Delegate for deriving a version on match</param>
/// <param name="setName">Unique name for the set</param>
public PathMatchSet(List<PathMatch> needles, GetPathVersion getVersion, string setName)
{
// Validate the inputs
if (needles.Count == 0)
throw new InvalidDataException(nameof(needles));
Matchers = needles;
SetName = setName;
GetVersion = getVersion;
}
#endregion
#region Matching
/// <summary>
/// Get if this match can be found in a stack
/// </summary>
/// <param name="stack">List of strings to search for the given content</param>
/// <returns>Matched item on success, null on error</returns>
public List<string> MatchesAll(string[]? stack)
=> MatchesAll(stack == null ? null : new List<string>(stack));
/// <summary>
/// Determine whether all path matches pass
/// </summary>
/// <param name="stack">List of strings to try to match</param>
/// <returns>List of matching values, if any</returns>
public List<string> MatchesAll(List<string>? stack)
{
// If either set is null or empty, we can't do anything
if (stack == null || stack.Count == 0 || Matchers.Count == 0)
return [];
// Initialize the value list
List<string> values = [];
// Loop through all path matches and make sure all pass
foreach (var pathMatch in Matchers)
{
string? value = pathMatch.Match(stack);
if (value == null)
return [];
else
values.Add(value);
}
return values;
}
/// <summary>
/// Get if this match can be found in a stack
/// </summary>
/// <param name="stack">List of strings to search for the given content</param>
/// <returns>Matched item on success, null on error</returns>
public string? MatchesAny(string[]? stack)
=> MatchesAny(stack == null ? null : new List<string>(stack));
/// <summary>
/// Determine whether any path matches pass
/// </summary>
/// <param name="stack">List of strings to try to match</param>
/// <returns>First matching value on success, null on error</returns>
public string? MatchesAny(List<string>? stack)
{
// If either set is null or empty, we can't do anything
if (stack == null || stack.Count == 0 || Matchers.Count == 0)
return null;
// Loop through all path matches and make sure all pass
foreach (var pathMatch in Matchers)
{
string? value = pathMatch.Match(stack);
if (value != null)
return value;
}
return null;
}
#endregion
}
}

View File

@@ -3,7 +3,7 @@ using System.Collections.Generic;
using System.IO;
using System.Text.RegularExpressions;
using SabreTools.IO.Extensions;
using SabreTools.Matching.Compare;
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.4</Version>
<Version>1.7.6</Version>
<!-- Package Properties -->
<Authors>Matt Nadareski</Authors>
@@ -30,8 +30,13 @@
</ItemGroup>
<ItemGroup>
<PackageReference Include="SabreTools.Matching" Version="1.6.0" />
<PackageReference Include="SabreTools.Hashing" Version="1.5.0" />
<InternalsVisibleTo Include="SabreTools.IO.Test" />
</ItemGroup>
<ItemGroup>
<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>