48 Commits
1.4.1 ... 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
31 changed files with 2510 additions and 424 deletions

View File

@@ -1,4 +1,4 @@
name: Nuget Pack
name: Build and Test
on:
push:
@@ -16,31 +16,22 @@ jobs:
- name: Setup .NET
uses: actions/setup-dotnet@v4
with:
dotnet-version: 9.0.x
dotnet-version: |
6.0.x
8.0.x
9.0.x
- name: Restore dependencies
run: dotnet restore
- name: Build library
run: dotnet build
- 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,10 +11,13 @@ jobs:
- name: Setup .NET
uses: actions/setup-dotnet@v4
with:
dotnet-version: 9.0.x
dotnet-version: |
6.0.x
8.0.x
9.0.x
- name: Build
run: dotnet build
- name: Run tests
run: dotnet test
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)

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

@@ -7,6 +7,142 @@ 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()
{
@@ -18,11 +154,149 @@ namespace SabreTools.Matching.Test
{
new(check, expected),
};
string? actual = MatchUtil.GetFirstMatch("testfile", source, matchers);
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()
{
@@ -36,9 +310,15 @@ namespace SabreTools.Matching.Test
{
new(check, expected),
};
string? actual = MatchUtil.GetFirstMatch("testfile", stream, matchers);
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,22 +1,22 @@
<Project Sdk="Microsoft.NET.Sdk">
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFrameworks>net6.0;net8.0;net9.0</TargetFrameworks>
<TargetFrameworks>net8.0;net9.0</TargetFrameworks>
<IsPackable>false</IsPackable>
<LangVersion>latest</LangVersion>
<NoWarn>NU1903</NoWarn>
<Nullable>enable</Nullable>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
<WarningsNotAsErrors>NU1903</WarningsNotAsErrors>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="coverlet.collector" Version="6.0.0">
<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.8.0" />
<PackageReference Include="xunit" Version="2.6.2" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.5.4">
<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>

View File

@@ -66,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);
@@ -77,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

@@ -77,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,31 +11,74 @@ namespace SabreTools.Matching.Content
/// <summary>
/// Content to match
/// </summary>
public byte?[]? Needle { get; }
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)
{
Needle = needle;
Start = start;
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>
@@ -42,32 +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 || Needle == null || 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 (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 and stack are identically sized, short-circuit
if (Needle.Length == stack.Length)
return EqualAt(stack, 0) ? 0 : -1;
// If the needle is larger than the stack window, it can't be contained within
if (Needle.Length > stack.Length - _start)
return -1;
// If the needle and stack window are identically sized, short-circuit
if (Needle.Length == stack.Length - _start)
return EqualAt(stack, _start) ? _start : -1;
// Return based on the direction of search
return reverse ? MatchReverse(stack) : MatchForward(stack);
}
/// <summary>
/// Match within a stack starting from the smallest index
/// </summary>
/// <param name="stack">Array to search for the given content</param>
/// <returns>Found position on success, -1 otherwise</returns>
private int MatchForward(byte[] stack)
{
// Set the default start and end values
int start = Start;
int end = 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 - 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,10 +173,6 @@ 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 (Needle == null)
return false;
// If the index is invalid, we can't do anything
if (index < 0)
return false;
@@ -123,32 +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 || Needle == null || 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 (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 and stack are identically sized, short-circuit
if (Needle.Length == stack.Length)
return EqualAt(stack, 0) ? 0 : -1;
// If the needle is larger than the stack window, it can't be contained within
if (Needle.Length > stack.Length - _start)
return -1;
// If the needle and stack window are identically sized, short-circuit
if (Needle.Length == stack.Length - _start)
return EqualAt(stack, _start) ? _start : -1;
// Return based on the direction of search
return reverse ? MatchReverse(stack) : MatchForward(stack);
}
/// <summary>
/// Match within a stack starting from the smallest index
/// </summary>
/// <param name="stack">Stream to search for the given content</param>
/// <returns>Found position on success, -1 otherwise</returns>
private int MatchForward(Stream stack)
{
// Set the default start and end values
int start = Start;
int end = 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 - 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)
@@ -170,10 +290,6 @@ 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 (Needle == null)
return false;
// If the index is invalid, we can't do anything
if (index < 0)
return false;
@@ -213,4 +329,4 @@ namespace SabreTools.Matching.Content
#endregion
}
}
}

