69 Commits
1.3.2 ... main

Author SHA1 Message Date
Matt Nadareski
77280e8da1 Update notice 2025-09-24 08:12:19 -04:00
Matt Nadareski
3d05135a81 Add notice 2025-09-23 11:00:46 -04:00
Matt Nadareski
b2dfffbc92 There 2025-09-10 21:52:47 -04:00
Matt Nadareski
036ee4246b Bump version 2025-07-23 10:13:30 -04:00
Matt Nadareski
8e3bcb9015 Update nuget packages 2025-07-23 10:11:08 -04:00
Matt Nadareski
8b1ac53ccf Remove now-unnecessary prefixes 2025-07-23 10:08:53 -04:00
Matt Nadareski
0c9e255d48 Split tests to match classes 2025-07-23 10:08:30 -04:00
Matt Nadareski
1227488572 Be consistent about end-of-file newlines 2025-07-23 10:07:10 -04:00
Matt Nadareski
bd878cf1a1 Fix nullability issue in MatchUtil 2025-07-23 10:04:43 -04:00
Matt Nadareski
b436b64c3a Be consistent about end-of-file newlines 2025-07-23 10:00:50 -04:00
Matt Nadareski
6f6d071a79 Add summaries to IMatch 2025-07-23 09:58:46 -04:00
Matt Nadareski
69130a6e9f Add .NET Standard 2.0 and 2.1 2025-07-23 09:52:35 -04:00
Matt Nadareski
93847420a5 Bump version 2025-05-12 08:11:12 -04:00
Matt Nadareski
76e13a47ec Fix length-based comparison issue 2025-05-12 08:09:59 -04:00
Matt Nadareski
4f56d716d4 Update copyright 2024-12-30 21:20:49 -05:00
Matt Nadareski
cd7e89f869 Remove unnecessary action step 2024-12-30 21:20:32 -05:00
Matt Nadareski
0810b4b083 Ensure .NET versions are installed for testing 2024-12-19 10:51:23 -05:00
Matt Nadareski
ba0b126714 Ensure .NET versions are installed for testing 2024-12-19 10:49:39 -05:00
Matt Nadareski
48cdcf96bf Bump version 2024-12-16 12:17:21 -05:00
Matt Nadareski
39ca26bc28 Allow symbols to be packed 2024-12-16 12:14:32 -05:00
Matt Nadareski
5f2940388e Use proper badge 2024-12-06 10:56:08 -05:00
Matt Nadareski
bac0913b35 Ensure publish script is executable 2024-12-06 10:55:08 -05:00
Matt Nadareski
c52973418d Use publish script and update README 2024-12-06 10:52:25 -05:00
Matt Nadareski
c97fe92a3e Update test package versions 2024-11-26 09:53:12 -05:00
Matt Nadareski
176a892993 Fix getopts in publish script 2024-11-26 00:35:49 -05:00
Matt Nadareski
3bb6a6f5c1 Version bump 2024-11-25 12:59:54 -05:00
Matt Nadareski
efd26133aa Fix region tags in path match set 2024-11-25 12:48:45 -05:00
Matt Nadareski
b1d95652ea Implement EqualsExactly extension 2024-11-25 12:27:16 -05:00
Matt Nadareski
ea52117717 Update Extensions and add tests 2024-11-25 12:21:08 -05:00
Matt Nadareski
50a0c62560 If either set... 2024-11-25 11:32:24 -05:00
Matt Nadareski
d98629d22b Move IsNullOrEmpty to IO 2024-11-25 11:29:50 -05:00
Matt Nadareski
2d767122d0 Update MatchUtil and add tests 2024-11-25 11:27:00 -05:00
Matt Nadareski
e4d9848756 Remove unncessary nullability from sets 2024-11-25 11:13:52 -05:00
Matt Nadareski
c96185d550 Rename path tests 2024-11-25 11:07:16 -05:00
Matt Nadareski
c1648ccb71 Rename content tests 2024-11-25 11:03:34 -05:00
Matt Nadareski
31fff83114 Rename natural comparer tests 2024-11-25 11:00:23 -05:00
Matt Nadareski
607b9d30b0 Convert MatchSet to an interface 2024-11-25 01:01:02 -05:00
Matt Nadareski
cfb82f055c Update PathMatchSet and add tests 2024-11-25 00:58:14 -05:00
Matt Nadareski
2057baeded Update ContentMatchSet and add tests 2024-11-25 00:42:12 -05:00
Matt Nadareski
bed920b97c Add case-insensitive matching to file path match 2024-11-24 23:54:41 -05:00
Matt Nadareski
cdf2dd6589 Add file path match tests 2024-11-24 23:52:52 -05:00
Matt Nadareski
f10585aa32 Update PathMatch and add tests 2024-11-24 23:50:02 -05:00
Matt Nadareski
2d4f974623 Update ContentMatch and add tests 2024-11-24 23:16:28 -05:00
Matt Nadareski
9490dabbd6 Overhaul matching extensions 2024-11-24 23:16:11 -05:00
Matt Nadareski
425f846e26 Slight optimizations for natural comparers 2024-11-24 23:15:42 -05:00
Matt Nadareski
a180499090 Fix missed standard byte variant 2024-11-21 10:06:22 -05:00
Matt Nadareski
c7633b1a53 Lists and arrays are better than enumerables 2024-11-21 10:01:23 -05:00
Matt Nadareski
e88771b11f Convert to NoWarn 2024-11-18 20:09:29 -05:00
Matt Nadareski
e596c7c2c1 Bump version 2024-11-15 20:32:23 -05:00
Matt Nadareski
910e963d13 Port extension attribute instead of framework gating 2024-11-15 20:30:15 -05:00
Matt Nadareski
751519fadb Framework only matters for executable 2024-11-15 20:20:33 -05:00
Matt Nadareski
f86f565136 Ensure tests pass 2024-11-13 00:43:29 -05:00
Matt Nadareski
1ad45c6d59 Ensure tests pass 2024-11-13 00:42:49 -05:00
Matt Nadareski
38796776ee Add .NET 9 to target frameworks 2024-11-13 00:34:24 -05:00
Matt Nadareski
c60f587b69 Bump version 2024-11-13 00:31:36 -05:00
Matt Nadareski
8844ba0ae3 Fix ordering bug in comparers 2024-11-13 00:31:00 -05:00
Matt Nadareski
cdd41e8bec Add .NET 9 to target frameworks 2024-11-13 00:26:42 -05:00
Matt Nadareski
655214503f Bump version 2024-11-05 20:22:40 -05:00
Matt Nadareski
5801424db7 Remove all external package requirements 2024-11-05 20:21:27 -05:00
Matt Nadareski
afb9ab3e4e Define delegates instead of anonymous funcs 2024-11-05 20:10:33 -05:00
Matt Nadareski
85006a71bf Replace Linq calls with cheaper methods 2024-11-05 20:01:59 -05:00
Matt Nadareski
90bb82306b Properly support all frameworks 2024-11-05 19:48:08 -05:00
Matt Nadareski
c38e10e55b Simplify enumerable types 2024-11-04 11:38:46 -05:00
Matt Nadareski
e96790d875 Bump version 2024-10-26 19:38:35 -04:00
Matt Nadareski
bf23967f74 Remove mentions of protection 2024-10-26 00:27:51 -04:00
Matt Nadareski
8debf24a44 Fix comparer tests 2024-10-26 00:26:07 -04:00
Matt Nadareski
333b5be76c Various cleanup 2024-10-26 00:21:36 -04:00
Matt Nadareski
b0fdff2d6e Find and fix equal size issue in streams 2024-10-26 00:14:29 -04:00
Matt Nadareski
3b0e74b9f4 Find and fix equal size issue 2024-10-26 00:04:35 -04:00
33 changed files with 2725 additions and 794 deletions

View File

@@ -1,4 +1,4 @@
name: Nuget Pack
name: Build and Test
on:
push:
@@ -16,25 +16,22 @@ jobs:
- name: Setup .NET
uses: actions/setup-dotnet@v4
with:
dotnet-version: 8.0.x
dotnet-version: |
6.0.x
8.0.x
9.0.x
- name: Restore dependencies
run: dotnet restore
- name: Run tests
run: dotnet test
- name: Pack
run: dotnet pack
- name: Upload build
uses: actions/upload-artifact@v4
with:
name: 'Nuget Package'
path: 'SabreTools.Matching/bin/Release/*.nupkg'
- name: Run publish script
run: ./publish-nix.sh
- name: Upload to rolling
uses: ncipollo/release-action@v1.14.0
with:
allowUpdates: True
artifacts: 'SabreTools.Matching/bin/Release/*.nupkg'
artifacts: "*.nupkg,*.snupkg"
body: 'Last built commit: ${{ github.sha }}'
name: 'Rolling Release'
prerelease: True

