28 Commits
1.7.3 ... 1.7.5

Author SHA1 Message Date
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
Matt Nadareski
5054aeb077 Bump version 2025-09-22 17:48:18 -04:00
Matt Nadareski
d2e9b8d6e5 Fix byte array test 2025-09-22 17:44:41 -04:00
Matt Nadareski
2c29aee834 Remove Models from references 2025-09-22 11:03:40 -04:00
Matt Nadareski
576bafcb87 Create minimal model for InflateWrapper 2025-09-22 11:03:07 -04:00
Matt Nadareski
2b310ac528 SZDD no longer uses models 2025-09-22 10:55:14 -04:00
Matt Nadareski
4f6b6d7b59 Reduce Models use another notch 2025-09-22 10:52:26 -04:00
Matt Nadareski
17e55ee233 Move BufferedStream out of SZDD 2025-09-22 10:50:53 -04:00
Matt Nadareski
8b78906d1d Move MoPaQ encryption constants from Models 2025-09-22 10:37:27 -04:00
Matt Nadareski
cff2dcf4cc Move LZX models from Models 2025-09-22 10:35:04 -04:00
Matt Nadareski
a56942cb73 Move Quantum compression models from Models 2025-09-22 10:31:14 -04:00
Matt Nadareski
5ed661b77c Move MSZIP "model" from Models 2025-09-22 10:27:03 -04:00
Matt Nadareski
a0a0cd0386 Add more complete UTF-8 first-byte tests 2025-09-21 16:34:46 -04:00
Matt Nadareski
bcc0fca4ad Ensure 7-bit ASCII never reads above 0x7F 2025-09-21 16:27:03 -04:00
Matt Nadareski
843e821e5f Use extended check in slow path too 2025-09-21 16:07:20 -04:00
Matt Nadareski
630b01283e Latin1 instead of ASCII for .NET 5.0 and beyond 2025-09-21 15:12:16 -04:00
Matt Nadareski
22abb96013 Add remarks about what encodings are used 2025-09-21 15:02:33 -04:00
Matt Nadareski
314de12661 Fix tests, remove UTF-8 checks from irrelevant places 2025-09-21 14:03:45 -04:00
Matt Nadareski
a0b24031b5 Remove duplicate code from Stream implementation 2025-09-21 13:58:46 -04:00
Matt Nadareski
b4628485c3 Sync stream implementation with byte one 2025-09-21 13:58:13 -04:00
Matt Nadareski
4610ddc9b9 Don't read the string unless it's long enough 2025-09-21 13:53:16 -04:00
Matt Nadareski
e392ddc8d7 Fix code formatting 2025-09-21 13:52:05 -04:00
Matt Nadareski
1908d1b32e More generically support single-byte encodings 2025-09-21 13:50:08 -04:00
Matt Nadareski
9d73195f86 Big-endian unicode support because it's there 2025-09-21 13:42:05 -04:00
Matt Nadareski
335a486f17 Special handling of empty string builders 2025-09-21 13:41:06 -04:00
Matt Nadareski
d3e41ac187 Handle invalid offsets in byte array extensions 2025-09-21 11:43:07 -04:00
57 changed files with 4545 additions and 233 deletions

View File

@@ -0,0 +1,38 @@
using System;
using System.Linq;
using SabreTools.IO.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.IO.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.IO.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,394 @@ 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 ToHexString
[Fact]
@@ -143,27 +531,23 @@ namespace SabreTools.IO.Test.Extensions
];
var actual = arr.ReadStringsFrom(4);
Assert.NotNull(actual);
// ASCII and UTF-8 are identical for the character range
Assert.Equal(2, actual.Count);
}
[Fact]
public void ReadStringsFrom_UTF8_Filled()
public void ReadStringsFrom_Latin1Strings_Filled()
{
byte[]? arr =
[
.. Encoding.UTF8.GetBytes("TEST"),
.. Encoding.Latin1.GetBytes("TEST"),
.. new byte[] { 0x00 },
.. Encoding.UTF8.GetBytes("TWO"),
.. Encoding.Latin1.GetBytes("TWO"),
.. new byte[] { 0x00 },
.. Encoding.UTF8.GetBytes("DATA"),
.. Encoding.Latin1.GetBytes("DATA"),
.. new byte[] { 0x00 },
];
var actual = arr.ReadStringsFrom(4);
Assert.NotNull(actual);
// ASCII and UTF-8 are identical for the character range
Assert.Equal(2, actual.Count);
}
@@ -195,11 +579,11 @@ namespace SabreTools.IO.Test.Extensions
.. new byte[] { 0x00 },
.. Encoding.ASCII.GetBytes("DATA1"),
.. new byte[] { 0x00 },
.. Encoding.UTF8.GetBytes("TEST2"),
.. Encoding.Latin1.GetBytes("TEST2"),
.. new byte[] { 0x00 },
.. Encoding.UTF8.GetBytes("TWO2"),
.. Encoding.Latin1.GetBytes("TWO2"),
.. new byte[] { 0x00 },
.. Encoding.UTF8.GetBytes("DATA2"),
.. Encoding.Latin1.GetBytes("DATA2"),
.. new byte[] { 0x00 },
.. Encoding.Unicode.GetBytes("TEST3"),
.. new byte[] { 0x00 },
@@ -210,8 +594,6 @@ namespace SabreTools.IO.Test.Extensions
];
var actual = arr.ReadStringsFrom(5);
Assert.NotNull(actual);
// ASCII and UTF-8 are identical for the character range
Assert.Equal(6, actual.Count);
}
@@ -319,6 +701,22 @@ namespace SabreTools.IO.Test.Extensions
Assert.Equal(2, actual.Count);
}
[Fact]
public void ReadStringsWithEncoding_InvalidAsciiChars_Empty()
{
byte[]? arr =
[
0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07,
0x08, 0x09, 0x0A, 0x0B, 0x0C, 0x0D, 0x0E, 0x0F,
0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17,
0x18, 0x19, 0x1A, 0x1B, 0x1C, 0x1D, 0x1E, 0x1F,
.. Enumerable.Range(0x80, 0x80).Select(i => (byte)i),
];
var actual = arr.ReadStringsWithEncoding(1, Encoding.ASCII);
Assert.NotNull(actual);
Assert.Empty(actual);
}
[Fact]
public void ReadStringsWithEncoding_Latin1_Filled()
{
@@ -337,6 +735,25 @@ namespace SabreTools.IO.Test.Extensions
Assert.Equal(2, actual.Count);
}
[Fact]
public void ReadStringsWithEncoding_InvalidLatin1Chars_Empty()
{
byte[]? arr =
[
0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07,
0x08, 0x09, 0x0A, 0x0B, 0x0C, 0x0D, 0x0E, 0x0F,
0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17,
0x18, 0x19, 0x1A, 0x1B, 0x1C, 0x1D, 0x1E, 0x1F,
0x80, 0x81, 0x82, 0x83, 0x84, 0x85, 0x86, 0x87,
0x88, 0x89, 0x8A, 0x8B, 0x8C, 0x8D, 0x8E, 0x8F,
0x90, 0x91, 0x92, 0x93, 0x94, 0x95, 0x96, 0x97,
0x98, 0x99, 0x9A, 0x9B, 0x9C, 0x9D, 0x9E, 0x9F,
];
var actual = arr.ReadStringsWithEncoding(1, Encoding.Latin1);
Assert.NotNull(actual);
Assert.Empty(actual);
}
[Fact]
public void ReadStringsWithEncoding_UTF8_Filled()
{
@@ -355,6 +772,24 @@ namespace SabreTools.IO.Test.Extensions
Assert.Equal(2, actual.Count);
}
[Fact]
public void ReadStringsWithEncoding_InvalidUTF8Chars_Empty()
{
byte[]? arr =
[
0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07,
0x08, 0x09, 0x0A, 0x0B, 0x0C, 0x0D, 0x0E, 0x0F,
0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17,
0x18, 0x19, 0x1A, 0x1B, 0x1C, 0x1D, 0x1E, 0x1F,
.. Enumerable.Range(0x80, 0x42).Select(i => (byte)i),
0xF5, 0xF6, 0xF7, 0xF8, 0xF9, 0xFA, 0xFB, 0xFC,
0xFD, 0xFE, 0xFF,
];
var actual = arr.ReadStringsWithEncoding(1, Encoding.UTF8);
Assert.NotNull(actual);
Assert.Empty(actual);
}
[Fact]
public void ReadStringsWithEncoding_UTF16_Filled()
{
@@ -373,6 +808,21 @@ namespace SabreTools.IO.Test.Extensions
Assert.Equal(2, actual.Count);
}
[Fact]
public void ReadStringsWithEncoding_InvalidUTF16Chars_Empty()
{
byte[]? arr =
[
0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07,
0x08, 0x09, 0x0A, 0x0B, 0x0C, 0x0D, 0x0E, 0x0F,
0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17,
0x18, 0x19, 0x1A, 0x1B, 0x1C, 0x1D, 0x1E, 0x1F,
];
var actual = arr.ReadStringsWithEncoding(1, Encoding.Unicode);
Assert.NotNull(actual);
Assert.Empty(actual);
}
[Fact]
public void ReadStringsWithEncoding_UTF32_Filled()
{
@@ -391,6 +841,21 @@ namespace SabreTools.IO.Test.Extensions
Assert.Equal(2, actual.Count);
}
[Fact]
public void ReadStringsWithEncoding_InvalidUTF32Chars_Empty()
{
byte[]? arr =
[
0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07,
0x08, 0x09, 0x0A, 0x0B, 0x0C, 0x0D, 0x0E, 0x0F,
0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17,
0x18, 0x19, 0x1A, 0x1B, 0x1C, 0x1D, 0x1E, 0x1F,
];
var actual = arr.ReadStringsWithEncoding(1, Encoding.UTF32);
Assert.NotNull(actual);
Assert.Empty(actual);
}
#endregion
}
}

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