View File

@@ -6,8 +6,14 @@ 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?[]>
{
/// <inheritdoc/>
public List<ContentMatch> Matchers { get; }
/// <inheritdoc/>
public string SetName { get; }
/// <summary>
/// Function to get a content version
/// </summary>
@@ -34,56 +40,91 @@ namespace SabreTools.Matching.Content
#region Generic Constructors
public ContentMatchSet(byte?[] needle, string matchName)
: this([needle], getArrayVersion: null, matchName) { }
/// <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 matchName)
: this(needles, getArrayVersion: null, matchName) { }
/// <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 matchName)
: this([needle], getArrayVersion: null, matchName) { }
public ContentMatchSet(List<ContentMatch> needles, string matchName)
: this(needles, getArrayVersion: null, matchName) { }
Matchers = needles;
SetName = setName;
GetArrayVersion = null;
GetStreamVersion = null;
}
#endregion
#region Array Constructors
public ContentMatchSet(byte?[] needle, GetArrayVersion? getArrayVersion, string matchName)
: this([needle], getArrayVersion, matchName) { }
/// <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) { }
public ContentMatchSet(List<byte?[]> needles, GetArrayVersion? getArrayVersion, string matchName)
: this(needles.ConvertAll(n => new ContentMatch(n)), getArrayVersion, matchName) { }
public ContentMatchSet(ContentMatch needle, GetArrayVersion? getArrayVersion, string matchName)
: this([needle], getArrayVersion, matchName) { }
public ContentMatchSet(List<ContentMatch> needles, GetArrayVersion? getArrayVersion, string matchName)
/// <summary>
/// Constructor
/// </summary>
/// <param name="needles">List of ContentMatch objects representing the comparisons</param>
/// <param name="getVersion">Delegate for deriving a version on match of an array</param>
/// <param name="setName">Unique name for the set</param>
public ContentMatchSet(List<ContentMatch> needles, GetArrayVersion getVersion, string setName)
{
// Validate the inputs
if (needles.Count == 0)
throw new InvalidDataException(nameof(needles));
Matchers = needles;
GetArrayVersion = getArrayVersion;
MatchName = matchName;
SetName = setName;
GetArrayVersion = getVersion;
GetStreamVersion = null;
}
#endregion
#region Stream Constructors
public ContentMatchSet(byte?[] needle, GetStreamVersion? getStreamVersion, string matchName)
: this([needle], getStreamVersion, matchName) { }
/// <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) { }
public ContentMatchSet(List<byte?[]> needles, GetStreamVersion? getStreamVersion, string matchName)
: this(needles.ConvertAll(n => new ContentMatch(n)), getStreamVersion, matchName) { }
public ContentMatchSet(ContentMatch needle, GetStreamVersion? getStreamVersion, string matchName)
: this([needle], getStreamVersion, matchName) { }
public ContentMatchSet(List<ContentMatch> needles, GetStreamVersion? getStreamVersion, string matchName)
/// <summary>
/// Constructor
/// </summary>
/// <param name="needles">List of ContentMatch objects representing the comparisons</param>
/// <param name="getVersion">Delegate for deriving a version on match of a Stream</param>
/// <param name="setName">Unique name for the set</param>
public ContentMatchSet(List<ContentMatch> needles, GetStreamVersion getVersion, string setName)
{
// Validate the inputs
if (needles.Count == 0)
throw new InvalidDataException(nameof(needles));
Matchers = needles;
GetStreamVersion = getStreamVersion;
MatchName = matchName;
SetName = setName;
GetArrayVersion = null;
GetStreamVersion = getVersion;
}
#endregion
@@ -97,8 +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 (Matchers == null)
// If either set is null or empty
if (stack == null || stack.Length == 0 || Matchers.Count == 0)
return [];
// Initialize the position list
@@ -110,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;
@@ -124,8 +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 (Matchers == null)
// 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
@@ -150,8 +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 (Matchers == null)
// If either set is null or empty
if (stack == null || stack.Length == 0 || Matchers.Count == 0)
return [];
// Initialize the position list
@@ -163,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;
@@ -177,8 +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 (Matchers == null)
// 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
@@ -194,4 +235,4 @@ namespace SabreTools.Matching.Content
#endregion
}
}
}