View File

@@ -11,7 +11,13 @@ jobs:
- name: Setup .NET
uses: actions/setup-dotnet@v4
with:
dotnet-version: 8.0.x
dotnet-version: |
6.0.x
8.0.x
9.0.x
- name: Build
run: dotnet build
run: dotnet build
- name: Run tests
run: dotnet test

7
LICENSE Normal file
View File

@@ -0,0 +1,7 @@
Copyright (c) 2018-2025 Matt Nadareski
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice (including the next paragraph) shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

View File

@@ -1,5 +1,15 @@
# SabreTools.Matching
[![Build and Test](https://github.com/SabreTools/SabreTools.Matching/actions/workflows/build_and_test.yml/badge.svg)](https://github.com/SabreTools/SabreTools.Matching/actions/workflows/build_and_test.yml)
**NOTICE:** This library has been deprecated. All functionality formerly in this library is in [SabreTools.IO](https://github.com/SabreTools/SabreTools.IO) as of version 1.7.5.
This library comprises of code to perform search operations on both byte arrays and streams. There is also an implementation that allows searching for strings in a set of strings, usually used in the context of directory contents.
Find the link to the Nuget package [here](https://www.nuget.org/packages/SabreTools.Matching).
## Releases
For the most recent stable build, download the latest release here: [Releases Page](https://github.com/SabreTools/SabreTools.Matching/releases)
For the latest WIP build here: [Rolling Release](https://github.com/SabreTools/SabreTools.Matching/releases/rolling)

Binary file not shown.

View File

@@ -8,7 +8,7 @@ namespace SabreTools.Matching.Test.Compare
public class NaturalComparerTests
{
[Fact]
public void NaturalComparerListSortTest()
public void ListSort_Numeric()
{
// Setup arrays
string[] sortable = ["0", "100", "5", "2", "1000"];
@@ -22,17 +22,17 @@ namespace SabreTools.Matching.Test.Compare
}
[Fact]
public void NaturalReversedComparerListSortTest()
public void ListSort_Mixed()
{
// Setup arrays
string[] sortable = ["0", "100", "5", "2", "1000"];
string[] expected = ["1000", "100", "5", "2", "0"];
string[] sortable = ["b3b", "c", "b", "a", "a1"];
string[] expected = ["a", "a1", "b", "b3b", "c"];
// Run sorting on array
Array.Sort(sortable, new NaturalReversedComparer());
Array.Sort(sortable, new NaturalComparer());
// Check the output
Assert.True(sortable.SequenceEqual(expected));
}
}
}
}

View File

@@ -6,54 +6,61 @@ namespace SabreTools.Matching.Test.Compare
public class NaturalComparerUtilTests
{
[Fact]
public void CompareNumericBothNullTest()
public void CompareNumeric_BothNull_Equal()
{
int actual = NaturalComparerUtil.CompareNumeric(null, null);
int actual = NaturalComparerUtil.ComparePaths(null, null);
Assert.Equal(0, actual);
}
[Fact]
public void CompareNumericSingleNullTest()
public void CompareNumeric_SingleNull_Ordered()
{
int actual = NaturalComparerUtil.CompareNumeric(null, "notnull");
int actual = NaturalComparerUtil.ComparePaths(null, "notnull");
Assert.Equal(-1, actual);
actual = NaturalComparerUtil.CompareNumeric("notnull", null);
actual = NaturalComparerUtil.ComparePaths("notnull", null);
Assert.Equal(1, actual);
}
[Fact]
public void CompareNumericBothEqualTest()
public void CompareNumeric_BothEqual_Equal()
{
int actual = NaturalComparerUtil.CompareNumeric("notnull", "notnull");
int actual = NaturalComparerUtil.ComparePaths("notnull", "notnull");
Assert.Equal(0, actual);
}
[Fact]
public void CompareNumericBothEqualWithPathTest()
public void CompareNumeric_BothEqualWithPath_Equal()
{
int actual = NaturalComparerUtil.CompareNumeric("notnull/file.ext", "notnull/file.ext");
int actual = NaturalComparerUtil.ComparePaths("notnull/file.ext", "notnull/file.ext");
Assert.Equal(0, actual);
}
[Fact]
public void CompareNumericNumericNonDecimalStringTest()
public void CompareNumeric_BothEqualWithAltPath_Equal()
{
int actual = NaturalComparerUtil.CompareNumeric("100", "10");
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.CompareNumeric("10", "100");
actual = NaturalComparerUtil.ComparePaths("10", "100");
Assert.Equal(-1, actual);
}
[Fact]
public void CompareNumericNumericDecimalStringTest()
public void CompareNumeric_NumericDecimalString_Ordered()
{
int actual = NaturalComparerUtil.CompareNumeric("100.100", "100.10");
int actual = NaturalComparerUtil.ComparePaths("100.100", "100.10");
Assert.Equal(1, actual);
actual = NaturalComparerUtil.CompareNumeric("100.10", "100.100");
actual = NaturalComparerUtil.ComparePaths("100.10", "100.100");
Assert.Equal(-1, actual);
}
}
}
}

View File

@@ -0,0 +1,38 @@
using System;
using System.Linq;
using SabreTools.Matching.Compare;
using Xunit;
namespace SabreTools.Matching.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

@@ -0,0 +1,202 @@
using System;
using System.Collections.Generic;
using System.IO;
using SabreTools.Matching.Content;
using Xunit;
namespace SabreTools.Matching.Test.Content
{
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.Matching.Content;
using Xunit;
namespace SabreTools.Matching.Test.Content
{
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,396 @@
using System;
using Xunit;
namespace SabreTools.Matching.Test
{
public class ExtensionsTests
{
#region Find All Positions
[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 First Position
[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 Last Position
[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 Equals Exactly
[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 Starts With
[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 Ends With
[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
}
}

View File

@@ -0,0 +1,324 @@
using System.Collections.Generic;
using System.IO;
using SabreTools.Matching.Content;
using Xunit;
namespace SabreTools.Matching.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,22 @@
using System.IO;
using SabreTools.Matching.Paths;
using Xunit;
namespace SabreTools.Matching.Test.Paths
{
/// <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.Matching.Paths;
using Xunit;
namespace SabreTools.Matching.Test.Paths
{
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.Matching.Paths;
using Xunit;
namespace SabreTools.Matching.Test.Paths
{
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

@@ -1,29 +1,30 @@
<Project Sdk="Microsoft.NET.Sdk">
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFrameworks>net8.0;net9.0</TargetFrameworks>
<IsPackable>false</IsPackable>
<LangVersion>latest</LangVersion>
<NoWarn>NU1903</NoWarn>
<Nullable>enable</Nullable>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="coverlet.collector" Version="6.0.4">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.14.1" />
<PackageReference Include="xunit" Version="2.9.3" />
<PackageReference Include="xunit.runner.visualstudio" Version="3.1.3">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\SabreTools.Matching\SabreTools.Matching.csproj" />
</ItemGroup>
<PropertyGroup>
<TargetFrameworks>net6.0;net8.0</TargetFrameworks>
<IsPackable>false</IsPackable>
<LangVersion>latest</LangVersion>
<Nullable>enable</Nullable>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="coverlet.collector" Version="6.0.0">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.8.0" />
<PackageReference Include="xunit" Version="2.6.2" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.5.4">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\SabreTools.Matching\SabreTools.Matching.csproj" />
</ItemGroup>
</Project>

View File

@@ -11,25 +11,22 @@
using System;
using System.Collections.Generic;
#if NET40_OR_GREATER || NETCOREAPP
using System.Linq;
#endif
using System.Text.RegularExpressions;
namespace SabreTools.Matching.Compare
{
public class NaturalComparer : Comparer<string>, IDisposable
{
private readonly Dictionary<string, string[]> table;
private readonly Dictionary<string, string[]> _table;
public NaturalComparer()
{
table = [];
_table = [];
}
public void Dispose()
{
table.Clear();
_table.Clear();
}
public override int Compare(string? x, string? y)
@@ -47,46 +44,20 @@ namespace SabreTools.Matching.Compare
if (x.ToLowerInvariant() == y.ToLowerInvariant())
return x.CompareTo(y);
if (!table.TryGetValue(x, out string[]? x1))
if (!_table.TryGetValue(x, out string[]? x1))
{
//x1 = Regex.Split(x.Replace(" ", string.Empty), "([0-9]+)");
#if NET20 || NET35
var nonempty = new List<string>();
x1 = Regex.Split(x.ToLowerInvariant(), "([0-9]+)");
foreach (var s in x1)
{
if (!string.IsNullOrEmpty(s))
nonempty.Add(s);
}
x1 = nonempty.ToArray();
#else
x1 = Regex.Split(x.ToLowerInvariant(), "([0-9]+)")
.Where(s => !string.IsNullOrEmpty(s))
.ToArray();
#endif
table.Add(x, x1);
x1 = Array.FindAll(x1, s => !string.IsNullOrEmpty(s));
_table.Add(x, x1);
}
if (!table.TryGetValue(y, out string[]? y1))
if (!_table.TryGetValue(y, out string[]? y1))
{
//y1 = Regex.Split(y.Replace(" ", string.Empty), "([0-9]+)");
#if NET20 || NET35
var nonempty = new List<string>();
y1 = Regex.Split(x.ToLowerInvariant(), "([0-9]+)");
foreach (var s in y1)
{
if (!string.IsNullOrEmpty(s))
nonempty.Add(s);
}
y1 = nonempty.ToArray();
#else
y1 = Regex.Split(x.ToLowerInvariant(), "([0-9]+)")
.Where(s => !string.IsNullOrEmpty(s))
.ToArray();
#endif
table.Add(y, y1);
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++)
@@ -95,9 +66,9 @@ namespace SabreTools.Matching.Compare
return PartCompare(x1[i], y1[i]);
}
if (y1.Length > x1.Length)
if (x1.Length > y1.Length)
return 1;
else if (x1.Length > y1.Length)
else if (y1.Length > x1.Length)
return -1;
else
return x.CompareTo(y);
@@ -106,10 +77,10 @@ namespace SabreTools.Matching.Compare
private static int PartCompare(string left, string right)
{
if (!long.TryParse(left, out long x))
return NaturalComparerUtil.CompareNumeric(left, right);
return NaturalComparerUtil.ComparePaths(left, right);
if (!long.TryParse(right, out long y))
return NaturalComparerUtil.CompareNumeric(left, right);
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)

View File

@@ -1,82 +1,93 @@
using System.IO;
namespace SabreTools.Matching.Compare
namespace SabreTools.Matching.Compare
{
public static class NaturalComparerUtil
{
/// <summary>
/// Compare two strings by numeric parts
/// Compare two strings by path parts
/// </summary>
public static int CompareNumeric(string? s1, string? s2)
public static int ComparePaths(string? left, string? right)
{
// If both strings are null, return
if (s1 == null && s2 == null)
if (left == null && right == null)
return 0;
// If one is null, then say that's less than
if (s1 == null)
if (left == null)
return -1;
if (s2 == null)
if (right == null)
return 1;
// Save the orginal strings, for later comparison
string s1orig = s1;
string s2orig = s2;
// Normalize the path seperators
left = left.Replace('\\', '/');
right = right.Replace('\\', '/');
// We want to normalize the strings, so we set both to lower case
s1 = s1.ToLowerInvariant();
s2 = s2.ToLowerInvariant();
// 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 (s1 == s2)
return s1orig.CompareTo(s2orig);
if (left == right)
return leftOrig.CompareTo(rightOrig);
// Now split into path parts after converting AltDirSeparator to DirSeparator
s1 = s1.Replace(Path.AltDirectorySeparatorChar, Path.DirectorySeparatorChar);
s2 = s2.Replace(Path.AltDirectorySeparatorChar, Path.DirectorySeparatorChar);
string[] s1parts = s1.Split(Path.DirectorySeparatorChar);
string[] s2parts = s2.Split(Path.DirectorySeparatorChar);
// Now split into path parts
string[] leftParts = left.Split('/');
string[] rightParts = right.Split('/');
// Then compare each part in turn
for (int j = 0; j < s1parts.Length && j < s2parts.Length; j++)
for (int i = 0; i < leftParts.Length && i < rightParts.Length; i++)
{
int compared = CompareNumericPart(s1parts[j], s2parts[j]);
if (compared != 0)
return compared;
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 (s1parts.Length > s2parts.Length)
if (leftParts.Length > rightParts.Length)
return 1;
if (s1parts.Length < s2parts.Length)
if (leftParts.Length < rightParts.Length)
return -1;
return s1orig.CompareTo(s2orig);
return leftOrig.CompareTo(rightOrig);
}
private static int CompareNumericPart(string s1, string s2)
/// <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 < s1.Length && i < s2.Length; i++)
for (int i = 0; i < left.Length && i < right.Length; i++)
{
int s1c = s1[i];
int s2c = s2[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 (s1c == s2c)
if (leftChar == rightChar)
continue;
// If they're different, check which one was larger
if (s1c > s2c)
return 1;
if (s1c < s2c)
return -1;
return leftChar > rightChar ? 1 : -1;
}
// If we got out here, then it looped through at least one of the strings
if (s1.Length > s2.Length)
if (left.Length > right.Length)
return 1;
if (s1.Length < s2.Length)
if (left.Length < right.Length)
return -1;
return 0;

View File

@@ -11,25 +11,22 @@
using System;
using System.Collections.Generic;
#if NET40_OR_GREATER || NETCOREAPP
using System.Linq;
#endif
using System.Text.RegularExpressions;
namespace SabreTools.Matching.Compare
{
public class NaturalReversedComparer : Comparer<string>, IDisposable
{
private readonly Dictionary<string, string[]> table;
private readonly Dictionary<string, string[]> _table;
public NaturalReversedComparer()
{
table = [];
_table = [];
}
public void Dispose()
{
table.Clear();
_table.Clear();
}
public override int Compare(string? x, string? y)
@@ -47,46 +44,20 @@ namespace SabreTools.Matching.Compare
if (y.ToLowerInvariant() == x.ToLowerInvariant())
return y.CompareTo(x);
if (!table.TryGetValue(x, out string[]? x1))
if (!_table.TryGetValue(x, out string[]? x1))
{
//x1 = Regex.Split(x.Replace(" ", string.Empty), "([0-9]+)");
#if NET20 || NET35
var nonempty = new List<string>();
x1 = Regex.Split(x.ToLowerInvariant(), "([0-9]+)");
foreach (var s in x1)
{
if (!string.IsNullOrEmpty(s))
nonempty.Add(s);
}
x1 = nonempty.ToArray();
#else
x1 = Regex.Split(x.ToLowerInvariant(), "([0-9]+)")
.Where(s => !string.IsNullOrEmpty(s))
.ToArray();
#endif
table.Add(x, x1);
x1 = Array.FindAll(x1, s => !string.IsNullOrEmpty(s));
_table.Add(x, x1);
}
if (!table.TryGetValue(y, out string[]? y1))
if (!_table.TryGetValue(y, out string[]? y1))
{
//y1 = Regex.Split(y.Replace(" ", string.Empty), "([0-9]+)");
#if NET20 || NET35
var nonempty = new List<string>();
y1 = Regex.Split(x.ToLowerInvariant(), "([0-9]+)");
foreach (var s in y1)
{
if (!string.IsNullOrEmpty(s))
nonempty.Add(s);
}
y1 = nonempty.ToArray();
#else
y1 = Regex.Split(x.ToLowerInvariant(), "([0-9]+)")
.Where(s => !string.IsNullOrEmpty(s))
.ToArray();
#endif
table.Add(y, y1);
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++)
@@ -106,10 +77,10 @@ namespace SabreTools.Matching.Compare
private static int PartCompare(string left, string right)
{
if (!long.TryParse(left, out long x))
return NaturalComparerUtil.CompareNumeric(right, left);
return NaturalComparerUtil.ComparePaths(right, left);
if (!long.TryParse(right, out long y))
return NaturalComparerUtil.CompareNumeric(right, left);
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)

View File

@@ -1,3 +1,4 @@
using System;
using System.IO;
namespace SabreTools.Matching.Content
@@ -10,35 +11,74 @@ namespace SabreTools.Matching.Content
/// <summary>
/// Content to match
/// </summary>
#if NETFRAMEWORK || NETCOREAPP
public byte?[]? Needle { get; private set; }
#else
public byte?[]? Needle { get; init; }
#endif
public byte?[] Needle { get; }
/// <summary>
/// Starting index for matching
/// </summary>
public int Start { get; internal set; }
private readonly int _start;
/// <summary>
/// Ending index for matching
/// </summary>
public int End { get; private set; }
private readonly int _end;
/// <summary>
/// Constructor
/// </summary>
/// <param name="needle">Byte array representing the search</param>
/// <param name="start">Optional starting index</param>
/// <param name="end">Optional ending index</param>
public ContentMatch(byte?[]? needle, int start = -1, int end = -1)
/// <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)
{
this.Needle = needle;
this.Start = start;
this.End = end;
// 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>
@@ -46,28 +86,72 @@ namespace SabreTools.Matching.Content
/// </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 on error</returns>
/// <returns>Found position on success, -1 otherwise</returns>
public int Match(byte[]? stack, bool reverse = false)
{
// If either array is null or empty, we can't do anything
if (stack == null || stack.Length == 0 || this.Needle == null || this.Needle.Length == 0)
// If either set is null or empty
if (stack == null || stack.Length == 0 || Needle.Length == 0)
return -1;
// If the needle array is larger than the stack array, it can't be contained within
if (this.Needle.Length > stack.Length)
// 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 = this.Start;
int end = this.End;
int start = _start < 0 ? 0 : _start;
int end = _end < 0 ? stack.Length - Needle.Length : _end;
// If start or end are not set properly, set them to defaults
if (start < 0)
start = 0;
if (end < 0)
end = stack.Length - this.Needle.Length;
// 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;
for (int i = reverse ? end : start; reverse ? i > start : i < end; i += reverse ? -1 : 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)
@@ -89,25 +173,21 @@ namespace SabreTools.Matching.Content
/// <returns>True if the needle matches the stack at a given index</returns>
private bool EqualAt(byte[] stack, int index)
{
// If the needle is invalid, we can't do anything
if (this.Needle == null)
return false;
// 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 (this.Needle.Length > stack.Length - index)
if (Needle.Length > stack.Length - index)
return false;
// Loop through and check the value
for (int i = 0; i < this.Needle.Length; i++)
for (int i = 0; i < Needle.Length; i++)
{
// A null value is a wildcard
if (this.Needle[i] == null)
if (Needle[i] == null)
continue;
else if (stack[i + index] != this.Needle[i])
else if (stack[i + index] != Needle[i])
return false;
}
@@ -123,28 +203,72 @@ namespace SabreTools.Matching.Content
/// </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 on error</returns>
/// <returns>Found position on success, -1 otherwise</returns>
public int Match(Stream? stack, bool reverse = false)
{
// If either array is null or empty, we can't do anything
if (stack == null || stack.Length == 0 || this.Needle == null || this.Needle.Length == 0)
// If either set is null or empty
if (stack == null || stack.Length == 0 || Needle.Length == 0)
return -1;
// If the needle array is larger than the stack array, it can't be contained within
if (this.Needle.Length > stack.Length)
// 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 = this.Start;
int end = this.End;
int start = _start < 0 ? 0 : _start;
int end = _end < 0 ? (int)stack.Length - Needle.Length : _end;
// If start or end are not set properly, set them to defaults
if (start < 0)
start = 0;
if (end < 0)
end = (int)(stack.Length - this.Needle.Length);
// 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;
for (int i = reverse ? end : start; reverse ? i > start : i < end; i += reverse ? -1 : 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)
@@ -166,16 +290,12 @@ namespace SabreTools.Matching.Content
/// <returns>True if the needle matches the stack at a given index</returns>
private bool EqualAt(Stream stack, int index)
{
// If the needle is invalid, we can't do anything
if (this.Needle == null)
return false;
// 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 (this.Needle.Length > stack.Length - index)
if (Needle.Length > stack.Length - index)
return false;
// Save the current position and move to the index
@@ -186,16 +306,16 @@ namespace SabreTools.Matching.Content
bool matched = true;
// Loop through and check the value
for (int i = 0; i < this.Needle.Length; i++)
for (int i = 0; i < Needle.Length; i++)
{
byte stackValue = (byte)stack.ReadByte();
// A null value is a wildcard
if (this.Needle[i] == null)
if (Needle[i] == null)
{
continue;
}
else if (stackValue != this.Needle[i])
else if (stackValue != Needle[i])
{
matched = false;
break;
@@ -209,4 +329,4 @@ namespace SabreTools.Matching.Content
#endregion
}
}
}

View File

@@ -1,28 +1,18 @@
using System;
using System.Collections.Generic;
using System.IO;
#if NET40_OR_GREATER || NETCOREAPP
using System.Linq;
#endif
namespace SabreTools.Matching.Content
{
/// <summary>
/// A set of content matches that work together
/// </summary>
public class ContentMatchSet : MatchSet<ContentMatch, byte?[]>
public class ContentMatchSet : IMatchSet<ContentMatch, byte?[]>
{
/// <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 protection name, or `null`, in which case it will cause
/// the protection to be omitted.
/// </remarks>
public Func<string, byte[]?, List<int>, string?>? GetArrayVersion { get; private set; }
/// <inheritdoc/>
public List<ContentMatch> Matchers { get; }
/// <inheritdoc/>
public string SetName { get; }
/// <summary>
/// Function to get a content version
@@ -31,93 +21,110 @@ namespace SabreTools.Matching.Content
/// 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 protection name, or `null`, in which case it will cause
/// the protection to be omitted.
/// to the match name, or `null`, in which case it will cause
/// the match name to be omitted.
/// </remarks>
public Func<string, Stream?, List<int>, string?>? GetStreamVersion { get; private set; }
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
public ContentMatchSet(byte?[] needle, string protectionName)
: this(new List<byte?[]> { needle }, getArrayVersion: null, protectionName) { }
/// <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) { }
public ContentMatchSet(List<byte?[]> needles, string protectionName)
: this(needles, getArrayVersion: null, protectionName) { }
/// <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));
public ContentMatchSet(ContentMatch needle, string protectionName)
: this(new List<ContentMatch>() { needle }, getArrayVersion: null, protectionName) { }
public ContentMatchSet(List<ContentMatch> needles, string protectionName)
: this(needles, getArrayVersion: null, protectionName) { }
Matchers = needles;
SetName = setName;
GetArrayVersion = null;
GetStreamVersion = null;
}
#endregion
#region Array Constructors
public ContentMatchSet(byte?[] needle, Func<string, byte[]?, List<int>, string?>? getArrayVersion, string protectionName)
: this(new List<byte?[]> { needle }, getArrayVersion, protectionName) { }
/// <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) { }
#if NET20 || NET35
public ContentMatchSet(List<byte?[]> needles, Func<string, byte[]?, List<int>, string?>? getArrayVersion, string protectionName)
/// <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)
{
var matchers = new List<ContentMatch>();
foreach (var n in needles)
{
matchers.Add(new ContentMatch(n));
}
// Validate the inputs
if (needles.Count == 0)
throw new InvalidDataException(nameof(needles));
Matchers = matchers;
GetArrayVersion = getArrayVersion;
ProtectionName = protectionName;
}
#else
public ContentMatchSet(List<byte?[]> needles, Func<string, byte[]?, List<int>, string?>? getArrayVersion, string protectionName)
: this(needles.Select(n => new ContentMatch(n)).ToList(), getArrayVersion, protectionName) { }
#endif
public ContentMatchSet(ContentMatch needle, Func<string, byte[]?, List<int>, string?>? getArrayVersion, string protectionName)
: this(new List<ContentMatch>() { needle }, getArrayVersion, protectionName) { }
public ContentMatchSet(List<ContentMatch> needles, Func<string, byte[]?, List<int>, string?>? getArrayVersion, string protectionName)
{
Matchers = needles;
GetArrayVersion = getArrayVersion;
ProtectionName = protectionName;
SetName = setName;
GetArrayVersion = getVersion;
GetStreamVersion = null;
}
#endregion
#region Stream Constructors
public ContentMatchSet(byte?[] needle, Func<string, Stream?, List<int>, string?>? getStreamVersion, string protectionName)
: this(new List<byte?[]> { needle }, getStreamVersion, protectionName) { }
/// <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) { }
#if NET20 || NET35
public ContentMatchSet(List<byte?[]> needles, Func<string, Stream?, List<int>, string?>? getStreamVersion, string protectionName)
/// <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)
{
var matchers = new List<ContentMatch>();
foreach (var n in needles)
{
matchers.Add(new ContentMatch(n));
}
// Validate the inputs
if (needles.Count == 0)
throw new InvalidDataException(nameof(needles));
Matchers = matchers;
GetStreamVersion = getStreamVersion;
ProtectionName = protectionName;
}
#else
public ContentMatchSet(List<byte?[]> needles, Func<string, Stream?, List<int>, string?>? getStreamVersion, string protectionName)
: this(needles.Select(n => new ContentMatch(n)).ToList(), getStreamVersion, protectionName) { }
#endif
public ContentMatchSet(ContentMatch needle, Func<string, Stream?, List<int>, string?>? getStreamVersion, string protectionName)
: this(new List<ContentMatch>() { needle }, getStreamVersion, protectionName) { }
public ContentMatchSet(List<ContentMatch> needles, Func<string, Stream?, List<int>, string?>? getStreamVersion, string protectionName)
{
Matchers = needles;
GetStreamVersion = getStreamVersion;
ProtectionName = protectionName;
SetName = setName;
GetArrayVersion = null;
GetStreamVersion = getVersion;
}
#endregion
@@ -131,12 +138,8 @@ namespace SabreTools.Matching.Content
/// <returns>List of matching positions, if any</returns>
public List<int> MatchesAll(byte[]? stack)
{
// If no content matches are defined, we fail out
#if NET20 || NET35
if (Matchers == null || new List<ContentMatch>(Matchers).Count == 0)
#else
if (Matchers == null || !Matchers.Any())
#endif
// If either set is null or empty
if (stack == null || stack.Length == 0 || Matchers.Count == 0)
return [];
// Initialize the position list
@@ -148,8 +151,8 @@ namespace SabreTools.Matching.Content
int position = contentMatch.Match(stack);
if (position < 0)
return [];
else
positions.Add(position);
positions.Add(position);
}
return positions;
@@ -162,12 +165,8 @@ namespace SabreTools.Matching.Content
/// <returns>First matching position on success, -1 on error</returns>
public int MatchesAny(byte[]? stack)
{
// If no content matches are defined, we fail out
#if NET20 || NET35
if (Matchers == null || new List<ContentMatch>(Matchers).Count == 0)
#else
if (Matchers == null || !Matchers.Any())
#endif
// 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
@@ -192,12 +191,8 @@ namespace SabreTools.Matching.Content
/// <returns>List of matching positions, if any</returns>
public List<int> MatchesAll(Stream? stack)
{
// If no content matches are defined, we fail out
#if NET20 || NET35
if (Matchers == null || new List<ContentMatch>(Matchers).Count == 0)
#else
if (Matchers == null || !Matchers.Any())
#endif
// If either set is null or empty
if (stack == null || stack.Length == 0 || Matchers.Count == 0)
return [];
// Initialize the position list
@@ -209,8 +204,8 @@ namespace SabreTools.Matching.Content
int position = contentMatch.Match(stack);
if (position < 0)
return [];
else
positions.Add(position);
positions.Add(position);
}
return positions;
@@ -223,12 +218,8 @@ namespace SabreTools.Matching.Content
/// <returns>First matching position on success, -1 on error</returns>
public int MatchesAny(Stream? stack)
{
// If no content matches are defined, we fail out
#if NET20 || NET35
if (Matchers == null || new List<ContentMatch>(Matchers).Count == 0)
#else
if (Matchers == null || !Matchers.Any())
#endif
// 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
@@ -244,4 +235,4 @@ namespace SabreTools.Matching.Content
#endregion
}
}
}

View File

@@ -0,0 +1,31 @@
using System.Collections.Generic;
using System.IO;
namespace SabreTools.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,9 @@
#if NET20
namespace System.Runtime.CompilerServices
{
[AttributeUsage(AttributeTargets.Assembly | AttributeTargets.Class | AttributeTargets.Method)]
internal sealed class ExtensionAttribute : Attribute {}
}
#endif

View File

@@ -1,8 +1,5 @@
using System;
using System.Collections.Generic;
#if NET40_OR_GREATER || NETCOREAPP
using System.Linq;
#endif
using SabreTools.Matching.Content;
namespace SabreTools.Matching
@@ -10,34 +7,62 @@ namespace SabreTools.Matching
public static class Extensions
{
/// <summary>
/// Indicates whether the specified array is null or has a length of zero
/// Find all positions of one array in another, if possible
/// </summary>
public static bool IsNullOrEmpty(this Array? array)
/// <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)
{
return array == null || array.Length == 0;
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, if possible
/// Find all positions of one array in another, if possible
/// </summary>
public static List<int> FindAllPositions(this byte[] stack, byte?[]? needle, int start = 0, int end = -1)
/// <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 = [];
// Initialize the loop variables
int lastPosition = start;
var matcher = new ContentMatch(needle, end: end);
// If either set is null or empty
if (stack.Length == 0 || needle.Length == 0)
return positions;
// Loop over and get all positions
while (true)
// 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)
{
matcher.Start = lastPosition;
lastPosition = matcher.Match(stack, false);
if (lastPosition < 0)
// 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;
positions.Add(lastPosition);
// Append the position and reset the start index
positions.Add(position);
start = position + 1;
}
return positions;
@@ -46,114 +71,156 @@ namespace SabreTools.Matching
/// <summary>
/// Find the first position of one array in another, if possible
/// </summary>
public static bool FirstPosition(this byte[] stack, byte[]? needle, out int position, int start = 0, int end = -1)
/// <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 NET20 || NET35
byte?[]? nullableNeedle;
if (needle == null)
{
nullableNeedle = null;
}
else
{
nullableNeedle = new byte?[needle.Length];
for (int i = 0; i < needle.Length; i++)
{
nullableNeedle[i] = needle[i];
}
}
#else
byte?[]? nullableNeedle = needle?.Select(b => (byte?)b).ToArray();
#endif
return stack.FirstPosition(nullableNeedle, out position, start, end);
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>
public static bool FirstPosition(this byte[] stack, byte?[]? needle, out int position, int start = 0, int end = -1)
/// <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);
position = matcher.Match(stack, false);
return position >= 0;
return matcher.Match(stack, reverse: false);
}
/// <summary>
/// Find the last position of one array in another, if possible
/// </summary>
public static bool LastPosition(this byte[] stack, byte?[]? needle, out int position, int start = 0, int end = -1)
/// <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);
position = matcher.Match(stack, true);
return position >= 0;
return matcher.Match(stack, reverse: true);
}
/// <summary>
/// See if a byte array starts with another
/// Check if a byte array exactly matches another
/// </summary>
public static bool StartsWith(this byte[] stack, byte[]? needle, bool exact = false)
/// <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 we have any invalid inputs, we return false
if (needle == null
|| stack.Length == 0 || needle.Length == 0
|| needle.Length > stack.Length
|| (exact && stack.Length != needle.Length))
{
return false;
}
return stack.FirstPosition(needle, out int _, start: 0, end: 1);
byte?[] nullableNeedle = Array.ConvertAll(needle, b => (byte?)b);
return EqualsExactly(stack, nullableNeedle);
}
/// <summary>
/// See if a byte array starts with another
/// Check if a byte array exactly matches another
/// </summary>
public static bool StartsWith(this byte[] stack, byte?[]? needle, bool exact = false)
/// <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 we have any invalid inputs, we return false
if (needle == null
|| stack.Length == 0 || needle.Length == 0
|| needle.Length > stack.Length
|| (exact && stack.Length != needle.Length))
{
// If either set is null or empty
if (stack.Length == 0 || needle.Length == 0)
return false;
}
return stack.FirstPosition(needle, out int _, start: 0, end: 1);
// 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>
/// See if a byte array ends with another
/// Check if a byte array starts with another
/// </summary>
public static bool EndsWith(this byte[] stack, byte[]? needle, bool exact = false)
/// <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 we have any invalid inputs, we return false
if (needle == null
|| stack.Length == 0 || needle.Length == 0
|| needle.Length > stack.Length
|| (exact && stack.Length != needle.Length))
{
return false;
}
return stack.FirstPosition(needle, out int _, start: stack.Length - needle.Length);
byte?[] nullableNeedle = Array.ConvertAll(needle, b => (byte?)b);
return StartsWith(stack, nullableNeedle);
}
/// <summary>
/// See if a byte array ends with another
/// Check if a byte array starts with another
/// </summary>
public static bool EndsWith(this byte[] stack, byte?[]? needle, bool exact = false)
/// <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 we have any invalid inputs, we return false
if (needle == null
|| stack.Length == 0 || needle.Length == 0
|| needle.Length > stack.Length
|| (exact && stack.Length != needle.Length))
{
// If either set is null or empty
if (stack.Length == 0 || needle.Length == 0)
return false;
}
return stack.FirstPosition(needle, out int _, start: stack.Length - needle.Length);
// 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;
}
}
}
}

View File

@@ -1,7 +1,13 @@
namespace SabreTools.Matching
{
/// <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

@@ -5,16 +5,16 @@ namespace SabreTools.Matching
/// <summary>
/// Wrapper for a single set of matching criteria
/// </summary>
public abstract class MatchSet<T, U> where T : IMatch<U>
public interface IMatchSet<T, U> where T : IMatch<U>
{
/// <summary>
/// Set of all matchers
/// </summary>
public IEnumerable<T>? Matchers { get; set; }
public List<T> Matchers { get; }
/// <summary>
/// Name of the protection to show
/// Unique name for the match set
/// </summary>
public string? ProtectionName { get; set; }
public string SetName { get; }
}
}
}

View File

@@ -1,11 +1,6 @@
#if NET40_OR_GREATER || NETCOREAPP
using System.Collections.Concurrent;
#endif
using System.Collections.Generic;
using System.IO;
#if NET40_OR_GREATER || NETCOREAPP
using System.Linq;
#endif
using System.Text;
using SabreTools.Matching.Content;
using SabreTools.Matching.Paths;
@@ -23,37 +18,37 @@ namespace SabreTools.Matching
/// </summary>
/// <param name="file">File to check for matches</param>
/// <param name="stack">Array to search</param>
/// <param name="matchers">Enumerable of ContentMatchSets to be run on the file</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 matched protections, null or empty otherwise</returns>
#if NET20 || NET35
public static Queue<string>? GetAllMatches(string file, byte[]? stack, IEnumerable<ContentMatchSet>? matchers, bool includeDebug = false)
#else
public static ConcurrentQueue<string>? GetAllMatches(string file, byte[]? stack, IEnumerable<ContentMatchSet>? matchers, bool includeDebug = false)
#endif
{
return FindAllMatches(file, stack, matchers, includeDebug, false);
}
/// <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="matchers">Enumerable of ContentMatchSets to be run on the file</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 matched protection, null otherwise</returns>
public static string? GetFirstMatch(string file, byte[]? stack, IEnumerable<ContentMatchSet>? matchers, bool includeDebug = false)
/// <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, matchers, includeDebug, true);
var contentMatches = FindAllMatches(file, stack, matchSets, any, includeDebug, true);
if (contentMatches == null || contentMatches.Count == 0)
return null;
#if NET20 || NET35
return contentMatches.Peek();
#else
return contentMatches.First();
#endif
return contentMatches[0];
}
/// <summary>
@@ -61,74 +56,71 @@ namespace SabreTools.Matching
/// </summary>
/// <param name="file">File to check for matches</param>
/// <param name="stack">Array to search</param>
/// <param name="matchers">Enumerable of ContentMatchSets to be run on the file</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 matched protections, null or empty otherwise</returns>
#if NET20 || NET35
private static Queue<string>? FindAllMatches(string file, byte[]? stack, IEnumerable<ContentMatchSet>? matchers, bool includeDebug, bool stopAfterFirst)
#else
private static ConcurrentQueue<string>? FindAllMatches(string file, byte[]? stack, IEnumerable<ContentMatchSet>? matchers, bool includeDebug, bool stopAfterFirst)
#endif
/// <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 there's no mappings, we can't match
#if NET20 || NET35
if (matchers == null || new List<ContentMatchSet>(matchers).Count == 0)
#else
if (matchers == null || !matchers.Any())
#endif
return null;
// If either set is null or empty
if (stack == null || stack.Length == 0 || matchSets.Count == 0)
return [];
// Initialize the queue of matched protections
#if NET20 || NET35
var matchedProtections = new Queue<string>();
#else
var matchedProtections = new ConcurrentQueue<string>();
#endif
// Initialize the list of matches
var matchesList = new List<string>();
// Loop through and try everything otherwise
foreach (var matcher in matchers)
foreach (var matcher in matchSets)
{
// Determine if the matcher passes
var positions = matcher.MatchesAll(stack);
if (positions.Count == 0)
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;
// Format the list of all positions found
#if NET20 || NET35
var positionStrs = new List<string>();
foreach (int pos in positions)
{
positionStrs.Add(pos.ToString());
}
string positionsString = string.Join(", ", [.. positionStrs]);
#else
string positionsString = string.Join(", ", positions);
#endif
// Build the output string
var matchString = new StringBuilder();
matchString.Append(matcher.SetName);
// If we there is no version method, just return the protection name
if (matcher.GetArrayVersion == null)
{
matchedProtections.Enqueue((matcher.ProtectionName ?? "Unknown Protection") + (includeDebug ? $" (Index {positionsString})" : string.Empty));
}
// Otherwise, invoke the version method
else
// 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;
matchedProtections.Enqueue($"{matcher.ProtectionName ?? "Unknown Protection"} {version}".Trim() + (includeDebug ? $" (Index {positionsString})" : string.Empty));
// Trim and add the version
version = version.Trim();
if (version.Length > 0)
matchString.Append($" {version}");
}
// If we're stopping after the first protection, bail out here
// 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 matchedProtections;
return matchesList;
}
return matchedProtections;
return matchesList;
}
#endregion
@@ -140,37 +132,37 @@ namespace SabreTools.Matching
/// </summary>
/// <param name="file">File to check for matches</param>
/// <param name="stack">Stream to search</param>
/// <param name="matchers">Enumerable of ContentMatchSets to be run on the file</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 matched protections, null or empty otherwise</returns>
#if NET20 || NET35
public static Queue<string>? GetAllMatches(string file, Stream? stack, IEnumerable<ContentMatchSet>? matchers, bool includeDebug = false)
#else
public static ConcurrentQueue<string>? GetAllMatches(string file, Stream? stack, IEnumerable<ContentMatchSet>? matchers, bool includeDebug = false)
#endif
{
return FindAllMatches(file, stack, matchers, includeDebug, false);
}
/// <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="matchers">Enumerable of ContentMatchSets to be run on the file</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 matched protection, null otherwise</returns>
public static string? GetFirstMatch(string file, Stream? stack, IEnumerable<ContentMatchSet>? matchers, bool includeDebug = false)
/// <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, matchers, includeDebug, true);
var contentMatches = FindAllMatches(file, stack, matchSets, any, includeDebug, true);
if (contentMatches == null || contentMatches.Count == 0)
return null;
#if NET20 || NET35
return contentMatches.Peek();
#else
return contentMatches.First();
#endif
return contentMatches[0];
}
/// <summary>
@@ -178,74 +170,71 @@ namespace SabreTools.Matching
/// </summary>
/// <param name="file">File to check for matches</param>
/// <param name="stack">Stream to search</param>
/// <param name="matchers">Enumerable of ContentMatchSets to be run on the file</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 matched protections, null or empty otherwise</returns>
#if NET20 || NET35
private static Queue<string>? FindAllMatches(string file, Stream? stack, IEnumerable<ContentMatchSet>? matchers, bool includeDebug, bool stopAfterFirst)
#else
private static ConcurrentQueue<string>? FindAllMatches(string file, Stream? stack, IEnumerable<ContentMatchSet>? matchers, bool includeDebug, bool stopAfterFirst)
#endif
/// <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 there's no mappings, we can't match
#if NET20 || NET35
if (matchers == null || new List<ContentMatchSet>(matchers).Count == 0)
#else
if (matchers == null || !matchers.Any())
#endif
return null;
// If either set is null or empty
if (stack == null || stack.Length == 0 || matchSets.Count == 0)
return [];
// Initialize the queue of matched protections
#if NET20 || NET35
var matchedProtections = new Queue<string>();
#else
var matchedProtections = new ConcurrentQueue<string>();
#endif
// Initialize the list of matches
var matchesList = new List<string>();
// Loop through and try everything otherwise
foreach (var matcher in matchers)
foreach (var matcher in matchSets)
{
// Determine if the matcher passes
var positions = matcher.MatchesAll(stack);
if (positions.Count == 0)
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;
// Format the list of all positions found
#if NET20 || NET35
var positionStrs = new List<string>();
foreach (int pos in positions)
{
positionStrs.Add(pos.ToString());
}
string positionsString = string.Join(", ", [.. positionStrs]);
#else
string positionsString = string.Join(", ", positions);
#endif
// Build the output string
var matchString = new StringBuilder();
matchString.Append(matcher.SetName);
// If we there is no version method, just return the protection name
if (matcher.GetStreamVersion == null)
{
matchedProtections.Enqueue((matcher.ProtectionName ?? "Unknown Protection") + (includeDebug ? $" (Index {positionsString})" : string.Empty));
}
// Otherwise, invoke the version method
else
// 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;
matchedProtections.Enqueue($"{matcher.ProtectionName ?? "Unknown Protection"} {version}".Trim() + (includeDebug ? $" (Index {positionsString})" : string.Empty));
// Trim and add the version
version = version.Trim();
if (version.Length > 0)
matchString.Append($" {version}");
}
// If we're stopping after the first protection, bail out here
// 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 matchedProtections;
return matchesList;
}
return matchedProtections;
return matchesList;
}
#endregion
@@ -255,152 +244,121 @@ namespace SabreTools.Matching
/// <summary>
/// Get all path matches for a given list of matchers
/// </summary>
/// <param name="file">File path to check for matches</param>
/// <param name="matchers">Enumerable of PathMatchSets to be run on the file</param>
/// <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 matched protections, null or empty otherwise</returns>
#if NET20 || NET35
public static Queue<string> GetAllMatches(string file, IEnumerable<PathMatchSet>? matchers, bool any = false)
#else
public static ConcurrentQueue<string> GetAllMatches(string file, IEnumerable<PathMatchSet>? matchers, bool any = false)
#endif
{
return FindAllMatches(new List<string> { file }, matchers, any, false);
}
/// <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>
/// <summary>
/// Get all path matches for a given list of matchers
/// </summary>
/// <param name="files">File paths to check for matches</param>
/// <param name="matchers">Enumerable of PathMatchSets to be run on the file</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 matched protections, null or empty otherwise</returns>
#if NET20 || NET35
public static Queue<string> GetAllMatches(IEnumerable<string>? files, IEnumerable<PathMatchSet>? matchers, bool any = false)
#else
public static ConcurrentQueue<string> GetAllMatches(IEnumerable<string>? files, IEnumerable<PathMatchSet>? matchers, bool any = false)
#endif
/// <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)
{
return FindAllMatches(files, matchers, 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="file">File path to check for matches</param>
/// <param name="matchers">Enumerable of PathMatchSets to be run on the file</param>
/// <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 matched protection, null otherwise</returns>
public static string? GetFirstMatch(string file, IEnumerable<PathMatchSet> matchers, bool any = false)
/// <returns>String representing the match, null otherwise</returns>
public static string? GetFirstMatch(List<string> stack, List<PathMatchSet> matchSets, bool any = false)
{
var contentMatches = FindAllMatches(new List<string> { file }, matchers, any, true);
var contentMatches = FindAllMatches(stack, matchSets, any, true);
if (contentMatches == null || contentMatches.Count == 0)
return null;
#if NET20 || NET35
return contentMatches.Peek();
#else
return contentMatches.First();
#endif
}
/// <summary>
/// Get first path match for a given list of matchers
/// </summary>
/// <param name="files">File paths to check for matches</param>
/// <param name="matchers">Enumerable 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 matched protection, null otherwise</returns>
public static string? GetFirstMatch(IEnumerable<string> files, IEnumerable<PathMatchSet> matchers, bool any = false)
{
var contentMatches = FindAllMatches(files, matchers, any, true);
if (contentMatches == null || contentMatches.Count == 0)
return null;
#if NET20 || NET35
return contentMatches.Peek();
#else
return contentMatches.First();
#endif
return contentMatches[0];
}
/// <summary>
/// Get the required set of path matches on a per Matcher basis
/// </summary>
/// <param name="files">File paths to check for matches</param>
/// <param name="matchers">Enumerable of PathMatchSets to be run on the file</param>
/// <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 matched protections, null or empty otherwise</returns>
#if NET20 || NET35
private static Queue<string> FindAllMatches(IEnumerable<string>? files, IEnumerable<PathMatchSet>? matchers, bool any, bool stopAfterFirst)
#else
private static ConcurrentQueue<string> FindAllMatches(IEnumerable<string>? files, IEnumerable<PathMatchSet>? matchers, bool any, bool stopAfterFirst)
#endif
/// <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 there's no mappings, we can't match
#if NET20 || NET35
if (matchers == null || new List<PathMatchSet>(matchers).Count == 0)
#else
if (matchers == null || !matchers.Any())
#endif
return new();
// If either set is null or empty
if (stack == null || stack.Count == 0 || matchSets.Count == 0)
return [];
// Initialize the list of matched protections
#if NET20 || NET35
var matchedProtections = new Queue<string>();
#else
var matchedProtections = new ConcurrentQueue<string>();
#endif
// Initialize the list of matches
var matchesList = new List<string>();
// Loop through and try everything otherwise
foreach (var matcher in matchers)
foreach (var matcher in matchSets)
{
// Determine if the matcher passes
bool passes;
string? firstMatchedString;
List<string> matches = [];
if (any)
{
string? matchedString = matcher.MatchesAny(files);
passes = matchedString != null;
firstMatchedString = matchedString;
string? anyMatch = matcher.MatchesAny(stack);
if (anyMatch != null)
matches = [anyMatch];
}
else
{
List<string> matchedStrings = matcher.MatchesAll(files);
passes = matchedStrings.Count > 0;
firstMatchedString = passes ? matchedStrings[0] : null;
matches = matcher.MatchesAll(stack);
}
// If we don't have a pass, just continue
if (!passes || firstMatchedString == null)
if (matches.Count == 0)
continue;
// If we there is no version method, just return the protection name
if (matcher.GetVersion == null)
{
matchedProtections.Enqueue(matcher.ProtectionName ?? "Unknown Protection");
}
// Build the output string
var matchString = new StringBuilder();
matchString.Append(matcher.SetName);
// Otherwise, invoke the version method
else
// 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(firstMatchedString, files);
var version = matcher.GetVersion(matches[0], stack);
if (version == null)
continue;
matchedProtections.Enqueue($"{matcher.ProtectionName ?? "Unknown Protection"} {version}".Trim());
// Trim and add the version
version = version.Trim();
if (version.Length > 0)
matchString.Append($" {version}");
}
// If we're stopping after the first protection, bail out here
// Append the match to the list
matchesList.Add(matchString.ToString());
// If we're stopping after the first match, bail out here
if (stopAfterFirst)
return matchedProtections;
return matchesList;
}
return matchedProtections;
return matchesList;
}
#endregion
}
}
}

View File

@@ -11,6 +11,8 @@ namespace SabreTools.Matching.Paths
/// Constructor
/// </summary>
/// <param name="needle">String representing the search</param>
public FilePathMatch(string needle) : base($"{Path.DirectorySeparatorChar}{needle}", false, true) { }
/// <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

@@ -1,7 +1,5 @@
using System.Collections.Generic;
#if NET40_OR_GREATER || NETCOREAPP
using System.Linq;
#endif
using System.IO;
namespace SabreTools.Matching.Paths
{
@@ -13,63 +11,76 @@ namespace SabreTools.Matching.Paths
/// <summary>
/// String to match
/// </summary>
#if NETFRAMEWORK || NETCOREAPP
public string? Needle { get; private set; }
#else
public string? Needle { get; init; }
#endif
public string Needle { get; }
/// <summary>
/// Match exact casing instead of invariant
/// Match casing instead of invariant
/// </summary>
public bool MatchExact { get; private set; }
private readonly bool _matchCase;
/// <summary>
/// Match that values end with the needle and not just contains
/// </summary>
public bool UseEndsWith { get; private set; }
private readonly bool _useEndsWith;
/// <summary>
/// Constructor
/// </summary>
/// <param name="needle">String representing the search</param>
/// <param name="matchExact">True to match exact casing, false otherwise</param>
/// <param name="useEndsWith">True to match the end only, false for all contents</param>
public PathMatch(string? needle, bool matchExact = false, bool useEndsWith = false)
/// <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;
MatchExact = matchExact;
UseEndsWith = useEndsWith;
_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(IEnumerable<string>? stack)
public string? Match(List<string>? stack)
{
// If either array is null or empty, we can't do anything
#if NET20 || NET35
if (stack == null || new List<string>(stack).Count == 0 || Needle == null || Needle.Length == 0)
#else
if (stack == null || !stack.Any() || Needle == null || Needle.Length == 0)
#endif
// 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 = MatchExact ? Needle : Needle.ToLowerInvariant();
string procNeedle = _matchCase ? Needle : Needle.ToLowerInvariant();
foreach (string stackItem in stack)
{
// Preprocess the stack item, if necessary
string procStackItem = MatchExact ? stackItem : stackItem.ToLowerInvariant();
string procStackItem = _matchCase ? stackItem : stackItem.ToLowerInvariant();
if (UseEndsWith && procStackItem.EndsWith(procNeedle))
if (_useEndsWith && procStackItem.EndsWith(procNeedle))
return stackItem;
else if (!UseEndsWith && procStackItem.Contains(procNeedle))
else if (!_useEndsWith && procStackItem.Contains(procNeedle))
return stackItem;
}
@@ -78,4 +89,4 @@ namespace SabreTools.Matching.Paths
#endregion
}
}
}

View File

@@ -1,89 +1,107 @@
using System;
using System.Collections.Generic;
#if NET40_OR_GREATER || NETCOREAPP
using System.Linq;
#endif
using System.IO;
namespace SabreTools.Matching.Paths
{
/// <summary>
/// A set of path matches that work together
/// </summary>
public class PathMatchSet : MatchSet<PathMatch, string>
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 protection name, or `null`,
/// in which case it will cause the protection to be omitted.
/// 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 Func<string, IEnumerable<string>?, string?>? GetVersion { get; private set; }
public GetPathVersion? GetVersion { get; }
#region Constructors
#region Generic Constructors
public PathMatchSet(string needle, string protectionName)
: this(new List<string> { needle }, null, protectionName) { }
/// <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) { }
public PathMatchSet(List<string> needles, string protectionName)
: this(needles, null, protectionName) { }
public PathMatchSet(string needle, Func<string, IEnumerable<string>?, string?>? getVersion, string protectionName)
: this(new List<string> { needle }, getVersion, protectionName) { }
#if NET20 || NET35
public PathMatchSet(List<string> needles, Func<string, IEnumerable<string>?, string?>? getVersion, string protectionName)
/// <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)
{
var matchers = new List<PathMatch>();
foreach (var n in needles)
{
matchers.Add(new PathMatch(n));
}
// Validate the inputs
if (needles.Count == 0)
throw new InvalidDataException(nameof(needles));
Matchers = matchers;
GetVersion = getVersion;
ProtectionName = protectionName;
}
#else
public PathMatchSet(List<string> needles, Func<string, IEnumerable<string>?, string?>? getVersion, string protectionName)
: this(needles.Select(n => new PathMatch(n)).ToList(), getVersion, protectionName) { }
#endif
public PathMatchSet(PathMatch needle, string protectionName)
: this(new List<PathMatch>() { needle }, null, protectionName) { }
public PathMatchSet(List<PathMatch> needles, string protectionName)
: this(needles, null, protectionName) { }
public PathMatchSet(PathMatch needle, Func<string, IEnumerable<string>?, string?>? getVersion, string protectionName)
: this(new List<PathMatch>() { needle }, getVersion, protectionName) { }
public PathMatchSet(List<PathMatch> needles, Func<string, IEnumerable<string>?, string?>? getVersion, string protectionName)
{
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;
ProtectionName = protectionName;
}
#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(IEnumerable<string>? stack)
public List<string> MatchesAll(List<string>? stack)
{
// If no path matches are defined, we fail out
#if NET20 || NET35
if (Matchers == null || new List<PathMatch>(Matchers).Count == 0)
#else
if (Matchers == null || !Matchers.Any())
#endif
// 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
@@ -102,19 +120,23 @@ namespace SabreTools.Matching.Paths
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(IEnumerable<string>? stack)
public string? MatchesAny(List<string>? stack)
{
// If no path matches are defined, we fail out
#if NET20 || NET35
if (Matchers == null || new List<PathMatch>(Matchers).Count == 0)
#else
if (Matchers == null || !Matchers.Any())
#endif
// 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
@@ -130,4 +152,4 @@ namespace SabreTools.Matching.Paths
#endregion
}
}
}

View File

@@ -1,33 +1,34 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<!-- Assembly Properties -->
<TargetFrameworks>net20;net35;net40;net452;net462;net472;net48;netcoreapp3.1;net5.0;net6.0;net7.0;net8.0</TargetFrameworks>
<RuntimeIdentifiers>win-x86;win-x64;win-arm64;linux-x64;linux-arm64;osx-x64;osx-arm64</RuntimeIdentifiers>
<LangVersion>latest</LangVersion>
<Nullable>enable</Nullable>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
<Version>1.3.2</Version>
<!-- Package Properties -->
<Authors>Matt Nadareski</Authors>
<Description>Byte array and stream matching library</Description>
<Copyright>Copyright (c) Matt Nadareski 2018-2024</Copyright>
<PackageProjectUrl>https://github.com/SabreTools/</PackageProjectUrl>
<PackageReadmeFile>README.md</PackageReadmeFile>
<RepositoryUrl>https://github.com/SabreTools/SabreTools.Matching</RepositoryUrl>
<RepositoryType>git</RepositoryType>
<PackageTags>byte array stream match matching</PackageTags>
<PackageLicenseExpression>MIT</PackageLicenseExpression>
</PropertyGroup>
<ItemGroup>
<None Include="../README.md" Pack="true" PackagePath=""/>
</ItemGroup>
<PropertyGroup>
<!-- Assembly Properties -->
<TargetFrameworks>net20;net35;net40;net452;net462;net472;net48;netcoreapp3.1;net5.0;net6.0;net7.0;net8.0;net9.0;netstandard2.0;netstandard2.1</TargetFrameworks>
<IncludeSymbols>true</IncludeSymbols>
<LangVersion>latest</LangVersion>
<NoWarn>NU1903</NoWarn>
<Nullable>enable</Nullable>
<SymbolPackageFormat>snupkg</SymbolPackageFormat>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
<Version>1.6.0</Version>
<!-- Support for old .NET versions -->
<ItemGroup Condition="$(TargetFramework.StartsWith(`net2`))">
<PackageReference Include="Net30.LinqBridge" Version="1.3.0" />
</ItemGroup>
<!-- Package Properties -->
<Authors>Matt Nadareski</Authors>
<Description>Byte array and stream matching library</Description>
<Copyright>Copyright (c) Matt Nadareski 2018-2025</Copyright>
<PackageProjectUrl>https://github.com/SabreTools/</PackageProjectUrl>
<PackageReadmeFile>README.md</PackageReadmeFile>
<RepositoryUrl>https://github.com/SabreTools/SabreTools.Matching</RepositoryUrl>
<RepositoryType>git</RepositoryType>
<PackageTags>byte array stream match matching</PackageTags>
<PackageLicenseExpression>MIT</PackageLicenseExpression>
</PropertyGroup>
</Project>
<ItemGroup>
<InternalsVisibleTo Include="SabreTools.Matching.Test" />
</ItemGroup>
<ItemGroup>
<None Include="../README.md" Pack="true" PackagePath="" />
</ItemGroup>
</Project>

4
publish-nix.sh Normal file → Executable file
View File

@@ -1,14 +1,14 @@
#! /bin/bash
# This batch file assumes the following:
# - .NET 8.0 (or newer) SDK is installed and in PATH
# - .NET 9.0 (or newer) SDK is installed and in PATH
#
# If any of these are not satisfied, the operation may fail
# in an unpredictable way and result in an incomplete output.
# Optional parameters
NO_BUILD=false
while getopts "uba" OPTION
while getopts "b" OPTION
do
case $OPTION in
b)

View File

@@ -1,5 +1,5 @@
# This batch file assumes the following:
# - .NET 8.0 (or newer) SDK is installed and in PATH
# - .NET 9.0 (or newer) SDK is installed and in PATH
#
# If any of these are not satisfied, the operation may fail
# in an unpredictable way and result in an incomplete output.