@@ -200,29 +200,25 @@ namespace SabreTools.IO.Test.Extensions
Stream? stream = new MemoryStream(bytes);
var actual = stream.ReadStringsFrom(0, bytes.Length, 4);
Assert.NotNull(actual);
// ASCII and UTF-8 are identical for the character range
Assert.Equal(4, actual.Count);
Assert.Equal(2, actual.Count);
}
[Fact]
public void ReadStringsFrom_UTF8_Filled()
public void ReadStringsFrom_Latin1Strings_Filled()
{
byte[]? bytes =
[
.. Encoding.UTF8.GetBytes("TEST"),
.. Encoding.Latin1.GetBytes("TEST"),
.. new byte[] { 0x00 },
.. Encoding.UTF8.GetBytes("TWO"),
.. Encoding.Latin1.GetBytes("TWO"),
.. new byte[] { 0x00 },
.. Encoding.UTF8.GetBytes("DATA"),
.. Encoding.Latin1.GetBytes("DATA"),
.. new byte[] { 0x00 },
];
Stream? stream = new MemoryStream(bytes);
var actual = stream.ReadStringsFrom(0, bytes.Length, 4);
Assert.NotNull(actual);
// ASCII and UTF-8 are identical for the character range
Assert.Equal(4, actual.Count);
Assert.Equal(2, actual.Count);
}
[Fact]
@@ -254,11 +250,11 @@ namespace SabreTools.IO.Test.Extensions
.. new byte[] { 0x00 },
.. Encoding.ASCII.GetBytes("DATA1"),
.. new byte[] { 0x00 },
.. Encoding.UTF8.GetBytes("TEST2"),
.. Encoding.Latin1.GetBytes("TEST2"),
.. new byte[] { 0x00 },
.. Encoding.UTF8.GetBytes("TWO2"),
.. Encoding.Latin1.GetBytes("TWO2"),
.. new byte[] { 0x00 },
.. Encoding.UTF8.GetBytes("DATA2"),
.. Encoding.Latin1.GetBytes("DATA2"),
.. new byte[] { 0x00 },
.. Encoding.Unicode.GetBytes("TEST3"),
.. new byte[] { 0x00 },
@@ -270,9 +266,7 @@ namespace SabreTools.IO.Test.Extensions
Stream? stream = new MemoryStream(bytes);
var actual = stream.ReadStringsFrom(0, bytes.Length, 5);
Assert.NotNull(actual);
// ASCII and UTF-8 are identical for the character range
Assert.Equal(10, actual.Count);
Assert.Equal(6, actual.Count);
}
#endregion

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

@@ -0,0 +1,40 @@
using System.IO;
using Xunit;
namespace SabreTools.IO.Test.Streams
{
public class BufferedStreamTests
{
#region ReadNextByte
[Fact]
public void ReadNextByte_Empty_Null()
{
var source = new MemoryStream();
var stream = new IO.Streams.BufferedStream(source);
byte? actual = stream.ReadNextByte();
Assert.Null(actual);
}
[Fact]
public void ReadNextByte_Filled_ValidPosition_Byte()
{
var source = new MemoryStream(new byte[1024]);
var stream = new IO.Streams.BufferedStream(source);
byte? actual = stream.ReadNextByte();
Assert.Equal((byte)0x00, actual);
}
[Fact]
public void ReadNextByte_Filled_InvalidPosition_Null()
{
var source = new MemoryStream(new byte[1024]);
source.Seek(0, SeekOrigin.End);
var stream = new IO.Streams.BufferedStream(source);
byte? actual = stream.ReadNextByte();
Assert.Null(actual);
}
#endregion
}
}

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.IO.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.IO.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.IO.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