View File

@@ -18,7 +18,7 @@ namespace SabreTools.Matching
/// <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, IEnumerable<string>? files);
public delegate string? GetPathVersion(string path, List<string>? files);
/// <summary>
/// Get a version number from a file
@@ -28,4 +28,4 @@ namespace SabreTools.Matching
/// <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

@@ -6,4 +6,4 @@ namespace System.Runtime.CompilerServices
internal sealed class ExtensionAttribute : Attribute {}
}
#endif
#endif

View File

@@ -7,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;
@@ -43,115 +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)
{
// Convert the needle to a nullable byte array
byte?[]? nullableNeedle = null;
if (needle != null)
nullableNeedle = Array.ConvertAll(needle, b => (byte?)b);
return FirstPosition(stack, 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)
{
// Convert the needle to a nullable byte array
byte?[]? nullableNeedle = null;
if (needle != null)
nullableNeedle = Array.ConvertAll(needle, b => (byte?)b);
return LastPosition(stack, nullableNeedle, out position, start, end);
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>
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)
{
// 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 FirstPosition(stack, 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 FirstPosition(stack, 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 FirstPosition(stack, 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 FirstPosition(stack, 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>
/// Unique name for the match set
/// </summary>
public string? MatchName { get; set; }
public string SetName { get; }
}
}
}

View File