@@ -3,8 +3,6 @@ using System.IO;
using System.Text;
using SabreTools.Hashing;
using SabreTools.IO.Extensions;
using SabreTools.Models.PKZIP;
using static SabreTools.Models.PKZIP.Constants;
namespace SabreTools.IO.Compression.Deflate
{
@@ -20,6 +18,46 @@ namespace SabreTools.IO.Compression.Deflate
/// </summary>
private const int BufferSize = 1024 * 1024;
/// <summary>
/// Local file header signature
/// </summary>
private const uint LocalFileHeaderSignature = 0x04034B50;
#endregion
#region Private Classes
/// <summary>
/// Minimal PKZIP local file header information
/// </summary>
private class MinLocalFileHeader
{
/// <summary>
/// Signature (0x04034B50)
/// </summary>
public uint Signature { get; set; }
/// <summary>
/// CRC-32
/// </summary>
public uint CRC32 { get; set; }
/// <summary>
/// Compressed size
/// </summary>
public uint CompressedSize { get; set; }
/// <summary>
/// Uncompressed size
/// </summary>
public uint UncompressedSize { get; set; }
/// <summary>
/// File name (variable size)
/// </summary>
public string? FileName { get; set; }
}
#endregion
#region Extraction
@@ -140,7 +178,7 @@ namespace SabreTools.IO.Compression.Deflate
long current = source.Position;
// Parse the PKZIP header, if it exists
LocalFileHeader? zipHeader = ParseLocalFileHeader(source);
MinLocalFileHeader? zipHeader = ParseLocalFileHeader(source);
long zipHeaderBytes = source.Position - current;
// Always trust the PKZIP CRC-32 value over what is supplied
@@ -269,46 +307,39 @@ namespace SabreTools.IO.Compression.Deflate
}
/// <summary>
/// Parse a Stream into a local file header
/// Parse a Stream into a minimal local file header
/// </summary>
/// <param name="data">Stream to parse</param>
/// <returns>Filled local file header on success, null on error</returns>
/// <remarks>Mirror of method in Serialization</remarks>
private static LocalFileHeader? ParseLocalFileHeader(Stream data)
/// <returns>Filled minimal local file header on success, null on error</returns>
/// <remarks>Partial mirror of method in Serialization</remarks>
private static MinLocalFileHeader? ParseLocalFileHeader(Stream data)
{
var header = new LocalFileHeader();
var header = new MinLocalFileHeader();
header.Signature = data.ReadUInt32LittleEndian();
if (header.Signature != LocalFileHeaderSignature)
return null;
header.Version = data.ReadUInt16LittleEndian();
header.Flags = (GeneralPurposeBitFlags)data.ReadUInt16LittleEndian();
header.CompressionMethod = (CompressionMethod)data.ReadUInt16LittleEndian();
header.LastModifedFileTime = data.ReadUInt16LittleEndian();
header.LastModifiedFileDate = data.ReadUInt16LittleEndian();
_ = data.ReadUInt16LittleEndian(); // Version
_ = data.ReadUInt16LittleEndian(); // Flags
_ = data.ReadUInt16LittleEndian(); // CompressionMethod
_ = data.ReadUInt16LittleEndian(); // LastModifedFileTime
_ = data.ReadUInt16LittleEndian(); // LastModifiedFileDate
header.CRC32 = data.ReadUInt32LittleEndian();
header.CompressedSize = data.ReadUInt32LittleEndian();
header.UncompressedSize = data.ReadUInt32LittleEndian();
header.FileNameLength = data.ReadUInt16LittleEndian();
header.ExtraFieldLength = data.ReadUInt16LittleEndian();
ushort fileNameLength = data.ReadUInt16LittleEndian();
ushort extraFieldLength = data.ReadUInt16LittleEndian();
if (header.FileNameLength > 0 && data.Position + header.FileNameLength <= data.Length)
if (fileNameLength > 0 && data.Position + fileNameLength <= data.Length)
{
byte[] filenameBytes = data.ReadBytes(header.FileNameLength);
if (filenameBytes.Length != header.FileNameLength)
return null;
byte[] filenameBytes = data.ReadBytes(fileNameLength);
header.FileName = Encoding.ASCII.GetString(filenameBytes);
}
// Parsing extras is skipped here, unlike in Serialization
if (header.ExtraFieldLength > 0 && data.Position + header.ExtraFieldLength <= data.Length)
{
byte[] extraBytes = data.ReadBytes(header.ExtraFieldLength);
if (extraBytes.Length != header.ExtraFieldLength)
return null;
}
if (extraFieldLength > 0 && data.Position + extraFieldLength <= data.Length)
_ = data.ReadBytes(extraFieldLength);
return header;
}

View File

@@ -0,0 +1,58 @@
namespace SabreTools.IO.Compression.LZX
{
/// <summary>
/// An aligned offset block is identical to the verbatim block except for the presence of the aligned offset
/// tree preceding the other trees.
/// </summary>
/// <see href="https://interoperability.blob.core.windows.net/files/MS-PATCH/%5bMS-PATCH%5d.pdf"/>
internal class AlignedOffsetBlockData : BlockData
{
/// <summary>
/// Aligned offset tree
/// </summary>
/// <remarks>8 elements, 3 bits each</remarks>
public byte[]? AlignedOffsetTree { get; set; }
/// <summary>
/// Pretree for first 256 elements of main tree
/// </summary>
/// <remarks>20 elements, 4 bits each</remarks>
public byte[]? PretreeFirst256 { get; set; }
/// <summary>
/// Path lengths of first 256 elements of main tree
/// </summary>
/// <remarks>Encoded using pretree</remarks>
public int[]? PathLengthsFirst256 { get; set; }
/// <summary>
/// Pretree for remainder of main tree
/// </summary>
/// <remarks>20 elements, 4 bits each</remarks>
public byte[]? PretreeRemainder { get; set; }
/// <summary>
/// Path lengths of remaining elements of main tree
/// </summary>
/// <remarks>Encoded using pretree</remarks>
public int[]? PathLengthsRemainder { get; set; }
/// <summary>
/// Pretree for length tree
/// </summary>
/// <remarks>20 elements, 4 bits each</remarks>
public byte[]? PretreeLengthTree { get; set; }
/// <summary>
/// Path lengths of elements in length tree
/// </summary>
/// <remarks>Encoded using pretree</remarks>
public int[]? PathLengthsLengthTree { get; set; }
/// <summary>
/// Token sequence (matches and literals)
/// </summary>
/// <remarks>Variable</remarks>
public byte[]? TokenSequence { get; set; }
}
}

View File

@@ -0,0 +1,24 @@
namespace SabreTools.IO.Compression.LZX
{
/// <summary>
/// An LZXD block represents a sequence of compressed data that is encoded with the same set of
/// Huffman trees, or a sequence of uncompressed data. There can be one or more LZXD blocks in a
/// compressed stream, each with its own set of Huffman trees. Blocks do not have to start or end on a
/// chunk boundary; blocks can span multiple chunks, or a single chunk can contain multiple blocks. The
/// number of chunks is related to the size of the data being compressed, while the number of blocks is
/// related to how well the data is compressed.
/// </summary>
/// <see href="https://interoperability.blob.core.windows.net/files/MS-PATCH/%5bMS-PATCH%5d.pdf"/>
internal class Block
{
/// <summary>
/// Block header
/// </summary>
public BlockHeader? Header { get; set; }
/// <summary>
/// Block data
/// </summary>
public BlockData? BlockData { get; set; }
}
}

View File

@@ -0,0 +1,8 @@
namespace SabreTools.IO.Compression.LZX
{
/// <see href="https://interoperability.blob.core.windows.net/files/MS-PATCH/%5bMS-PATCH%5d.pdf"/>
internal abstract class BlockData
{
// No common fields between all block data
}
}

View File

@@ -0,0 +1,33 @@
namespace SabreTools.IO.Compression.LZX
{
/// <summary>
/// The Block Type field, as specified in section 2.3.1.1, indicates which type of block follows,
/// and the Block Size field, as specified in section 2.3.1.2, indicates the number of
/// uncompressed bytes represented by the block. Following the generic block
/// header is a type-specific header that describes the remainder of the block.
/// </summary>
/// <see href="https://interoperability.blob.core.windows.net/files/MS-PATCH/%5bMS-PATCH%5d.pdf"/>
internal class BlockHeader
{
/// <remarks>3 bits</remarks>
public BlockType BlockType { get; set; }
/// <summary>
/// Block size is the high 8 bits of 24
/// </summary>
/// <remarks>8 bits</remarks>
public byte BlockSizeMSB { get; set; }
/// <summary>
/// Block size is the middle 8 bits of 24
/// </summary>
/// <remarks>8 bits</remarks>
public byte BlockSizeByte2 { get; set; }
/// <summary>
/// Block size is the low 8 bits of 24
/// </summary>
/// <remarks>8 bits</remarks>
public byte BlocksizeLSB { get; set; }
}
}

View File

@@ -0,0 +1,25 @@
namespace SabreTools.IO.Compression.LZX
{
/// <summary>
/// The LZXD compressor emits chunks of compressed data. A chunk represents exactly 32 KB of
/// uncompressed data until the last chunk in the stream, which can represent less than 32 KB. To
/// ensure that an exact number of input bytes represent an exact number of output bytes for each
/// chunk, after each 32 KB of uncompressed data is represented in the output compressed bitstream, the
/// output bitstream is padded with up to 15 bits of zeros to realign the bitstream on a 16-bit boundary
/// (even byte boundary) for the next 32 KB of data. This results in a compressed chunk of a byte-aligned
/// size. The compressed chunk could be smaller than 32 KB or larger than 32 KB if the data is
/// incompressible when the chunk is not the last one.
/// </summary>
internal class Chunk
{
/// <summary>
/// Chunk header
/// </summary>
public ChunkHeader? Header { get; set; }
/// <summary>
/// Block headers and data
/// </summary>
public Block[]? Blocks { get; set; }
}
}

View File

@@ -0,0 +1,46 @@
namespace SabreTools.IO.Compression.LZX
{
/// <summary>
/// The LZXD compressor emits chunks of compressed data. A chunk represents exactly 32 KB of
/// uncompressed data until the last chunk in the stream, which can represent less than 32 KB. To
/// ensure that an exact number of input bytes represent an exact number of output bytes for each
/// chunk, after each 32 KB of uncompressed data is represented in the output compressed bitstream, the
/// output bitstream is padded with up to 15 bits of zeros to realign the bitstream on a 16-bit boundary
/// (even byte boundary) for the next 32 KB of data. This results in a compressed chunk of a byte-aligned
/// size. The compressed chunk could be smaller than 32 KB or larger than 32 KB if the data is
/// incompressible when the chunk is not the last one.
/// </summary>
internal class ChunkHeader
{
/// <summary>
/// The LZXD engine encodes a compressed, chunk-size prefix field preceding each compressed chunk in
/// the compressed byte stream. The compressed, chunk-size prefix field is a byte aligned, little-endian,
/// 16-bit field. The chunk prefix chain could be followed in the compressed stream without
/// decompressing any data. The next chunk prefix is at a location computed by the absolute byte offset
/// location of this chunk prefix plus 2 (for the size of the chunk-size prefix field) plus the current chunk
/// size.
/// </summary>
public ushort ChunkSize { get; set; }
/// <summary>
/// The first bit in the first chunk in the LZXD bitstream (following the 2-byte, chunk-size prefix described
/// in section 2.2.1) indicates the presence or absence of two 16-bit fields immediately following the
/// single bit. If the bit is set, E8 translation is enabled for all the following chunks in the stream using the
/// 32-bit value derived from the two 16-bit fields as the E8_file_size provided to the compressor when E8
/// translation was enabled. Note that E8_file_size is completely independent of the length of the
/// uncompressed data. E8 call translation is disabled after the 32,768th chunk (after 1 gigabyte (GB) of
/// uncompressed data).
/// </summary>
public byte E8Translation { get; set; }
/// <summary>
/// E8 translation size, high WORD
/// </summary>
public ushort? TranslationSizeHighWord { get; set; }
/// <summary>
/// E8 translation size, low WORD
/// </summary>
public ushort? TranslationSizeLowWord { get; set; }
}
}

View File