@@ -1,5 +1,6 @@
using System.Collections.Generic;
using System.IO;
using System.Text;
using SabreTools.Matching.Content;
using SabreTools.Matching.Paths;
@@ -17,23 +18,33 @@ 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 matches, null or empty otherwise</returns>
public static List<string>? GetAllMatches(string file, byte[]? stack, IEnumerable<ContentMatchSet>? matchers, bool includeDebug = false)
=> FindAllMatches(file, stack, matchers, includeDebug, false);
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 match, null otherwise</returns>
public static string? GetFirstMatch(string file, byte[]? stack, IEnumerable<ContentMatchSet>? matchers, bool includeDebug = false)
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;
@@ -45,47 +56,65 @@ 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 matches, empty otherwise</returns>
private static List<string> FindAllMatches(string file, byte[]? stack, IEnumerable<ContentMatchSet>? matchers, bool includeDebug, bool stopAfterFirst)
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 (matchers == null)
// If either set is null or empty
if (stack == null || stack.Length == 0 || matchSets.Count == 0)
return [];
// Initialize the list of matches
var matchesList = new List<string>();
// Loop through and try everything otherwise
foreach (var matcher in 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
string positionsString = string.Join(", ", [.. positions.ConvertAll(p => p.ToString())]);
// Build the output string
var matchString = new StringBuilder();
matchString.Append(matcher.SetName);
// If we there is no version method, just return the match name
if (matcher.GetArrayVersion == null)
{
matchesList.Add((matcher.MatchName ?? "Unknown") + (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;
matchesList.Add($"{matcher.MatchName ?? "Unknown"} {version}".Trim() + (includeDebug ? $" (Index {positionsString})" : string.Empty));
// Trim and add the version
version = version.Trim();
if (version.Length > 0)
matchString.Append($" {version}");
}
// Append the positional data if required
if (includeDebug)
{
string positionsString = string.Join(", ", [.. positions.ConvertAll(p => p.ToString())]);
matchString.Append($" (Index {positionsString})");
}
// Append the match to the list
matchesList.Add(matchString.ToString());
// If we're stopping after the first match, bail out here
if (stopAfterFirst)
return matchesList;
@@ -103,23 +132,33 @@ 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 matches, null or empty otherwise</returns>
public static List<string>? GetAllMatches(string file, Stream? stack, IEnumerable<ContentMatchSet>? matchers, bool includeDebug = false)
=> FindAllMatches(file, stack, matchers, includeDebug, false);
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 match, null otherwise</returns>
public static string? GetFirstMatch(string file, Stream? stack, IEnumerable<ContentMatchSet>? matchers, bool includeDebug = false)
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;
@@ -131,47 +170,65 @@ 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 matches, empty otherwise</returns>
private static List<string> FindAllMatches(string file, Stream? stack, IEnumerable<ContentMatchSet>? matchers, bool includeDebug, bool stopAfterFirst)
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 (matchers == null)
// If either set is null or empty
if (stack == null || stack.Length == 0 || matchSets.Count == 0)
return [];
// Initialize the list of matches
var matchesList = new List<string>();
// Loop through and try everything otherwise
foreach (var matcher in 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
string positionsString = string.Join(", ", [.. positions.ConvertAll(p => p.ToString())]);
// Build the output string
var matchString = new StringBuilder();
matchString.Append(matcher.SetName);
// If we there is no version method, just return the match name
if (matcher.GetStreamVersion == null)
{
matchesList.Add((matcher.MatchName ?? "Unknown") + (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;
matchesList.Add($"{matcher.MatchName ?? "Unknown"} {version}".Trim() + (includeDebug ? $" (Index {positionsString})" : string.Empty));
// Trim and add the version
version = version.Trim();
if (version.Length > 0)
matchString.Append($" {version}");
}
// Append the positional data if required
if (includeDebug)
{
string positionsString = string.Join(", ", [.. positions.ConvertAll(p => p.ToString())]);
matchString.Append($" (Index {positionsString})");
}
// Append the match to the list
matchesList.Add(matchString.ToString());
// If we're stopping after the first match, bail out here
if (stopAfterFirst)
return matchesList;
@@ -187,33 +244,33 @@ 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 matches, null or empty otherwise</returns>
public static List<string> GetAllMatches(string file, IEnumerable<PathMatchSet>? matchers, bool any = false)
=> FindAllMatches([file], matchers, any, false);
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 matches, null or empty otherwise</returns>
public static List<string> GetAllMatches(IEnumerable<string>? files, IEnumerable<PathMatchSet>? matchers, bool any = false)
=> FindAllMatches(files, matchers, any, false);
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="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>String representing the match, null otherwise</returns>
public static string? GetFirstMatch(string file, IEnumerable<PathMatchSet> matchers, bool any = false)
public static string? GetFirstMatch(string stack, List<PathMatchSet> matchSets, bool any = false)
{
var contentMatches = FindAllMatches([file], matchers, any, true);
var contentMatches = FindAllMatches([stack], matchSets, any, true);
if (contentMatches == null || contentMatches.Count == 0)
return null;
@@ -223,13 +280,13 @@ namespace SabreTools.Matching
/// <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="stack">File paths to check for matches</param>
/// <param name="matchSets">List of PathMatchSets to be run on the file</param>
/// <param name="any">True if any path match is a success, false if all have to match</param>
/// <returns>String representing the match, null otherwise</returns>
public static string? GetFirstMatch(IEnumerable<string> files, IEnumerable<PathMatchSet> matchers, bool any = false)
public static string? GetFirstMatch(List<string> stack, List<PathMatchSet> matchSets, bool any = false)
{
var contentMatches = FindAllMatches(files, matchers, any, true);
var contentMatches = FindAllMatches(stack, matchSets, any, true);
if (contentMatches == null || contentMatches.Count == 0)
return null;
@@ -239,60 +296,61 @@ namespace SabreTools.Matching
/// <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 matches, null or empty otherwise</returns>
private static List<string> FindAllMatches(IEnumerable<string>? files, IEnumerable<PathMatchSet>? matchers, bool any, bool stopAfterFirst)
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 (matchers == null)
// If either set is null or empty
if (stack == null || stack.Count == 0 || matchSets.Count == 0)
return [];
// Initialize the list of matches
var matchesList = new List<string>();
// Loop through and try everything otherwise
foreach (var matcher in 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 match name
if (matcher.GetVersion == null)
{
matchesList.Add(matcher.MatchName ?? "Unknown");
}
// 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;
matchesList.Add($"{matcher.MatchName ?? "Unknown"} {version}".Trim());
// Trim and add the version
version = version.Trim();
if (version.Length > 0)
matchString.Append($" {version}");
}
// Append the match to the list
matchesList.Add(matchString.ToString());
// If we're stopping after the first match, bail out here
if (stopAfterFirst)
return matchesList;
@@ -303,4 +361,4 @@ namespace SabreTools.Matching
#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,4 +1,5 @@
using System.Collections.Generic;
using System.IO;
namespace SabreTools.Matching.Paths
{
@@ -10,55 +11,76 @@ namespace SabreTools.Matching.Paths
/// <summary>
/// String to match
/// </summary>
public string? Needle { get; }
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 (stack == null || Needle == null || Needle.Length == 0)
// 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;
}
@@ -67,4 +89,4 @@ namespace SabreTools.Matching.Paths
#endregion
}
}
}

View File

@@ -1,12 +1,19 @@
using System.Collections.Generic;
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>
@@ -18,49 +25,83 @@ namespace SabreTools.Matching.Paths
/// </remarks>
public GetPathVersion? GetVersion { get; }
#region Constructors
#region Generic Constructors
public PathMatchSet(string needle, string matchName)
: this([needle], null, matchName) { }
/// <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 matchName)
: this(needles, null, matchName) { }
public PathMatchSet(string needle, GetPathVersion? getVersion, string matchName)
: this([needle], getVersion, matchName) { }
public PathMatchSet(List<string> needles, GetPathVersion? getVersion, string matchName)
: this(needles.ConvertAll(n => new PathMatch(n)), getVersion, matchName) { }
public PathMatchSet(PathMatch needle, string matchName)
: this([needle], null, matchName) { }
public PathMatchSet(List<PathMatch> needles, string matchName)
: this(needles, null, matchName) { }
public PathMatchSet(PathMatch needle, GetPathVersion? getVersion, string matchName)
: this([needle], getVersion, matchName) { }
public PathMatchSet(List<PathMatch> needles, GetPathVersion? getVersion, string matchName)
/// <summary>
/// Constructor
/// </summary>
/// <param name="needles">List of PathMatch objects representing the comparisons</param>
/// <param name="setName">Unique name for the set</param>
public PathMatchSet(List<PathMatch> needles, string setName)
{
// Validate the inputs
if (needles.Count == 0)
throw new InvalidDataException(nameof(needles));
Matchers = needles;
SetName = setName;
GetVersion = null;
}
#endregion
#region Version Constructors
/// <summary>
/// Constructor
/// </summary>
/// <param name="needle">PathMatch representing the comparisons</param>
/// <param name="getVersion">Delegate for deriving a version on match</param>
/// <param name="setName">Unique name for the set</param>
public PathMatchSet(PathMatch needle, GetPathVersion getVersion, string setName)
: this([needle], getVersion, setName) { }
/// <summary>
/// Constructor
/// </summary>
/// <param name="needles">List of PathMatch objects representing the comparisons</param>
/// <param name="getVersion">Delegate for deriving a version on match</param>
/// <param name="setName">Unique name for the set</param>
public PathMatchSet(List<PathMatch> needles, GetPathVersion getVersion, string setName)
{
// Validate the inputs
if (needles.Count == 0)
throw new InvalidDataException(nameof(needles));
Matchers = needles;
SetName = setName;
GetVersion = getVersion;
MatchName = matchName;
}
#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 (Matchers == null)
// 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
@@ -79,15 +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 (Matchers == null)
// 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
@@ -103,4 +152,4 @@ namespace SabreTools.Matching.Paths
#endregion
}
}
}

View File

@@ -2,25 +2,31 @@
<PropertyGroup>
<!-- Assembly Properties -->
<TargetFrameworks>net20;net35;net40;net452;net462;net472;net48;netcoreapp3.1;net5.0;net6.0;net7.0;net8.0;net9.0</TargetFrameworks>
<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.4.1</Version>
<Version>1.6.0</Version>
<!-- Package Properties -->
<Authors>Matt Nadareski</Authors>
<Description>Byte array and stream matching library</Description>
<Copyright>Copyright (c) Matt Nadareski 2018-2024</Copyright>
<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>
<WarningsNotAsErrors>NU1903</WarningsNotAsErrors>
</PropertyGroup>
<ItemGroup>
<InternalsVisibleTo Include="SabreTools.Matching.Test" />
</ItemGroup>
<ItemGroup>
<None Include="../README.md" Pack="true" PackagePath="" />
</ItemGroup>

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

@@ -8,7 +8,7 @@
# Optional parameters
NO_BUILD=false
while getopts "uba" OPTION
while getopts "b" OPTION
do
case $OPTION in
b)