@@ -0,0 +1,38 @@
namespace SabreTools.IO.Compression.LZX
{
internal static class Constants
{
/* some constants defined by the LZX specification */
public const int LZX_MIN_MATCH = 2;
public const int LZX_MAX_MATCH = 257;
public const int LZX_NUM_CHARS = 256;
public const int LZX_PRETREE_NUM_ELEMENTS = 20;
/// <summary>
/// aligned offset tree #elements
/// </summary>
public const int LZX_ALIGNED_NUM_ELEMENTS = 8;
/// <summary>
/// this one missing from spec!
/// </summary>
public const int LZX_NUM_PRIMARY_LENGTHS = 7;
/// <summary>
/// length tree #elements
/// </summary>
public const int LZX_NUM_SECONDARY_LENGTHS = 249;
/* LZX huffman defines: tweak tablebits as desired */
public const int LZX_PRETREE_MAXSYMBOLS = LZX_PRETREE_NUM_ELEMENTS;
public const int LZX_PRETREE_TABLEBITS = 6;
public const int LZX_MAINTREE_MAXSYMBOLS = LZX_NUM_CHARS + 50 * 8;
public const int LZX_MAINTREE_TABLEBITS = 12;
public const int LZX_LENGTH_MAXSYMBOLS = LZX_NUM_SECONDARY_LENGTHS + 1;
public const int LZX_LENGTH_TABLEBITS = 12;
public const int LZX_ALIGNED_MAXSYMBOLS = LZX_ALIGNED_NUM_ELEMENTS;
public const int LZX_ALIGNED_TABLEBITS = 7;
public const int LZX_LENTABLE_SAFETY = 64; /* we allow length table decoding overruns */
}
}

View File

@@ -0,0 +1,48 @@
namespace SabreTools.IO.Compression.LZX
{
/// <summary>
/// 3-bit block type
/// </summary>
internal enum BlockType : byte
{
/// <summary>
/// Not valid
/// </summary>
INVALID_0 = 0b000,
/// <summary>
/// Verbatim block
/// </summary>
Verbatim = 0b001,
/// <summary>
/// Aligned offset block
/// </summary>
AlignedOffset = 0b010,
/// <summary>
/// Uncompressed block
/// </summary>
Uncompressed = 0b011,
/// <summary>
/// Not valid
/// </summary>
INVALID_4 = 0b100,
/// <summary>
/// Not valid
/// </summary>
INVALID_5 = 0b101,
/// <summary>
/// Not valid
/// </summary>
INVALID_6 = 0b110,
/// <summary>
/// Not valid
/// </summary>
INVALID_7 = 0b111,
}
}

View File

@@ -0,0 +1,54 @@
namespace SabreTools.IO.Compression.LZX
{
/// <summary>
/// Following the generic block header, an uncompressed block begins with 1 to 16 bits of zero padding
/// to align the bit buffer on a 16-bit boundary. At this point, the bitstream ends and a byte stream
/// begins. Following the zero padding, new 32-bit values for R0, R1, and R2 are output in little-endian
/// form, followed by the uncompressed data bytes themselves. Finally, if the uncompressed data length
/// is odd, one extra byte of zero padding is encoded to realign the following bitstream.
///
/// Then the bitstream of byte-swapped 16-bit integers resumes for the next Block Type field (if there
/// are subsequent blocks).
///
/// The decoded R0, R1, and R2 values are used as initial repeated offset values to decode the
/// subsequent compressed block if present.
/// </summary>
/// <see href="https://interoperability.blob.core.windows.net/files/MS-PATCH/%5bMS-PATCH%5d.pdf"/>
internal class UncompressedBlockData : BlockData
{
/// <summary>
/// Padding to align following field on 16-bit boundary
/// </summary>
/// <remarks>Bits have a value of zero</remarks>
public ushort PaddingBits { get; set; }
/// <summary>
/// Least significant to most significant byte (little-endian DWORD ([MS-DTYP]))
/// </summary>
/// <remarks>Encoded directly in the byte stream, not in the bitstream of byte-swapped 16-bit words</remarks>
public uint R0 { get; set; }
/// <summary>
/// Least significant to most significant byte (little-endian DWORD)
/// </summary>
/// <remarks>Encoded directly in the byte stream, not in the bitstream of byte-swapped 16-bit words</remarks>
public uint R1 { get; set; }
/// <summary>
/// Least significant to most significant byte (little-endian DWORD)
/// </summary>
/// <remarks>Encoded directly in the byte stream, not in the bitstream of byte-swapped 16-bit words</remarks>
public uint R2 { get; set; }
/// <summary>
/// Can use the direct memcpy function, as specified in [IEEE1003.1]
/// </summary>
/// <remarks>Encoded directly in the byte stream, not in the bitstream of byte-swapped 16-bit words</remarks>
public byte[]? RawDataBytes { get; set; }
/// <summary>
/// Only if uncompressed size is odd
/// </summary>
public byte AlignmentByte { get; set; }
}
}

View File

@@ -0,0 +1,51 @@
namespace SabreTools.IO.Compression.LZX
{
/// <summary>
/// The fields of a verbatim block that follow the generic block header
/// </summary>
/// <see href="https://interoperability.blob.core.windows.net/files/MS-PATCH/%5bMS-PATCH%5d.pdf"/>
internal class VerbatimBlockData : BlockData
{
/// <summary>
/// Pretree for first 256 elements of main tree
/// </summary>
/// <remarks>20 elements, 4 bits each</remarks>
public byte[]? PretreeFirst256 { get; set; }
/// <summary>
/// Path lengths of first 256 elements of main tree
/// </summary>
/// <remarks>Encoded using pretree</remarks>
public int[]? PathLengthsFirst256 { get; set; }
/// <summary>
/// Pretree for remainder of main tree
/// </summary>
/// <remarks>20 elements, 4 bits each</remarks>
public byte[]? PretreeRemainder { get; set; }
/// <summary>
/// Path lengths of remaining elements of main tree
/// </summary>
/// <remarks>Encoded using pretree</remarks>
public int[]? PathLengthsRemainder { get; set; }
/// <summary>
/// Pretree for length tree
/// </summary>
/// <remarks>20 elements, 4 bits each</remarks>
public byte[]? PretreeLengthTree { get; set; }
/// <summary>
/// Path lengths of elements in length tree
/// </summary>
/// <remarks>Encoded using pretree</remarks>
public int[]? PathLengthsLengthTree { get; set; }
/// <summary>
/// Token sequence (matches and literals)
/// </summary>
/// <remarks>Variable</remarks>
public byte[]? TokenSequence { get; set; }
}
}

View File

@@ -0,0 +1,28 @@
namespace SabreTools.IO.Compression.MSZIP
{
/// <summary>
/// Each MSZIP block MUST consist of a 2-byte MSZIP signature and one or more RFC 1951 blocks. The
/// 2-byte MSZIP signature MUST consist of the bytes 0x43 and 0x4B. The MSZIP signature MUST be
/// the first 2 bytes in the MSZIP block. The MSZIP signature is shown in the following packet diagram.
///
/// Each MSZIP block is the result of a single deflate compression operation, as defined in [RFC1951].
/// The compressor that performs the compression operation MUST generate one or more RFC 1951
/// blocks, as defined in [RFC1951]. The number, deflation mode, and type of RFC 1951 blocks in each
/// MSZIP block is determined by the compressor, as defined in [RFC1951]. The last RFC 1951 block in
/// each MSZIP block MUST be marked as the "end" of the stream(1), as defined by [RFC1951]
/// section 3.2.3. Decoding trees MUST be discarded after each RFC 1951 block, but the history buffer
/// MUST be maintained.Each MSZIP block MUST represent no more than 32 KB of uncompressed data.
///
/// The maximum compressed size of each MSZIP block is 32 KB + 12 bytes. This enables the MSZIP
/// block to contain 32 KB of data split between two noncompressed RFC 1951 blocks, each of which
/// has a value of BTYPE = 00.
/// </summary>
/// <see href="https://interoperability.blob.core.windows.net/files/MS-MCI/%5bMS-MCI%5d.pdf"/>
internal class BlockHeader
{
/// <summary>
/// 'CK'
/// </summary>
public ushort Signature { get; set; }
}
}

View File

@@ -1,7 +1,6 @@
using System;
using System.IO;
using SabreTools.IO.Extensions;
using SabreTools.Models.Compression.MSZIP;
namespace SabreTools.IO.Compression.MSZIP
{

View File

@@ -0,0 +1,50 @@
namespace SabreTools.IO.Compression.Quantum
{
/// <see href="http://www.russotto.net/quantumcomp.html"/>
internal static class Constants
{
public static readonly int[] PositionSlot =
[
0x00000, 0x00001, 0x00002, 0x00003, 0x00004, 0x00006, 0x00008, 0x0000c,
0x00010, 0x00018, 0x00020, 0x00030, 0x00040, 0x00060, 0x00080, 0x000c0,
0x00100, 0x00180, 0x00200, 0x00300, 0x00400, 0x00600, 0x00800, 0x00c00,
0x01000, 0x01800, 0x02000, 0x03000, 0x04000, 0x06000, 0x08000, 0x0c000,
0x10000, 0x18000, 0x20000, 0x30000, 0x40000, 0x60000, 0x80000, 0xc0000,
0x100000, 0x180000
];
public static readonly int[] PositionExtraBits =
[
0, 0, 0, 0, 1, 1, 2, 2,
3, 3, 4, 4, 5, 5, 6, 6,
7, 7, 8, 8, 9, 9, 10, 10,
11, 11, 12, 12, 13, 13, 14, 14,
15, 15, 16, 16, 17, 17, 18, 18,
19, 19
];
public static readonly int[] LengthSlot =
[
0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x08,
0x0a, 0x0c, 0x0e, 0x12, 0x16, 0x1a, 0x1e, 0x26,
0x2e, 0x36, 0x3e, 0x4e, 0x5e, 0x6e, 0x7e, 0x9e,
0xbe, 0xde, 0xfe
];
public static readonly int[] LengthExtraBits =
[
0, 0, 0, 0, 0, 0, 1, 1,
1, 1, 2, 2, 2, 2, 3, 3,
3, 3, 4, 4, 4, 4, 5, 5,
5, 5, 0
];
/// <summary>
/// Number of position slots for (tsize - 10)
/// </summary>
public static readonly int[] NumPositionSlots =
[
20, 22, 24, 26, 28, 30, 32, 34, 36, 38, 40, 42
];
}
}

View File

@@ -2,8 +2,7 @@ using System;
using System.Collections.Generic;
using System.IO;
using SabreTools.IO.Streams;
using SabreTools.Models.Compression.Quantum;
using static SabreTools.Models.Compression.Quantum.Constants;
using static SabreTools.IO.Compression.Quantum.Constants;
namespace SabreTools.IO.Compression.Quantum
{

View File

@@ -0,0 +1,45 @@
namespace SabreTools.IO.Compression.Quantum
{
internal enum SelectorModel
{
/// <summary>
/// Literal model, 64 entries, start at symbol 0
/// </summary>
SELECTOR_0 = 0,
/// <summary>
/// Literal model, 64 entries, start at symbol 64
/// </summary>
SELECTOR_1 = 1,
/// <summary>
/// Literal model, 64 entries, start at symbol 128
/// </summary>
SELECTOR_2 = 2,
/// <summary>
/// Literal model, 64 entries, start at symbol 192
/// </summary>
SELECTOR_3 = 3,
/// <summary>
/// LZ model, 3 character matches, max 24 entries, start at symbol 0
/// </summary>
SELECTOR_4 = 4,
/// <summary>
/// LZ model, 4 character matches, max 36 entries, start at symbol 0
/// </summary>
SELECTOR_5 = 5,
/// <summary>
/// LZ model, 5+ character matches, max 42 entries, start at symbol 0
/// </summary>
SELECTOR_6_POSITION = 6,
/// <summary>
/// LZ model, 5+ character matches, 27 entries, start at symbol 0
/// </summary>
SELECTOR_6_LENGTH = 7,
}
}

View File

@@ -0,0 +1,24 @@
namespace SabreTools.IO.Compression.Quantum
{
/// <see href="http://www.russotto.net/quantumcomp.html"/>
internal sealed class Model
{
public int Entries { get; set; }
/// <remarks>
/// All the models are initialized with the symbols in symbol
/// order in the table, and with every symbol in the table
/// having a frequency of 1
/// </remarks>
public ModelSymbol[]? Symbols { get; set; }
/// <remarks>
/// The initial total frequency is equal to the number of entries
/// in the table
/// </remarks>
public int TotalFrequency { get; set; }
/// <remarks>The initial time_to_reorder value is 4</remarks>
public int TimeToReorder { get; set; }
}
}

View File

@@ -0,0 +1,15 @@
namespace SabreTools.IO.Compression.Quantum
{
/// <see href="http://www.russotto.net/quantumcomp.html"/>
internal sealed class ModelSymbol
{
public ushort Symbol { get; set; }
/// <summary>
/// The cumulative frequency is the frequency of all the symbols
/// which are at a higher index in the table than that symbol —
/// thus the last entry in the table has a cumulative frequency of 0.
/// </summary>
public ushort CumulativeFrequency { get; set; }
}
}

View File

@@ -1,6 +1,5 @@
using System;
using System.IO;
using SabreTools.Models.LZ;
namespace SabreTools.IO.Compression.SZDD
{
@@ -15,7 +14,7 @@ namespace SabreTools.IO.Compression.SZDD
/// <summary>
/// Source stream for the decompressor
/// </summary>
private readonly BufferedStream _source;
private readonly Streams.BufferedStream _source;
/// <summary>
/// SZDD format being decompressed
@@ -37,19 +36,19 @@ namespace SabreTools.IO.Compression.SZDD
// Initialize the window with space characters
_window = Array.ConvertAll(_window, b => (byte)0x20);
_source = new BufferedStream(source);
_source = new Streams.BufferedStream(source);
}
/// <summary>
/// Create a KWAJ decompressor
/// </summary>
public static Decompressor CreateKWAJ(byte[] source, KWAJCompressionType compressionType)
public static Decompressor CreateKWAJ(byte[] source, ushort compressionType)
=> CreateKWAJ(new MemoryStream(source), compressionType);
/// <summary>
/// Create a KWAJ decompressor
/// </summary>
public static Decompressor CreateKWAJ(Stream source, KWAJCompressionType compressionType)
public static Decompressor CreateKWAJ(Stream source, ushort compressionType)
{
// Create the decompressor
var decompressor = new Decompressor(source);
@@ -57,11 +56,11 @@ namespace SabreTools.IO.Compression.SZDD
// Set the format and return
decompressor._format = compressionType switch
{
KWAJCompressionType.NoCompression => Format.KWAJNoCompression,
KWAJCompressionType.NoCompressionXor => Format.KWAJXor,
KWAJCompressionType.QBasic => Format.KWAJQBasic,
KWAJCompressionType.LZH => Format.KWAJLZH,
KWAJCompressionType.MSZIP => Format.KWAJMSZIP,
0x0000 => Format.KWAJNoCompression,
0x0001 => Format.KWAJXor,
0x0002 => Format.KWAJQBasic,
0x0003 => Format.KWAJLZH,
0x0004 => Format.KWAJMSZIP,
_ => throw new IndexOutOfRangeException(nameof(source)),
};
return decompressor;
@@ -229,77 +228,5 @@ namespace SabreTools.IO.Compression.SZDD
dest.Flush();
return true;
}
/// <summary>
/// Buffered stream that reads in blocks
/// </summary>
private class BufferedStream
{
/// <summary>
/// Source stream for populating the buffer
/// </summary>
private readonly Stream _source;
/// <summary>
/// Internal buffer to read
/// </summary>
private readonly byte[] _buffer = new byte[2048];
/// <summary>
/// Current pointer into the buffer
/// </summary>
private int _bufferPtr = 0;
/// <summary>
/// Represents the number of available bytes
/// </summary>
private int _available = -1;
/// <summary>
/// Create a new buffered stream
/// </summary>
public BufferedStream(Stream source)
{
_source = source;
}
/// <summary>
/// Read the next byte from the buffer, if possible
/// </summary>
public byte? ReadNextByte()
{
// Ensure the buffer first
if (!EnsureBuffer())
return null;
// Return the next available value
return _buffer[_bufferPtr++];
}
/// <summary>
/// Ensure the buffer has data to read
/// </summary>
private bool EnsureBuffer()
{
// Force an update if in the initial state
if (_available == -1)
{
_available = _source.Read(_buffer, 0, _buffer.Length);
_bufferPtr = 0;
return _available != 0;
}
// If the pointer is out of range
if (_bufferPtr >= _available)
{
_available = _source.Read(_buffer, 0, _buffer.Length);
_bufferPtr = 0;
return _available != 0;
}
// Otherwise, assume data is available
return true;
}
}
}
}

View File

@@ -1,8 +1,7 @@
using System;
using System.IO;
using SabreTools.Hashing;
using SabreTools.Matching;
using static SabreTools.Models.MoPaQ.Constants;
using SabreTools.IO.Extensions;
namespace SabreTools.IO.Encryption
{
@@ -11,6 +10,14 @@ namespace SabreTools.IO.Encryption
/// </summary>
public class MoPaQDecrypter
{
#region Constants
private const uint MPQ_HASH_KEY2_MIX = 0x400;
private const uint STORM_BUFFER_SIZE = 0x500;
#endregion
#region Private Instance Variables
/// <summary>

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,229 @@ 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 Strings
/// <summary>
/// Convert a byte array to a hex string
/// </summary>
@@ -59,14 +283,24 @@ namespace SabreTools.IO.Extensions
/// </summary>
/// <param name="charLimit">Number of characters needed to be a valid string, default 5</param>
/// <returns>String list containing the requested data, null on error</returns>
#if NET5_0_OR_GREATER
/// <remarks>This reads both Latin1 and UTF-16 strings from the input data</remarks>
#else
/// <remarks>This reads both ASCII and UTF-16 strings from the input data</remarks>
#endif
public static List<string>? ReadStringsFrom(this byte[]? input, int charLimit = 5)
{
// Validate the data
if (input == null || input.Length == 0)
return null;
#if NET5_0_OR_GREATER
// Check for Latin1 strings
var asciiStrings = input.ReadStringsWithEncoding(charLimit, Encoding.Latin1);
#else
// Check for ASCII strings
var asciiStrings = input.ReadStringsWithEncoding(charLimit, Encoding.ASCII);
#endif
// Check for Unicode strings
// We are limiting the check for Unicode characters with a second byte of 0x00 for now
@@ -101,13 +335,17 @@ namespace SabreTools.IO.Extensions
// Short-circuit for some encoding types
if (encoding.CodePage == Encoding.ASCII.CodePage)
return bytes.ReadFixedWidthEncodingStrings(charLimit, Encoding.ASCII, 1);
return bytes.ReadAsciiStrings(charLimit);
#if NET5_0_OR_GREATER
else if (encoding.CodePage == Encoding.Latin1.CodePage)
return bytes.ReadFixedWidthEncodingStrings(charLimit, Encoding.Latin1, 1);
#endif
else if (encoding.IsSingleByte)
return bytes.ReadFixedWidthEncodingStrings(charLimit, encoding, 1);
else if (encoding.CodePage == Encoding.Unicode.CodePage)
return bytes.ReadFixedWidthEncodingStrings(charLimit, Encoding.Unicode, 2);
else if (encoding.CodePage == Encoding.BigEndianUnicode.CodePage)
return bytes.ReadFixedWidthEncodingStrings(charLimit, Encoding.BigEndianUnicode, 2);
else if (encoding.CodePage == Encoding.UTF32.CodePage)
return bytes.ReadFixedWidthEncodingStrings(charLimit, Encoding.UTF32, 4);
@@ -133,7 +371,7 @@ namespace SabreTools.IO.Extensions
char c = (char)reader.Read();
// If the character is invalid
if (char.IsControl(c) || (c & 0xFF00) != 0)
if (char.IsControl(c) || (c & 0xFFFFFF00) != 0)
{
// Seek to the end of the last found string
string str = sb.ToString();
@@ -141,6 +379,10 @@ namespace SabreTools.IO.Extensions
ms.Seek(lastOffset, SeekOrigin.Begin);
reader.DiscardBufferedData();
// If there is no cached string
if (str.Length == 0)
continue;
// Add the string if long enough
if (str.Length >= charLimit)
strings.Add(str);
@@ -165,8 +407,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>
@@ -209,10 +449,77 @@ namespace SabreTools.IO.Extensions
// Pretend only one byte was read
offset -= width - 1;
// If there is no cached string
if (sb.Length == 0)
continue;
// Add the string if long enough
string str = sb.ToString();
if (str.Length >= charLimit)
strings.Add(str);
if (sb.Length >= charLimit)
strings.Add(sb.ToString());
// Clear the builder and continue
#if NET20 || NET35
sb = new();
#else
sb.Clear();
#endif
continue;
}
// Otherwise, add the character to the builder and continue
sb.Append(c);
}
// Handle any remaining data
if (sb.Length >= charLimit)
strings.Add(sb.ToString());
return strings;
}
/// <summary>
/// Read string data from a byte array using ASCII encoding
/// </summary>
/// <param name="bytes">Byte array representing the source data</param>
/// <param name="charLimit">Number of characters needed to be a valid string</param>
/// <returns>String list containing the requested data, empty on error</returns>
/// <remarks>Handling for 7-bit ASCII needs to be done differently than other fixed-width encodings</remarks>
#if NET20
private static List<string> ReadAsciiStrings(this byte[] bytes, int charLimit)
#else
private static HashSet<string> ReadAsciiStrings(this byte[] bytes, int charLimit)
#endif
{
if (charLimit <= 0 || charLimit > bytes.Length)
return [];
// Create the string set to return
#if NET20
var strings = new List<string>();
#else
var strings = new HashSet<string>();
#endif
// Create a string builder for the loop
var sb = new StringBuilder();
// Check for strings
int offset = 0;
while (offset < bytes.Length)
{
// Read the next character from the stream
char c = bytes.ReadChar(ref offset);
// If the character is invalid
if (char.IsControl(c) || c > 0x7F)
{
// If there is no cached string
if (sb.Length == 0)
continue;
// Add the string if long enough
if (sb.Length >= charLimit)
strings.Add(sb.ToString());
// Clear the builder and continue
#if NET20 || NET35

View File

@@ -1065,6 +1065,10 @@ namespace SabreTools.IO.Extensions
/// </summary>
private static byte[] ReadExactlyToBuffer(byte[] content, ref int offset, int length)
{
// If we have an invalid offset
if (offset < 0 || offset >= content.Length)
throw new ArgumentOutOfRangeException($"{nameof(offset)} must be between 0 and {content.Length}, {offset} provided");
// If we have an invalid length
if (length < 0)
throw new ArgumentOutOfRangeException($"{nameof(length)} must be 0 or a positive value, {length} requested");

View File

@@ -1,6 +1,5 @@
using System.Collections.Generic;
using System.IO;
using System.Text;
namespace SabreTools.IO.Extensions
{
@@ -76,10 +75,15 @@ namespace SabreTools.IO.Extensions
/// <summary>
/// Read string data from a Stream
/// </summary>
/// <param name="charLimit">Number of characters needed to be a valid string, default 5</param>
/// <param name="position">Position in the source to read from</param>
/// <param name="length">Length of the requested data</param>
/// <param name="charLimit">Number of characters needed to be a valid string, default 5</param>
/// <returns>String list containing the requested data, null on error</returns>
#if NET5_0_OR_GREATER
/// <remarks>This reads both Latin1 and UTF-16 strings from the input data</remarks>
#else
/// <remarks>This reads both ASCII and UTF-16 strings from the input data</remarks>
#endif
public static List<string>? ReadStringsFrom(this Stream? input, int position, int length, int charLimit = 5)
{
// Read the data as a byte array first
@@ -87,23 +91,7 @@ namespace SabreTools.IO.Extensions
if (data == null)
return null;
// Check for ASCII strings
var asciiStrings = data.ReadStringsWithEncoding(charLimit, Encoding.ASCII);
// Check for UTF-8 strings
// We are limiting the check for Unicode characters with a second byte of 0x00 for now
var utf8Strings = data.ReadStringsWithEncoding(charLimit, Encoding.UTF8);
// Check for Unicode strings
// We are limiting the check for Unicode characters with a second byte of 0x00 for now
var unicodeStrings = data.ReadStringsWithEncoding(charLimit, Encoding.Unicode);
// Ignore duplicate strings across encodings
List<string> sourceStrings = [.. asciiStrings, .. utf8Strings, .. unicodeStrings];
// Sort the strings and return
sourceStrings.Sort();
return sourceStrings;
return data.ReadStringsFrom(charLimit);
}
/// <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

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

View File

@@ -11,7 +11,7 @@
<SuppressTfmSupportBuildWarnings>true</SuppressTfmSupportBuildWarnings>
<SymbolPackageFormat>snupkg</SymbolPackageFormat>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
<Version>1.7.3</Version>
<Version>1.7.5</Version>
<!-- Package Properties -->
<Authors>Matt Nadareski</Authors>
@@ -30,8 +30,10 @@
</ItemGroup>
<ItemGroup>
<PackageReference Include="SabreTools.Matching" Version="1.6.0" />
<PackageReference Include="SabreTools.Models" Version="1.7.1" />
<InternalsVisibleTo Include="SabreTools.IO.Test" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="SabreTools.Hashing" Version="1.5.0" />
</ItemGroup>

View File

@@ -0,0 +1,77 @@
using System.IO;
namespace SabreTools.IO.Streams
{
/// <summary>
/// Buffered stream that reads in blocks
/// </summary>
/// <remarks>Not a true <see cref="Stream"/> implementation yet</remarks>
public class BufferedStream
{
/// <summary>
/// Source stream for populating the buffer
/// </summary>
private readonly Stream _source;
/// <summary>
/// Internal buffer to read
/// </summary>
private readonly byte[] _buffer = new byte[2048];
/// <summary>
/// Current pointer into the buffer
/// </summary>
private int _bufferPtr = 0;
/// <summary>
/// Represents the number of available bytes
/// </summary>
private int _available = -1;
/// <summary>
/// Create a new buffered stream
/// </summary>
public BufferedStream(Stream source)
{
_source = source;
}
/// <summary>
/// Read the next byte from the buffer, if possible
/// </summary>
public byte? ReadNextByte()
{
// Ensure the buffer first
if (!EnsureBuffer())
return null;
// Return the next available value
return _buffer[_bufferPtr++];
}
/// <summary>
/// Ensure the buffer has data to read
/// </summary>
private bool EnsureBuffer()
{
// Force an update if in the initial state
if (_available == -1)
{
_available = _source.Read(_buffer, 0, _buffer.Length);
_bufferPtr = 0;
return _available != 0;
}
// If the pointer is out of range
if (_bufferPtr >= _available)
{
_available = _source.Read(_buffer, 0, _buffer.Length);
_bufferPtr = 0;
return _available != 0;
}
// Otherwise, assume data is available
return true;
}
}
}