mirror of
https://github.com/SabreTools/SabreTools.Matching.git
synced 2026-02-05 13:49:48 +00:00
Compare commits
81 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
036ee4246b | ||
|
|
8e3bcb9015 | ||
|
|
8b1ac53ccf | ||
|
|
0c9e255d48 | ||
|
|
1227488572 | ||
|
|
bd878cf1a1 | ||
|
|
b436b64c3a | ||
|
|
6f6d071a79 | ||
|
|
69130a6e9f | ||
|
|
93847420a5 | ||
|
|
76e13a47ec | ||
|
|
4f56d716d4 | ||
|
|
cd7e89f869 | ||
|
|
0810b4b083 | ||
|
|
ba0b126714 | ||
|
|
48cdcf96bf | ||
|
|
39ca26bc28 | ||
|
|
5f2940388e | ||
|
|
bac0913b35 | ||
|
|
c52973418d | ||
|
|
c97fe92a3e | ||
|
|
176a892993 | ||
|
|
3bb6a6f5c1 | ||
|
|
efd26133aa | ||
|
|
b1d95652ea | ||
|
|
ea52117717 | ||
|
|
50a0c62560 | ||
|
|
d98629d22b | ||
|
|
2d767122d0 | ||
|
|
e4d9848756 | ||
|
|
c96185d550 | ||
|
|
c1648ccb71 | ||
|
|
31fff83114 | ||
|
|
607b9d30b0 | ||
|
|
cfb82f055c | ||
|
|
2057baeded | ||
|
|
bed920b97c | ||
|
|
cdf2dd6589 | ||
|
|
f10585aa32 | ||
|
|
2d4f974623 | ||
|
|
9490dabbd6 | ||
|
|
425f846e26 | ||
|
|
a180499090 | ||
|
|
c7633b1a53 | ||
|
|
e88771b11f | ||
|
|
e596c7c2c1 | ||
|
|
910e963d13 | ||
|
|
751519fadb | ||
|
|
f86f565136 | ||
|
|
1ad45c6d59 | ||
|
|
38796776ee | ||
|
|
c60f587b69 | ||
|
|
8844ba0ae3 | ||
|
|
cdd41e8bec | ||
|
|
655214503f | ||
|
|
5801424db7 | ||
|
|
afb9ab3e4e | ||
|
|
85006a71bf | ||
|
|
90bb82306b | ||
|
|
c38e10e55b | ||
|
|
e96790d875 | ||
|
|
bf23967f74 | ||
|
|
8debf24a44 | ||
|
|
333b5be76c | ||
|
|
b0fdff2d6e | ||
|
|
3b0e74b9f4 | ||
|
|
883dd00e8f | ||
|
|
aa8506e0ec | ||
|
|
e25fb02f03 | ||
|
|
aa16a83426 | ||
|
|
cbf6523a8f | ||
|
|
e947687f42 | ||
|
|
b4510f00e1 | ||
|
|
058923a39d | ||
|
|
113e8c9151 | ||
|
|
cb3a6f8992 | ||
|
|
5a3de8e124 | ||
|
|
08ef69e7c1 | ||
|
|
19971ec62c | ||
|
|
4650399bc1 | ||
|
|
aad97d03fd |
@@ -1,4 +1,4 @@
|
||||
name: Nuget Pack
|
||||
name: Build and Test
|
||||
|
||||
on:
|
||||
push:
|
||||
@@ -16,25 +16,22 @@ jobs:
|
||||
- name: Setup .NET
|
||||
uses: actions/setup-dotnet@v4
|
||||
with:
|
||||
dotnet-version: 8.0.x
|
||||
dotnet-version: |
|
||||
6.0.x
|
||||
8.0.x
|
||||
9.0.x
|
||||
|
||||
- name: Restore dependencies
|
||||
run: dotnet restore
|
||||
- name: Run tests
|
||||
run: dotnet test
|
||||
|
||||
- name: Pack
|
||||
run: dotnet pack
|
||||
|
||||
- name: Upload build
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: 'Nuget Package'
|
||||
path: '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: 'bin/Release/*.nupkg'
|
||||
artifacts: "*.nupkg,*.snupkg"
|
||||
body: 'Last built commit: ${{ github.sha }}'
|
||||
name: 'Rolling Release'
|
||||
prerelease: True
|
||||
10
.github/workflows/check_pr.yml
vendored
10
.github/workflows/check_pr.yml
vendored
@@ -11,7 +11,13 @@ jobs:
|
||||
- name: Setup .NET
|
||||
uses: actions/setup-dotnet@v4
|
||||
with:
|
||||
dotnet-version: 8.0.x
|
||||
dotnet-version: |
|
||||
6.0.x
|
||||
8.0.x
|
||||
9.0.x
|
||||
|
||||
- name: Build
|
||||
run: dotnet build
|
||||
run: dotnet build
|
||||
|
||||
- name: Run tests
|
||||
run: dotnet test
|
||||
|
||||
212
ContentMatch.cs
212
ContentMatch.cs
@@ -1,212 +0,0 @@
|
||||
using System.IO;
|
||||
|
||||
namespace SabreTools.Matching
|
||||
{
|
||||
/// <summary>
|
||||
/// Content matching criteria
|
||||
/// </summary>
|
||||
public class ContentMatch : IMatch<byte?[]>
|
||||
{
|
||||
/// <summary>
|
||||
/// Content to match
|
||||
/// </summary>
|
||||
#if NETFRAMEWORK || NETCOREAPP
|
||||
public byte?[]? Needle { get; private set; }
|
||||
#else
|
||||
public byte?[]? Needle { get; init; }
|
||||
#endif
|
||||
|
||||
/// <summary>
|
||||
/// Starting index for matching
|
||||
/// </summary>
|
||||
public int Start { get; internal set; }
|
||||
|
||||
/// <summary>
|
||||
/// Ending index for matching
|
||||
/// </summary>
|
||||
public int End { get; private set; }
|
||||
|
||||
/// <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)
|
||||
{
|
||||
this.Needle = needle;
|
||||
this.Start = start;
|
||||
this.End = end;
|
||||
}
|
||||
|
||||
#region Array Matching
|
||||
|
||||
/// <summary>
|
||||
/// Get if this match can be found in a stack
|
||||
/// </summary>
|
||||
/// <param name="stack">Array to search for the given content</param>
|
||||
/// <param name="reverse">True to search from the end of the array, false from the start</param>
|
||||
/// <returns>Tuple of success and found position</returns>
|
||||
public (bool success, int position) Match(byte[]? stack, bool reverse = false)
|
||||
{
|
||||
// If either array is null or empty, we can't do anything
|
||||
if (stack == null || stack.Length == 0 || this.Needle == null || this.Needle.Length == 0)
|
||||
return (false, -1);
|
||||
|
||||
// If the needle array is larger than the stack array, it can't be contained within
|
||||
if (this.Needle.Length > stack.Length)
|
||||
return (false, -1);
|
||||
|
||||
// Set the default start and end values
|
||||
int start = this.Start;
|
||||
int end = this.End;
|
||||
|
||||
// If start or end are not set properly, set them to defaults
|
||||
if (start < 0)
|
||||
start = 0;
|
||||
if (end < 0)
|
||||
end = stack.Length - this.Needle.Length;
|
||||
|
||||
for (int i = reverse ? end : start; reverse ? i > start : i < end; i += reverse ? -1 : 1)
|
||||
{
|
||||
// If we somehow have an invalid end and we haven't matched, return
|
||||
if (i > stack.Length)
|
||||
return (false, -1);
|
||||
|
||||
// Check to see if the values are equal
|
||||
if (EqualAt(stack, i))
|
||||
return (true, i);
|
||||
}
|
||||
|
||||
return (false, -1);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get if a stack at a certain index is equal to a needle
|
||||
/// </summary>
|
||||
/// <param name="stack">Array to search for the given content</param>
|
||||
/// <param name="index">Starting index to check equality</param>
|
||||
/// <returns>True if the needle matches the stack at a given index</returns>
|
||||
private bool EqualAt(byte[] stack, int index)
|
||||
{
|
||||
// If the needle is invalid, we can't do anything
|
||||
if (this.Needle == null)
|
||||
return false;
|
||||
|
||||
// If the index is invalid, we can't do anything
|
||||
if (index < 0)
|
||||
return false;
|
||||
|
||||
// If we're too close to the end of the stack, return false
|
||||
if (this.Needle.Length > stack.Length - index)
|
||||
return false;
|
||||
|
||||
// Loop through and check the value
|
||||
for (int i = 0; i < this.Needle.Length; i++)
|
||||
{
|
||||
// A null value is a wildcard
|
||||
if (this.Needle[i] == null)
|
||||
continue;
|
||||
else if (stack[i + index] != this.Needle[i])
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Stream Matching
|
||||
|
||||
/// <summary>
|
||||
/// Get if this match can be found in a stack
|
||||
/// </summary>
|
||||
/// <param name="stack">Stream to search for the given content</param>
|
||||
/// <param name="reverse">True to search from the end of the array, false from the start</param>
|
||||
/// <returns>Tuple of success and found position</returns>
|
||||
public (bool success, int position) Match(Stream? stack, bool reverse = false)
|
||||
{
|
||||
// If either array is null or empty, we can't do anything
|
||||
if (stack == null || stack.Length == 0 || this.Needle == null || this.Needle.Length == 0)
|
||||
return (false, -1);
|
||||
|
||||
// If the needle array is larger than the stack array, it can't be contained within
|
||||
if (this.Needle.Length > stack.Length)
|
||||
return (false, -1);
|
||||
|
||||
// Set the default start and end values
|
||||
int start = this.Start;
|
||||
int end = this.End;
|
||||
|
||||
// If start or end are not set properly, set them to defaults
|
||||
if (start < 0)
|
||||
start = 0;
|
||||
if (end < 0)
|
||||
end = (int)(stack.Length - this.Needle.Length);
|
||||
|
||||
for (int i = reverse ? end : start; reverse ? i > start : i < end; i += reverse ? -1 : 1)
|
||||
{
|
||||
// If we somehow have an invalid end and we haven't matched, return
|
||||
if (i > stack.Length)
|
||||
return (false, -1);
|
||||
|
||||
// Check to see if the values are equal
|
||||
if (EqualAt(stack, i))
|
||||
return (true, i);
|
||||
}
|
||||
|
||||
return (false, -1);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get if a stack at a certain index is equal to a needle
|
||||
/// </summary>
|
||||
/// <param name="stack">Stream to search for the given content</param>
|
||||
/// <param name="index">Starting index to check equality</param>
|
||||
/// <returns>True if the needle matches the stack at a given index</returns>
|
||||
private bool EqualAt(Stream stack, int index)
|
||||
{
|
||||
// If the needle is invalid, we can't do anything
|
||||
if (this.Needle == null)
|
||||
return false;
|
||||
|
||||
// If the index is invalid, we can't do anything
|
||||
if (index < 0)
|
||||
return false;
|
||||
|
||||
// If we're too close to the end of the stack, return false
|
||||
if (this.Needle.Length > stack.Length - index)
|
||||
return false;
|
||||
|
||||
// Save the current position and move to the index
|
||||
long currentPosition = stack.Position;
|
||||
stack.Seek(index, SeekOrigin.Begin);
|
||||
|
||||
// Set the return value
|
||||
bool matched = true;
|
||||
|
||||
// Loop through and check the value
|
||||
for (int i = 0; i < this.Needle.Length; i++)
|
||||
{
|
||||
byte stackValue = (byte)stack.ReadByte();
|
||||
|
||||
// A null value is a wildcard
|
||||
if (this.Needle[i] == null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
else if (stackValue != this.Needle[i])
|
||||
{
|
||||
matched = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Reset the position and return the value
|
||||
stack.Seek(currentPosition, SeekOrigin.Begin);
|
||||
return matched;
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
@@ -1,199 +0,0 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
|
||||
namespace SabreTools.Matching
|
||||
{
|
||||
/// <summary>
|
||||
/// A set of content matches that work together
|
||||
/// </summary>
|
||||
public class ContentMatchSet : MatchSet<ContentMatch, byte?[]>
|
||||
{
|
||||
/// <summary>
|
||||
/// Function to get a content version
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// A content version method takes the file path, the file contents,
|
||||
/// and a list of found positions and returns a single string. That
|
||||
/// string is either a version string, in which case it will be appended
|
||||
/// to the protection name, or `null`, in which case it will cause
|
||||
/// the protection to be omitted.
|
||||
/// </remarks>
|
||||
public Func<string, byte[]?, List<int>, string?>? GetArrayVersion { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Function to get a content version
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// A content version method takes the file path, the file contents,
|
||||
/// and a list of found positions and returns a single string. That
|
||||
/// string is either a version string, in which case it will be appended
|
||||
/// to the protection name, or `null`, in which case it will cause
|
||||
/// the protection to be omitted.
|
||||
/// </remarks>
|
||||
public Func<string, Stream?, List<int>, string?>? GetStreamVersion { get; private set; }
|
||||
|
||||
#region Generic Constructors
|
||||
|
||||
public ContentMatchSet(byte?[] needle, string protectionName)
|
||||
: this(new List<byte?[]> { needle }, getArrayVersion: null, protectionName) { }
|
||||
|
||||
public ContentMatchSet(List<byte?[]> needles, string protectionName)
|
||||
: this(needles, getArrayVersion: null, protectionName) { }
|
||||
|
||||
public ContentMatchSet(ContentMatch needle, string protectionName)
|
||||
: this(new List<ContentMatch>() { needle }, getArrayVersion: null, protectionName) { }
|
||||
|
||||
public ContentMatchSet(List<ContentMatch> needles, string protectionName)
|
||||
: this(needles, getArrayVersion: null, protectionName) { }
|
||||
|
||||
#endregion
|
||||
|
||||
#region Array Constructors
|
||||
|
||||
public ContentMatchSet(byte?[] needle, Func<string, byte[]?, List<int>, string?>? getArrayVersion, string protectionName)
|
||||
: this(new List<byte?[]> { needle }, getArrayVersion, protectionName) { }
|
||||
|
||||
public ContentMatchSet(List<byte?[]> needles, Func<string, byte[]?, List<int>, string?>? getArrayVersion, string protectionName)
|
||||
: this(needles.Select(n => new ContentMatch(n)).ToList(), getArrayVersion, protectionName) { }
|
||||
|
||||
public ContentMatchSet(ContentMatch needle, Func<string, byte[]?, List<int>, string?>? getArrayVersion, string protectionName)
|
||||
: this(new List<ContentMatch>() { needle }, getArrayVersion, protectionName) { }
|
||||
|
||||
public ContentMatchSet(List<ContentMatch> needles, Func<string, byte[]?, List<int>, string?>? getArrayVersion, string protectionName)
|
||||
{
|
||||
Matchers = needles;
|
||||
GetArrayVersion = getArrayVersion;
|
||||
ProtectionName = protectionName;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Stream Constructors
|
||||
|
||||
public ContentMatchSet(byte?[] needle, Func<string, Stream?, List<int>, string?>? getStreamVersion, string protectionName)
|
||||
: this(new List<byte?[]> { needle }, getStreamVersion, protectionName) { }
|
||||
|
||||
public ContentMatchSet(List<byte?[]> needles, Func<string, Stream?, List<int>, string?>? getStreamVersion, string protectionName)
|
||||
: this(needles.Select(n => new ContentMatch(n)).ToList(), getStreamVersion, protectionName) { }
|
||||
|
||||
public ContentMatchSet(ContentMatch needle, Func<string, Stream?, List<int>, string?>? getStreamVersion, string protectionName)
|
||||
: this(new List<ContentMatch>() { needle }, getStreamVersion, protectionName) { }
|
||||
|
||||
public ContentMatchSet(List<ContentMatch> needles, Func<string, Stream?, List<int>, string?>? getStreamVersion, string protectionName)
|
||||
{
|
||||
Matchers = needles;
|
||||
GetStreamVersion = getStreamVersion;
|
||||
ProtectionName = protectionName;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Array Matching
|
||||
|
||||
/// <summary>
|
||||
/// Determine whether all content matches pass
|
||||
/// </summary>
|
||||
/// <param name="stack">Array to search</param>
|
||||
/// <returns>Tuple of passing status and matching positions</returns>
|
||||
public (bool, List<int>) MatchesAll(byte[]? stack)
|
||||
{
|
||||
// If no content matches are defined, we fail out
|
||||
if (Matchers == null || !Matchers.Any())
|
||||
return (false, new List<int>());
|
||||
|
||||
// Initialize the position list
|
||||
var positions = new List<int>();
|
||||
|
||||
// Loop through all content matches and make sure all pass
|
||||
foreach (var contentMatch in Matchers)
|
||||
{
|
||||
(bool match, int position) = contentMatch.Match(stack);
|
||||
if (!match)
|
||||
return (false, new List<int>());
|
||||
else
|
||||
positions.Add(position);
|
||||
}
|
||||
|
||||
return (true, positions);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Determine whether any content matches pass
|
||||
/// </summary>
|
||||
/// <param name="stack">Array to search</param>
|
||||
/// <returns>Tuple of passing status and first matching position</returns>
|
||||
public (bool, int) MatchesAny(byte[]? stack)
|
||||
{
|
||||
// If no content matches are defined, we fail out
|
||||
if (Matchers == null || !Matchers.Any())
|
||||
return (false, -1);
|
||||
|
||||
// Loop through all content matches and make sure all pass
|
||||
foreach (var contentMatch in Matchers)
|
||||
{
|
||||
(bool match, int position) = contentMatch.Match(stack);
|
||||
if (match)
|
||||
return (true, position);
|
||||
}
|
||||
|
||||
return (false, -1);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Stream Matching
|
||||
|
||||
/// <summary>
|
||||
/// Determine whether all content matches pass
|
||||
/// </summary>
|
||||
/// <param name="stack">Stream to search</param>
|
||||
/// <returns>Tuple of passing status and matching positions</returns>
|
||||
public (bool, List<int>) MatchesAll(Stream? stack)
|
||||
{
|
||||
// If no content matches are defined, we fail out
|
||||
if (Matchers == null || !Matchers.Any())
|
||||
return (false, new List<int>());
|
||||
|
||||
// Initialize the position list
|
||||
var positions = new List<int>();
|
||||
|
||||
// Loop through all content matches and make sure all pass
|
||||
foreach (var contentMatch in Matchers)
|
||||
{
|
||||
(bool match, int position) = contentMatch.Match(stack);
|
||||
if (!match)
|
||||
return (false, new List<int>());
|
||||
else
|
||||
positions.Add(position);
|
||||
}
|
||||
|
||||
return (true, positions);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Determine whether any content matches pass
|
||||
/// </summary>
|
||||
/// <param name="stack">Stream to search</param>
|
||||
/// <returns>Tuple of passing status and first matching position</returns>
|
||||
public (bool, int) MatchesAny(Stream? stack)
|
||||
{
|
||||
// If no content matches are defined, we fail out
|
||||
if (Matchers == null || !Matchers.Any())
|
||||
return (false, -1);
|
||||
|
||||
// Loop through all content matches and make sure all pass
|
||||
foreach (var contentMatch in Matchers)
|
||||
{
|
||||
(bool match, int position) = contentMatch.Match(stack);
|
||||
if (match)
|
||||
return (true, position);
|
||||
}
|
||||
|
||||
return (false, -1);
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
108
Extensions.cs
108
Extensions.cs
@@ -1,108 +0,0 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
|
||||
namespace SabreTools.Matching
|
||||
{
|
||||
public static class Extensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Find all positions of one array in another, if possible, if possible
|
||||
/// </summary>
|
||||
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
|
||||
bool found = true;
|
||||
int lastPosition = start;
|
||||
var matcher = new ContentMatch(needle, end: end);
|
||||
|
||||
// Loop over and get all positions
|
||||
while (found)
|
||||
{
|
||||
matcher.Start = lastPosition;
|
||||
(found, lastPosition) = matcher.Match(stack, false);
|
||||
if (found)
|
||||
positions.Add(lastPosition);
|
||||
}
|
||||
|
||||
return positions;
|
||||
}
|
||||
|
||||
/// <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)
|
||||
{
|
||||
byte?[]? nullableNeedle = needle?.Select(b => (byte?)b).ToArray();
|
||||
return stack.FirstPosition(nullableNeedle, out position, 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)
|
||||
{
|
||||
var matcher = new ContentMatch(needle, start, end);
|
||||
(bool found, int foundPosition) = matcher.Match(stack, false);
|
||||
position = foundPosition;
|
||||
return found;
|
||||
}
|
||||
|
||||
/// <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)
|
||||
{
|
||||
var matcher = new ContentMatch(needle, start, end);
|
||||
(bool found, int foundPosition) = matcher.Match(stack, true);
|
||||
position = foundPosition;
|
||||
return found;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// See if a byte array starts with another
|
||||
/// </summary>
|
||||
public static bool StartsWith(this byte[] stack, byte[]? needle)
|
||||
{
|
||||
if (needle == null)
|
||||
return false;
|
||||
|
||||
return stack.FirstPosition(needle, out int _, start: 0, end: 1);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// See if a byte array starts with another
|
||||
/// </summary>
|
||||
public static bool StartsWith(this byte[] stack, byte?[]? needle)
|
||||
{
|
||||
if (needle == null)
|
||||
return false;
|
||||
|
||||
return stack.FirstPosition(needle, out int _, start: 0, end: 1);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// See if a byte array ends with another
|
||||
/// </summary>
|
||||
public static bool EndsWith(this byte[] stack, byte[]? needle)
|
||||
{
|
||||
if (needle == null)
|
||||
return false;
|
||||
|
||||
return stack.FirstPosition(needle, out int _, start: stack.Length - needle.Length);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// See if a byte array ends with another
|
||||
/// </summary>
|
||||
public static bool EndsWith(this byte[] stack, byte?[]? needle)
|
||||
{
|
||||
if (needle == null)
|
||||
return false;
|
||||
|
||||
return stack.FirstPosition(needle, out int _, start: stack.Length - needle.Length);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,7 +0,0 @@
|
||||
namespace SabreTools.Matching
|
||||
{
|
||||
public interface IMatch<T>
|
||||
{
|
||||
T? Needle { get; }
|
||||
}
|
||||
}
|
||||
374
MatchUtil.cs
374
MatchUtil.cs
@@ -1,374 +0,0 @@
|
||||
#if NET40_OR_GREATER || NETCOREAPP
|
||||
using System.Collections.Concurrent;
|
||||
#endif
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
|
||||
namespace SabreTools.Matching
|
||||
{
|
||||
/// <summary>
|
||||
/// Helper class for matching
|
||||
/// </summary>
|
||||
public static class MatchUtil
|
||||
{
|
||||
#region Array Content Matching
|
||||
|
||||
/// <summary>
|
||||
/// Get all content matches for a given list of matchers
|
||||
/// </summary>
|
||||
/// <param name="file">File to check for matches</param>
|
||||
/// <param name="stack">Array to search</param>
|
||||
/// <param name="matchers">Enumerable of ContentMatchSets to be run on the file</param>
|
||||
/// <param name="includeDebug">True to include positional data, false otherwise</param>
|
||||
/// <returns>List of strings representing the matched protections, null or empty otherwise</returns>
|
||||
#if NET20 || NET35
|
||||
public static Queue<string>? GetAllMatches(string file, byte[]? stack, IEnumerable<ContentMatchSet>? matchers, bool includeDebug = false)
|
||||
#else
|
||||
public static ConcurrentQueue<string>? GetAllMatches(string file, byte[]? stack, IEnumerable<ContentMatchSet>? matchers, bool includeDebug = false)
|
||||
#endif
|
||||
{
|
||||
return FindAllMatches(file, stack, matchers, includeDebug, false);
|
||||
}
|
||||
|
||||
/// <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="includeDebug">True to include positional data, false otherwise</param>
|
||||
/// <returns>String representing the matched protection, null otherwise</returns>
|
||||
public static string? GetFirstMatch(string file, byte[]? stack, IEnumerable<ContentMatchSet>? matchers, bool includeDebug = false)
|
||||
{
|
||||
var contentMatches = FindAllMatches(file, stack, matchers, includeDebug, true);
|
||||
if (contentMatches == null || !contentMatches.Any())
|
||||
return null;
|
||||
|
||||
return contentMatches.First();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get the required set of content matches on a per Matcher basis
|
||||
/// </summary>
|
||||
/// <param name="file">File to check for matches</param>
|
||||
/// <param name="stack">Array to search</param>
|
||||
/// <param name="matchers">Enumerable of ContentMatchSets to be run on the file</param>
|
||||
/// <param name="includeDebug">True to include positional data, false otherwise</param>
|
||||
/// <param name="stopAfterFirst">True to stop after the first match, false otherwise</param>
|
||||
/// <returns>List of strings representing the matched protections, null or empty otherwise</returns>
|
||||
#if NET20 || NET35
|
||||
private static Queue<string>? FindAllMatches(string file, byte[]? stack, IEnumerable<ContentMatchSet>? matchers, bool includeDebug, bool stopAfterFirst)
|
||||
#else
|
||||
private static ConcurrentQueue<string>? FindAllMatches(string file, byte[]? stack, IEnumerable<ContentMatchSet>? matchers, bool includeDebug, bool stopAfterFirst)
|
||||
#endif
|
||||
{
|
||||
// If there's no mappings, we can't match
|
||||
if (matchers == null || !matchers.Any())
|
||||
return null;
|
||||
|
||||
// Initialize the queue of matched protections
|
||||
#if NET20 || NET35
|
||||
var matchedProtections = new Queue<string>();
|
||||
#else
|
||||
var matchedProtections = new ConcurrentQueue<string>();
|
||||
#endif
|
||||
|
||||
// Loop through and try everything otherwise
|
||||
foreach (var matcher in matchers)
|
||||
{
|
||||
// Determine if the matcher passes
|
||||
(bool passes, List<int> positions) = matcher.MatchesAll(stack);
|
||||
if (!passes)
|
||||
continue;
|
||||
|
||||
// Format the list of all positions found
|
||||
#if NET20 || NET35
|
||||
var positionStrs = new List<string>();
|
||||
foreach (int pos in positions)
|
||||
{
|
||||
positionStrs.Add(pos.ToString());
|
||||
}
|
||||
string positionsString = string.Join(", ", [.. positionStrs]);
|
||||
#else
|
||||
string positionsString = string.Join(", ", positions);
|
||||
#endif
|
||||
|
||||
// If we there is no version method, just return the protection name
|
||||
if (matcher.GetArrayVersion == null)
|
||||
{
|
||||
matchedProtections.Enqueue((matcher.ProtectionName ?? "Unknown Protection") + (includeDebug ? $" (Index {positionsString})" : string.Empty));
|
||||
}
|
||||
|
||||
// Otherwise, invoke the version method
|
||||
else
|
||||
{
|
||||
// A null version returned means the check didn't pass at the version step
|
||||
var version = matcher.GetArrayVersion(file, stack, positions);
|
||||
if (version == null)
|
||||
continue;
|
||||
|
||||
matchedProtections.Enqueue($"{matcher.ProtectionName ?? "Unknown Protection"} {version}".Trim() + (includeDebug ? $" (Index {positionsString})" : string.Empty));
|
||||
}
|
||||
|
||||
// If we're stopping after the first protection, bail out here
|
||||
if (stopAfterFirst)
|
||||
return matchedProtections;
|
||||
}
|
||||
|
||||
return matchedProtections;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Stream Content Matching
|
||||
|
||||
/// <summary>
|
||||
/// Get all content matches for a given list of matchers
|
||||
/// </summary>
|
||||
/// <param name="file">File to check for matches</param>
|
||||
/// <param name="stack">Stream to search</param>
|
||||
/// <param name="matchers">Enumerable of ContentMatchSets to be run on the file</param>
|
||||
/// <param name="includeDebug">True to include positional data, false otherwise</param>
|
||||
/// <returns>List of strings representing the matched protections, null or empty otherwise</returns>
|
||||
#if NET20 || NET35
|
||||
public static Queue<string>? GetAllMatches(string file, Stream? stack, IEnumerable<ContentMatchSet>? matchers, bool includeDebug = false)
|
||||
#else
|
||||
public static ConcurrentQueue<string>? GetAllMatches(string file, Stream? stack, IEnumerable<ContentMatchSet>? matchers, bool includeDebug = false)
|
||||
#endif
|
||||
{
|
||||
return FindAllMatches(file, stack, matchers, includeDebug, false);
|
||||
}
|
||||
|
||||
/// <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="includeDebug">True to include positional data, false otherwise</param>
|
||||
/// <returns>String representing the matched protection, null otherwise</returns>
|
||||
public static string? GetFirstMatch(string file, Stream? stack, IEnumerable<ContentMatchSet>? matchers, bool includeDebug = false)
|
||||
{
|
||||
var contentMatches = FindAllMatches(file, stack, matchers, includeDebug, true);
|
||||
if (contentMatches == null || !contentMatches.Any())
|
||||
return null;
|
||||
|
||||
return contentMatches.First();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get the required set of content matches on a per Matcher basis
|
||||
/// </summary>
|
||||
/// <param name="file">File to check for matches</param>
|
||||
/// <param name="stack">Stream to search</param>
|
||||
/// <param name="matchers">Enumerable of ContentMatchSets to be run on the file</param>
|
||||
/// <param name="includeDebug">True to include positional data, false otherwise</param>
|
||||
/// <param name="stopAfterFirst">True to stop after the first match, false otherwise</param>
|
||||
/// <returns>List of strings representing the matched protections, null or empty otherwise</returns>
|
||||
#if NET20 || NET35
|
||||
private static Queue<string>? FindAllMatches(string file, Stream? stack, IEnumerable<ContentMatchSet>? matchers, bool includeDebug, bool stopAfterFirst)
|
||||
#else
|
||||
private static ConcurrentQueue<string>? FindAllMatches(string file, Stream? stack, IEnumerable<ContentMatchSet>? matchers, bool includeDebug, bool stopAfterFirst)
|
||||
#endif
|
||||
{
|
||||
// If there's no mappings, we can't match
|
||||
if (matchers == null || !matchers.Any())
|
||||
return null;
|
||||
|
||||
// Initialize the queue of matched protections
|
||||
#if NET20 || NET35
|
||||
var matchedProtections = new Queue<string>();
|
||||
#else
|
||||
var matchedProtections = new ConcurrentQueue<string>();
|
||||
#endif
|
||||
|
||||
// Loop through and try everything otherwise
|
||||
foreach (var matcher in matchers)
|
||||
{
|
||||
// Determine if the matcher passes
|
||||
(bool passes, List<int> positions) = matcher.MatchesAll(stack);
|
||||
if (!passes)
|
||||
continue;
|
||||
|
||||
// Format the list of all positions found
|
||||
#if NET20 || NET35
|
||||
var positionStrs = new List<string>();
|
||||
foreach (int pos in positions)
|
||||
{
|
||||
positionStrs.Add(pos.ToString());
|
||||
}
|
||||
string positionsString = string.Join(", ", [.. positionStrs]);
|
||||
#else
|
||||
string positionsString = string.Join(", ", positions);
|
||||
#endif
|
||||
|
||||
// If we there is no version method, just return the protection name
|
||||
if (matcher.GetStreamVersion == null)
|
||||
{
|
||||
matchedProtections.Enqueue((matcher.ProtectionName ?? "Unknown Protection") + (includeDebug ? $" (Index {positionsString})" : string.Empty));
|
||||
}
|
||||
|
||||
// Otherwise, invoke the version method
|
||||
else
|
||||
{
|
||||
// A null version returned means the check didn't pass at the version step
|
||||
var version = matcher.GetStreamVersion(file, stack, positions);
|
||||
if (version == null)
|
||||
continue;
|
||||
|
||||
matchedProtections.Enqueue($"{matcher.ProtectionName ?? "Unknown Protection"} {version}".Trim() + (includeDebug ? $" (Index {positionsString})" : string.Empty));
|
||||
}
|
||||
|
||||
// If we're stopping after the first protection, bail out here
|
||||
if (stopAfterFirst)
|
||||
return matchedProtections;
|
||||
}
|
||||
|
||||
return matchedProtections;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Path 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="any">True if any path match is a success, false if all have to match</param>
|
||||
/// <returns>List of strings representing the matched protections, null or empty otherwise</returns>
|
||||
#if NET20 || NET35
|
||||
public static Queue<string> GetAllMatches(string file, IEnumerable<PathMatchSet>? matchers, bool any = false)
|
||||
#else
|
||||
public static ConcurrentQueue<string> GetAllMatches(string file, IEnumerable<PathMatchSet>? matchers, bool any = false)
|
||||
#endif
|
||||
{
|
||||
return FindAllMatches(new List<string> { file }, matchers, any, false);
|
||||
}
|
||||
|
||||
// <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="any">True if any path match is a success, false if all have to match</param>
|
||||
/// <returns>List of strings representing the matched protections, null or empty otherwise</returns>
|
||||
#if NET20 || NET35
|
||||
public static Queue<string> GetAllMatches(IEnumerable<string>? files, IEnumerable<PathMatchSet>? matchers, bool any = false)
|
||||
#else
|
||||
public static ConcurrentQueue<string> GetAllMatches(IEnumerable<string>? files, IEnumerable<PathMatchSet>? matchers, bool any = false)
|
||||
#endif
|
||||
{
|
||||
return FindAllMatches(files, matchers, 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="any">True if any path match is a success, false if all have to match</param>
|
||||
/// <returns>String representing the matched protection, null otherwise</returns>
|
||||
public static string? GetFirstMatch(string file, IEnumerable<PathMatchSet> matchers, bool any = false)
|
||||
{
|
||||
var contentMatches = FindAllMatches(new List<string> { file }, matchers, any, true);
|
||||
if (contentMatches == null || !contentMatches.Any())
|
||||
return null;
|
||||
|
||||
return contentMatches.First();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get first path match for a given list of matchers
|
||||
/// </summary>
|
||||
/// <param name="files">File paths to check for matches</param>
|
||||
/// <param name="matchers">Enumerable of PathMatchSets to be run on the file</param>
|
||||
/// <param name="any">True if any path match is a success, false if all have to match</param>
|
||||
/// <returns>String representing the matched protection, null otherwise</returns>
|
||||
public static string? GetFirstMatch(IEnumerable<string> files, IEnumerable<PathMatchSet> matchers, bool any = false)
|
||||
{
|
||||
var contentMatches = FindAllMatches(files, matchers, any, true);
|
||||
if (contentMatches == null || !contentMatches.Any())
|
||||
return null;
|
||||
|
||||
return contentMatches.First();
|
||||
}
|
||||
|
||||
/// <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="any">True if any path match is a success, false if all have to match</param>
|
||||
/// <param name="stopAfterFirst">True to stop after the first match, false otherwise</param>
|
||||
/// <returns>List of strings representing the matched protections, null or empty otherwise</returns>
|
||||
#if NET20 || NET35
|
||||
private static Queue<string> FindAllMatches(IEnumerable<string>? files, IEnumerable<PathMatchSet>? matchers, bool any, bool stopAfterFirst)
|
||||
#else
|
||||
private static ConcurrentQueue<string> FindAllMatches(IEnumerable<string>? files, IEnumerable<PathMatchSet>? matchers, bool any, bool stopAfterFirst)
|
||||
#endif
|
||||
{
|
||||
// If there's no mappings, we can't match
|
||||
if (matchers == null || !matchers.Any())
|
||||
return new();
|
||||
|
||||
// Initialize the list of matched protections
|
||||
#if NET20 || NET35
|
||||
var matchedProtections = new Queue<string>();
|
||||
#else
|
||||
var matchedProtections = new ConcurrentQueue<string>();
|
||||
#endif
|
||||
|
||||
// Loop through and try everything otherwise
|
||||
foreach (var matcher in matchers)
|
||||
{
|
||||
// Determine if the matcher passes
|
||||
bool passes;
|
||||
string? firstMatchedString;
|
||||
if (any)
|
||||
{
|
||||
(bool anyPasses, var matchedString) = matcher.MatchesAny(files);
|
||||
passes = anyPasses;
|
||||
firstMatchedString = matchedString;
|
||||
}
|
||||
else
|
||||
{
|
||||
(bool allPasses, List<string> matchedStrings) = matcher.MatchesAll(files);
|
||||
passes = allPasses;
|
||||
firstMatchedString = matchedStrings.FirstOrDefault();
|
||||
}
|
||||
|
||||
// If we don't have a pass, just continue
|
||||
if (!passes || firstMatchedString == null)
|
||||
continue;
|
||||
|
||||
// If we there is no version method, just return the protection name
|
||||
if (matcher.GetVersion == null)
|
||||
{
|
||||
matchedProtections.Enqueue(matcher.ProtectionName ?? "Unknown Protection");
|
||||
}
|
||||
|
||||
// Otherwise, invoke the version method
|
||||
else
|
||||
{
|
||||
// A null version returned means the check didn't pass at the version step
|
||||
var version = matcher.GetVersion(firstMatchedString, files);
|
||||
if (version == null)
|
||||
continue;
|
||||
|
||||
matchedProtections.Enqueue($"{matcher.ProtectionName ?? "Unknown Protection"} {version}".Trim());
|
||||
}
|
||||
|
||||
// If we're stopping after the first protection, bail out here
|
||||
if (stopAfterFirst)
|
||||
return matchedProtections;
|
||||
}
|
||||
|
||||
return matchedProtections;
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
75
PathMatch.cs
75
PathMatch.cs
@@ -1,75 +0,0 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
|
||||
namespace SabreTools.Matching
|
||||
{
|
||||
/// <summary>
|
||||
/// Path matching criteria
|
||||
/// </summary>
|
||||
public class PathMatch : IMatch<string>
|
||||
{
|
||||
/// <summary>
|
||||
/// String to match
|
||||
/// </summary>
|
||||
#if NETFRAMEWORK || NETCOREAPP
|
||||
public string? Needle { get; private set; }
|
||||
#else
|
||||
public string? Needle { get; init; }
|
||||
#endif
|
||||
|
||||
/// <summary>
|
||||
/// Match exact casing instead of invariant
|
||||
/// </summary>
|
||||
public bool MatchExact { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Match that values end with the needle and not just contains
|
||||
/// </summary>
|
||||
public bool UseEndsWith { get; private set; }
|
||||
|
||||
/// <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)
|
||||
{
|
||||
this.Needle = needle;
|
||||
this.MatchExact = matchExact;
|
||||
this.UseEndsWith = useEndsWith;
|
||||
}
|
||||
|
||||
#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>Tuple of success and matched item</returns>
|
||||
public (bool, string?) Match(IEnumerable<string>? stack)
|
||||
{
|
||||
// If either array is null or empty, we can't do anything
|
||||
if (stack == null || !stack.Any() || this.Needle == null || this.Needle.Length == 0)
|
||||
return (false, null);
|
||||
|
||||
// Preprocess the needle, if necessary
|
||||
string procNeedle = this.MatchExact ? this.Needle : this.Needle.ToLowerInvariant();
|
||||
|
||||
foreach (string stackItem in stack)
|
||||
{
|
||||
// Preprocess the stack item, if necessary
|
||||
string procStackItem = this.MatchExact ? stackItem : stackItem.ToLowerInvariant();
|
||||
|
||||
if (this.UseEndsWith && procStackItem.EndsWith(procNeedle))
|
||||
return (true, stackItem);
|
||||
else if (!this.UseEndsWith && procStackItem.Contains(procNeedle))
|
||||
return (true, stackItem);
|
||||
}
|
||||
|
||||
return (false, null);
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
108
PathMatchSet.cs
108
PathMatchSet.cs
@@ -1,108 +0,0 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
|
||||
namespace SabreTools.Matching
|
||||
{
|
||||
/// <summary>
|
||||
/// A set of path matches that work together
|
||||
/// </summary>
|
||||
public class PathMatchSet : MatchSet<PathMatch, string>
|
||||
{
|
||||
/// <summary>
|
||||
/// Function to get a path version for this Matcher
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// A path version method takes the matched path and an enumerable of files
|
||||
/// and returns a single string. That string is either a version string,
|
||||
/// in which case it will be appended to the protection name, or `null`,
|
||||
/// in which case it will cause the protection to be omitted.
|
||||
/// </remarks>
|
||||
public Func<string, IEnumerable<string>?, string?>? GetVersion { get; private set; }
|
||||
|
||||
#region Constructors
|
||||
|
||||
public PathMatchSet(string needle, string protectionName)
|
||||
: this(new List<string> { needle }, null, protectionName) { }
|
||||
|
||||
public PathMatchSet(List<string> needles, string protectionName)
|
||||
: this(needles, null, protectionName) { }
|
||||
|
||||
public PathMatchSet(string needle, Func<string, IEnumerable<string>?, string?>? getVersion, string protectionName)
|
||||
: this(new List<string> { needle }, getVersion, protectionName) { }
|
||||
|
||||
public PathMatchSet(List<string> needles, Func<string, IEnumerable<string>?, string?>? getVersion, string protectionName)
|
||||
: this(needles.Select(n => new PathMatch(n)).ToList(), getVersion, protectionName) { }
|
||||
|
||||
public PathMatchSet(PathMatch needle, string protectionName)
|
||||
: this(new List<PathMatch>() { needle }, null, protectionName) { }
|
||||
|
||||
public PathMatchSet(List<PathMatch> needles, string protectionName)
|
||||
: this(needles, null, protectionName) { }
|
||||
|
||||
public PathMatchSet(PathMatch needle, Func<string, IEnumerable<string>?, string?>? getVersion, string protectionName)
|
||||
: this(new List<PathMatch>() { needle }, getVersion, protectionName) { }
|
||||
|
||||
public PathMatchSet(List<PathMatch> needles, Func<string, IEnumerable<string>?, string?>? getVersion, string protectionName)
|
||||
{
|
||||
Matchers = needles;
|
||||
GetVersion = getVersion;
|
||||
ProtectionName = protectionName;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Matching
|
||||
|
||||
/// <summary>
|
||||
/// Determine whether all path matches pass
|
||||
/// </summary>
|
||||
/// <param name="stack">List of strings to try to match</param>
|
||||
/// <returns>Tuple of passing status and matching values</returns>
|
||||
public (bool, List<string>) MatchesAll(IEnumerable<string>? stack)
|
||||
{
|
||||
// If no path matches are defined, we fail out
|
||||
if (Matchers == null || !Matchers.Any())
|
||||
return (false, new List<string>());
|
||||
|
||||
// Initialize the value list
|
||||
List<string> values = [];
|
||||
|
||||
// Loop through all path matches and make sure all pass
|
||||
foreach (var pathMatch in Matchers)
|
||||
{
|
||||
(bool match, string? value) = pathMatch.Match(stack);
|
||||
if (!match || value == null)
|
||||
return (false, new List<string>());
|
||||
else
|
||||
values.Add(value);
|
||||
}
|
||||
|
||||
return (true, values);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Determine whether any path matches pass
|
||||
/// </summary>
|
||||
/// <param name="stack">List of strings to try to match</param>
|
||||
/// <returns>Tuple of passing status and first matching value</returns>
|
||||
public (bool, string?) MatchesAny(IEnumerable<string>? stack)
|
||||
{
|
||||
// If no path matches are defined, we fail out
|
||||
if (Matchers == null || !Matchers.Any())
|
||||
return (false, null);
|
||||
|
||||
// Loop through all path matches and make sure all pass
|
||||
foreach (var pathMatch in Matchers)
|
||||
{
|
||||
(bool match, string? value) = pathMatch.Match(stack);
|
||||
if (match)
|
||||
return (true, value);
|
||||
}
|
||||
|
||||
return (false, null);
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,13 @@
|
||||
# SabreTools.Matching
|
||||
|
||||
[](https://github.com/SabreTools/SabreTools.Matching/actions/workflows/build_and_test.yml)
|
||||
|
||||
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)
|
||||
|
||||
38
SabreTools.Matching.Test/Compare/NaturalComparerTests.cs
Normal file
38
SabreTools.Matching.Test/Compare/NaturalComparerTests.cs
Normal file
@@ -0,0 +1,38 @@
|
||||
using System;
|
||||
using System.Linq;
|
||||
using SabreTools.Matching.Compare;
|
||||
using Xunit;
|
||||
|
||||
namespace SabreTools.Matching.Test.Compare
|
||||
{
|
||||
public class NaturalComparerTests
|
||||
{
|
||||
[Fact]
|
||||
public void ListSort_Numeric()
|
||||
{
|
||||
// Setup arrays
|
||||
string[] sortable = ["0", "100", "5", "2", "1000"];
|
||||
string[] expected = ["0", "2", "5", "100", "1000"];
|
||||
|
||||
// Run sorting on array
|
||||
Array.Sort(sortable, new NaturalComparer());
|
||||
|
||||
// Check the output
|
||||
Assert.True(sortable.SequenceEqual(expected));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ListSort_Mixed()
|
||||
{
|
||||
// Setup arrays
|
||||
string[] sortable = ["b3b", "c", "b", "a", "a1"];
|
||||
string[] expected = ["a", "a1", "b", "b3b", "c"];
|
||||
|
||||
// Run sorting on array
|
||||
Array.Sort(sortable, new NaturalComparer());
|
||||
|
||||
// Check the output
|
||||
Assert.True(sortable.SequenceEqual(expected));
|
||||
}
|
||||
}
|
||||
}
|
||||
66
SabreTools.Matching.Test/Compare/NaturalComparerUtilTests.cs
Normal file
66
SabreTools.Matching.Test/Compare/NaturalComparerUtilTests.cs
Normal file
@@ -0,0 +1,66 @@
|
||||
using SabreTools.Matching.Compare;
|
||||
using Xunit;
|
||||
|
||||
namespace SabreTools.Matching.Test.Compare
|
||||
{
|
||||
public class NaturalComparerUtilTests
|
||||
{
|
||||
[Fact]
|
||||
public void CompareNumeric_BothNull_Equal()
|
||||
{
|
||||
int actual = NaturalComparerUtil.ComparePaths(null, null);
|
||||
Assert.Equal(0, actual);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CompareNumeric_SingleNull_Ordered()
|
||||
{
|
||||
int actual = NaturalComparerUtil.ComparePaths(null, "notnull");
|
||||
Assert.Equal(-1, actual);
|
||||
|
||||
actual = NaturalComparerUtil.ComparePaths("notnull", null);
|
||||
Assert.Equal(1, actual);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CompareNumeric_BothEqual_Equal()
|
||||
{
|
||||
int actual = NaturalComparerUtil.ComparePaths("notnull", "notnull");
|
||||
Assert.Equal(0, actual);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CompareNumeric_BothEqualWithPath_Equal()
|
||||
{
|
||||
int actual = NaturalComparerUtil.ComparePaths("notnull/file.ext", "notnull/file.ext");
|
||||
Assert.Equal(0, actual);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CompareNumeric_BothEqualWithAltPath_Equal()
|
||||
{
|
||||
int actual = NaturalComparerUtil.ComparePaths("notnull/file.ext", "notnull\\file.ext");
|
||||
Assert.Equal(0, actual);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CompareNumeric_NumericNonDecimalString_Ordered()
|
||||
{
|
||||
int actual = NaturalComparerUtil.ComparePaths("100", "10");
|
||||
Assert.Equal(1, actual);
|
||||
|
||||
actual = NaturalComparerUtil.ComparePaths("10", "100");
|
||||
Assert.Equal(-1, actual);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CompareNumeric_NumericDecimalString_Ordered()
|
||||
{
|
||||
int actual = NaturalComparerUtil.ComparePaths("100.100", "100.10");
|
||||
Assert.Equal(1, actual);
|
||||
|
||||
actual = NaturalComparerUtil.ComparePaths("100.10", "100.100");
|
||||
Assert.Equal(-1, actual);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
}
|
||||
202
SabreTools.Matching.Test/Content/ContentMatchSetTests.cs
Normal file
202
SabreTools.Matching.Test/Content/ContentMatchSetTests.cs
Normal 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
|
||||
}
|
||||
}
|
||||
271
SabreTools.Matching.Test/Content/ContentMatchTests.cs
Normal file
271
SabreTools.Matching.Test/Content/ContentMatchTests.cs
Normal 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
|
||||
}
|
||||
}
|
||||
396
SabreTools.Matching.Test/ExtensionsTests.cs
Normal file
396
SabreTools.Matching.Test/ExtensionsTests.cs
Normal 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
|
||||
}
|
||||
}
|
||||
324
SabreTools.Matching.Test/MatchUtilTests.cs
Normal file
324
SabreTools.Matching.Test/MatchUtilTests.cs
Normal file
@@ -0,0 +1,324 @@
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using SabreTools.Matching.Content;
|
||||
using Xunit;
|
||||
|
||||
namespace SabreTools.Matching.Test
|
||||
{
|
||||
public class MatchUtilTests
|
||||
{
|
||||
#region Array
|
||||
|
||||
[Fact]
|
||||
public void ArrayGetAllMatches_NullStack_NoMatches()
|
||||
{
|
||||
byte[]? stack = null;
|
||||
List<ContentMatchSet> matchSets = [new ContentMatchSet(new byte[1], "name")];
|
||||
var matches = MatchUtil.GetAllMatches("file", stack, matchSets);
|
||||
Assert.Empty(matches);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ArrayGetAllMatches_EmptyStack_NoMatches()
|
||||
{
|
||||
byte[] stack = [];
|
||||
List<ContentMatchSet> matchSets = [new ContentMatchSet(new byte[1], "name")];
|
||||
var matches = MatchUtil.GetAllMatches("file", stack, matchSets);
|
||||
Assert.Empty(matches);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ArrayGetAllMatches_EmptyMatchSets_NoMatches()
|
||||
{
|
||||
byte[] stack = [0x01];
|
||||
List<ContentMatchSet> matchSets = [];
|
||||
var matches = MatchUtil.GetAllMatches("file", stack, matchSets);
|
||||
Assert.Empty(matches);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ArrayGetAllMatches_Matching_Matches()
|
||||
{
|
||||
byte[] stack = [0x01];
|
||||
List<ContentMatchSet> matchSets = [new ContentMatchSet(new byte[] { 0x01 }, "name")];
|
||||
var matches = MatchUtil.GetAllMatches("file", stack, matchSets);
|
||||
string setName = Assert.Single(matches);
|
||||
Assert.Equal("name", setName);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ArrayGetAllMatches_PartialMatchingAny_Matches()
|
||||
{
|
||||
byte[] stack = [0x01];
|
||||
List<ContentMatchSet> matchSets =
|
||||
[
|
||||
new ContentMatchSet([
|
||||
new byte[] { 0x00 },
|
||||
new ContentMatch([0x01]),
|
||||
], "name")
|
||||
];
|
||||
var matches = MatchUtil.GetAllMatches("file", stack, matchSets, any: true);
|
||||
string setName = Assert.Single(matches);
|
||||
Assert.Equal("name", setName);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ArrayGetAllMatches_PartialMatchingAll_NoMatches()
|
||||
{
|
||||
byte[] stack = [0x01];
|
||||
List<ContentMatchSet> matchSets =
|
||||
[
|
||||
new ContentMatchSet([
|
||||
new byte[] { 0x00 },
|
||||
new ContentMatch([0x01]),
|
||||
], "name")
|
||||
];
|
||||
var matches = MatchUtil.GetAllMatches("file", stack, matchSets, any: false);
|
||||
Assert.Empty(matches);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ArrayGetFirstMatch_NullStack_NoMatches()
|
||||
{
|
||||
byte[]? stack = null;
|
||||
List<ContentMatchSet> matchSets = [new ContentMatchSet(new byte[1], "name")];
|
||||
string? match = MatchUtil.GetFirstMatch("file", stack, matchSets);
|
||||
Assert.Null(match);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ArrayGetFirstMatch_EmptyStack_NoMatches()
|
||||
{
|
||||
byte[] stack = [];
|
||||
List<ContentMatchSet> matchSets = [new ContentMatchSet(new byte[1], "name")];
|
||||
string? match = MatchUtil.GetFirstMatch("file", stack, matchSets);
|
||||
Assert.Null(match);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ArrayGetFirstMatch_EmptyMatchSets_NoMatches()
|
||||
{
|
||||
byte[] stack = [0x01];
|
||||
List<ContentMatchSet> matchSets = [];
|
||||
string? match = MatchUtil.GetFirstMatch("file", stack, matchSets);
|
||||
Assert.Null(match);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ArrayGetFirstMatch_Matching_Matches()
|
||||
{
|
||||
byte[] stack = [0x01];
|
||||
List<ContentMatchSet> matchSets = [new ContentMatchSet(new byte[] { 0x01 }, "name")];
|
||||
string? setName = MatchUtil.GetFirstMatch("file", stack, matchSets);
|
||||
Assert.Equal("name", setName);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ArrayGetFirstMatch_PartialMatchingAny_Matches()
|
||||
{
|
||||
byte[] stack = [0x01];
|
||||
List<ContentMatchSet> matchSets =
|
||||
[
|
||||
new ContentMatchSet([
|
||||
new byte[] { 0x00 },
|
||||
new ContentMatch([0x01]),
|
||||
], "name")
|
||||
];
|
||||
string? setName = MatchUtil.GetFirstMatch("file", stack, matchSets, any: true);
|
||||
Assert.Equal("name", setName);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ArrayGetFirstMatch_PartialMatchingAll_NoMatches()
|
||||
{
|
||||
byte[] stack = [0x01];
|
||||
List<ContentMatchSet> matchSets =
|
||||
[
|
||||
new ContentMatchSet([
|
||||
new byte[] { 0x00 },
|
||||
new ContentMatch([0x01]),
|
||||
], "name")
|
||||
];
|
||||
string? setName = MatchUtil.GetFirstMatch("file", stack, matchSets, any: false);
|
||||
Assert.Null(setName);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ExactSizeArrayMatch()
|
||||
{
|
||||
byte[] source = [0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07];
|
||||
byte?[] check = [0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07];
|
||||
string expected = "match";
|
||||
|
||||
var matchers = new List<ContentMatchSet>
|
||||
{
|
||||
new(check, expected),
|
||||
};
|
||||
|
||||
string? actual = MatchUtil.GetFirstMatch("testfile", source, matchers, any: false);
|
||||
Assert.Equal(expected, actual);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Stream
|
||||
|
||||
[Fact]
|
||||
public void StreamGetAllMatches_NullStack_NoMatches()
|
||||
{
|
||||
Stream? stack = null;
|
||||
List<ContentMatchSet> matchSets = [new ContentMatchSet(new byte[1], "name")];
|
||||
var matches = MatchUtil.GetAllMatches("file", stack, matchSets);
|
||||
Assert.Empty(matches);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void StreamGetAllMatches_EmptyStack_NoMatches()
|
||||
{
|
||||
Stream stack = new MemoryStream();
|
||||
List<ContentMatchSet> matchSets = [new ContentMatchSet(new byte[1], "name")];
|
||||
var matches = MatchUtil.GetAllMatches("file", stack, matchSets);
|
||||
Assert.Empty(matches);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void StreamGetAllMatches_EmptyMatchSets_NoMatches()
|
||||
{
|
||||
Stream stack = new MemoryStream([0x01]);
|
||||
List<ContentMatchSet> matchSets = [];
|
||||
var matches = MatchUtil.GetAllMatches("file", stack, matchSets);
|
||||
Assert.Empty(matches);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void StreamGetAllMatches_Matching_Matches()
|
||||
{
|
||||
Stream stack = new MemoryStream([0x01]);
|
||||
List<ContentMatchSet> matchSets = [new ContentMatchSet(new byte[] { 0x01 }, "name")];
|
||||
var matches = MatchUtil.GetAllMatches("file", stack, matchSets);
|
||||
string setName = Assert.Single(matches);
|
||||
Assert.Equal("name", setName);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void StreamGetAllMatches_PartialMatchingAny_Matches()
|
||||
{
|
||||
Stream stack = new MemoryStream([0x01]);
|
||||
List<ContentMatchSet> matchSets =
|
||||
[
|
||||
new ContentMatchSet([
|
||||
new byte[] { 0x00 },
|
||||
new ContentMatch([0x01]),
|
||||
], "name")
|
||||
];
|
||||
var matches = MatchUtil.GetAllMatches("file", stack, matchSets, any: true);
|
||||
string setName = Assert.Single(matches);
|
||||
Assert.Equal("name", setName);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void StreamGetAllMatches_PartialMatchingAll_NoMatches()
|
||||
{
|
||||
Stream stack = new MemoryStream([0x01]);
|
||||
List<ContentMatchSet> matchSets =
|
||||
[
|
||||
new ContentMatchSet([
|
||||
new byte[] { 0x00 },
|
||||
new ContentMatch([0x01]),
|
||||
], "name")
|
||||
];
|
||||
var matches = MatchUtil.GetAllMatches("file", stack, matchSets, any: false);
|
||||
Assert.Empty(matches);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void StreamGetFirstMatch_NullStack_NoMatches()
|
||||
{
|
||||
Stream? stack = null;
|
||||
List<ContentMatchSet> matchSets = [new ContentMatchSet(new byte[1], "name")];
|
||||
string? match = MatchUtil.GetFirstMatch("file", stack, matchSets);
|
||||
Assert.Null(match);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void StreamGetFirstMatch_EmptyStack_NoMatches()
|
||||
{
|
||||
Stream stack = new MemoryStream();
|
||||
List<ContentMatchSet> matchSets = [new ContentMatchSet(new byte[1], "name")];
|
||||
string? match = MatchUtil.GetFirstMatch("file", stack, matchSets);
|
||||
Assert.Null(match);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void StreamGetFirstMatch_EmptyMatchSets_NoMatches()
|
||||
{
|
||||
Stream stack = new MemoryStream([0x01]);
|
||||
List<ContentMatchSet> matchSets = [];
|
||||
string? match = MatchUtil.GetFirstMatch("file", stack, matchSets);
|
||||
Assert.Null(match);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void StreamGetFirstMatch_Matching_Matches()
|
||||
{
|
||||
Stream stack = new MemoryStream([0x01]);
|
||||
List<ContentMatchSet> matchSets = [new ContentMatchSet(new byte[] { 0x01 }, "name")];
|
||||
string? setName = MatchUtil.GetFirstMatch("file", stack, matchSets);
|
||||
Assert.Equal("name", setName);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void StreamGetFirstMatch_PartialMatchingAny_Matches()
|
||||
{
|
||||
Stream stack = new MemoryStream([0x01]);
|
||||
List<ContentMatchSet> matchSets =
|
||||
[
|
||||
new ContentMatchSet([
|
||||
new byte[] { 0x00 },
|
||||
new ContentMatch([0x01]),
|
||||
], "name")
|
||||
];
|
||||
string? setName = MatchUtil.GetFirstMatch("file", stack, matchSets, any: true);
|
||||
Assert.Equal("name", setName);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void StreamGetFirstMatch_PartialMatchingAll_NoMatches()
|
||||
{
|
||||
Stream stack = new MemoryStream([0x01]);
|
||||
List<ContentMatchSet> matchSets =
|
||||
[
|
||||
new ContentMatchSet([
|
||||
new byte[] { 0x00 },
|
||||
new ContentMatch([0x01]),
|
||||
], "name")
|
||||
];
|
||||
string? setName = MatchUtil.GetFirstMatch("file", stack, matchSets, any: false);
|
||||
Assert.Null(setName);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ExactSizeStreamMatch()
|
||||
{
|
||||
byte[] source = [0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07];
|
||||
var stream = new MemoryStream(source);
|
||||
|
||||
byte?[] check = [0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07];
|
||||
string expected = "match";
|
||||
|
||||
var matchers = new List<ContentMatchSet>
|
||||
{
|
||||
new(check, expected),
|
||||
};
|
||||
|
||||
string? actual = MatchUtil.GetFirstMatch("testfile", stream, matchers, any: false);
|
||||
Assert.Equal(expected, actual);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Path
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
22
SabreTools.Matching.Test/Paths/FilePathMatchTests.cs
Normal file
22
SabreTools.Matching.Test/Paths/FilePathMatchTests.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
186
SabreTools.Matching.Test/Paths/PathMatchSetTests.cs
Normal file
186
SabreTools.Matching.Test/Paths/PathMatchSetTests.cs
Normal 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
|
||||
}
|
||||
}
|
||||
293
SabreTools.Matching.Test/Paths/PathMatchTests.cs
Normal file
293
SabreTools.Matching.Test/Paths/PathMatchTests.cs
Normal 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
|
||||
}
|
||||
}
|
||||
30
SabreTools.Matching.Test/SabreTools.Matching.Test.csproj
Normal file
30
SabreTools.Matching.Test/SabreTools.Matching.Test.csproj
Normal file
@@ -0,0 +1,30 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFrameworks>net8.0;net9.0</TargetFrameworks>
|
||||
<IsPackable>false</IsPackable>
|
||||
<LangVersion>latest</LangVersion>
|
||||
<NoWarn>NU1903</NoWarn>
|
||||
<Nullable>enable</Nullable>
|
||||
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="coverlet.collector" Version="6.0.4">
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.14.1" />
|
||||
<PackageReference Include="xunit" Version="2.9.3" />
|
||||
<PackageReference Include="xunit.runner.visualstudio" Version="3.1.3">
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
</PackageReference>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\SabreTools.Matching\SabreTools.Matching.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
||||
@@ -1,37 +0,0 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<!-- Assembly Properties -->
|
||||
<TargetFrameworks>net20;net35;net40;net452;net462;net472;net48;netcoreapp3.1;net5.0;net6.0;net7.0;net8.0</TargetFrameworks>
|
||||
<RuntimeIdentifiers>win-x86;win-x64;win-arm64;linux-x64;linux-arm64;osx-x64;osx-arm64</RuntimeIdentifiers>
|
||||
<LangVersion>latest</LangVersion>
|
||||
<Nullable>enable</Nullable>
|
||||
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
|
||||
<Version>1.3.0</Version>
|
||||
|
||||
<!-- Package Properties -->
|
||||
<Authors>Matt Nadareski</Authors>
|
||||
<Description>Byte array and stream matching library</Description>
|
||||
<Copyright>Copyright (c) Matt Nadareski 2018-2024</Copyright>
|
||||
<PackageProjectUrl>https://github.com/SabreTools/</PackageProjectUrl>
|
||||
<PackageReadmeFile>README.md</PackageReadmeFile>
|
||||
<RepositoryUrl>https://github.com/SabreTools/SabreTools.Matching</RepositoryUrl>
|
||||
<RepositoryType>git</RepositoryType>
|
||||
<PackageTags>byte array stream match matching</PackageTags>
|
||||
<PackageLicenseExpression>MIT</PackageLicenseExpression>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<None Include="README.md" Pack="true" PackagePath=""/>
|
||||
</ItemGroup>
|
||||
|
||||
<!-- Support for old .NET versions -->
|
||||
<ItemGroup Condition="$(TargetFramework.StartsWith(`net2`)) OR $(TargetFramework.StartsWith(`net3`))">
|
||||
<PackageReference Include="Net30.LinqBridge" Version="1.3.0" />
|
||||
<PackageReference Include="MinValueTupleBridge" Version="0.2.0" />
|
||||
</ItemGroup>
|
||||
<ItemGroup Condition="$(TargetFramework.StartsWith(`net4`))">
|
||||
<PackageReference Include="System.ValueTuple" Version="4.5.0" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
@@ -3,7 +3,9 @@ Microsoft Visual Studio Solution File, Format Version 12.00
|
||||
# Visual Studio Version 17
|
||||
VisualStudioVersion = 17.0.31903.59
|
||||
MinimumVisualStudioVersion = 10.0.40219.1
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SabreTools.Matching", "SabreTools.Matching.csproj", "{9555EB79-4E81-4988-9ABE-D6CE6042197E}"
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SabreTools.Matching", "SabreTools.Matching\SabreTools.Matching.csproj", "{9555EB79-4E81-4988-9ABE-D6CE6042197E}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SabreTools.Matching.Test", "SabreTools.Matching.Test\SabreTools.Matching.Test.csproj", "{B17EB9F4-E041-4E5C-A966-D2BEEDEA621F}"
|
||||
EndProject
|
||||
Global
|
||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||
@@ -18,5 +20,9 @@ Global
|
||||
{9555EB79-4E81-4988-9ABE-D6CE6042197E}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{9555EB79-4E81-4988-9ABE-D6CE6042197E}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{9555EB79-4E81-4988-9ABE-D6CE6042197E}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{B17EB9F4-E041-4E5C-A966-D2BEEDEA621F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{B17EB9F4-E041-4E5C-A966-D2BEEDEA621F}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{B17EB9F4-E041-4E5C-A966-D2BEEDEA621F}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{B17EB9F4-E041-4E5C-A966-D2BEEDEA621F}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
EndGlobalSection
|
||||
EndGlobal
|
||||
|
||||
92
SabreTools.Matching/Compare/NaturalComparer.cs
Normal file
92
SabreTools.Matching/Compare/NaturalComparer.cs
Normal file
@@ -0,0 +1,92 @@
|
||||
/*
|
||||
*
|
||||
* Links for info and original source code:
|
||||
*
|
||||
* https://blog.codinghorror.com/sorting-for-humans-natural-sort-order/
|
||||
* http://www.codeproject.com/Articles/22517/Natural-Sort-Comparer
|
||||
*
|
||||
* Exact code implementation used with permission, originally by motoschifo
|
||||
*
|
||||
*/
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text.RegularExpressions;
|
||||
|
||||
namespace SabreTools.Matching.Compare
|
||||
{
|
||||
public class NaturalComparer : Comparer<string>, IDisposable
|
||||
{
|
||||
private readonly Dictionary<string, string[]> _table;
|
||||
|
||||
public NaturalComparer()
|
||||
{
|
||||
_table = [];
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_table.Clear();
|
||||
}
|
||||
|
||||
public override int Compare(string? x, string? y)
|
||||
{
|
||||
if (x == null || y == null)
|
||||
{
|
||||
if (x == null && y != null)
|
||||
return -1;
|
||||
else if (x != null && y == null)
|
||||
return 1;
|
||||
else
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (x.ToLowerInvariant() == y.ToLowerInvariant())
|
||||
return x.CompareTo(y);
|
||||
|
||||
if (!_table.TryGetValue(x, out string[]? x1))
|
||||
{
|
||||
//x1 = Regex.Split(x.Replace(" ", string.Empty), "([0-9]+)");
|
||||
x1 = Regex.Split(x.ToLowerInvariant(), "([0-9]+)");
|
||||
x1 = Array.FindAll(x1, s => !string.IsNullOrEmpty(s));
|
||||
_table.Add(x, x1);
|
||||
}
|
||||
|
||||
if (!_table.TryGetValue(y, out string[]? y1))
|
||||
{
|
||||
//y1 = Regex.Split(y.Replace(" ", string.Empty), "([0-9]+)");
|
||||
y1 = Regex.Split(y.ToLowerInvariant(), "([0-9]+)");
|
||||
y1 = Array.FindAll(y1, s => !string.IsNullOrEmpty(s));
|
||||
_table.Add(y, y1);
|
||||
}
|
||||
|
||||
for (int i = 0; i < x1.Length && i < y1.Length; i++)
|
||||
{
|
||||
if (x1[i] != y1[i])
|
||||
return PartCompare(x1[i], y1[i]);
|
||||
}
|
||||
|
||||
if (x1.Length > y1.Length)
|
||||
return 1;
|
||||
else if (y1.Length > x1.Length)
|
||||
return -1;
|
||||
else
|
||||
return x.CompareTo(y);
|
||||
}
|
||||
|
||||
private static int PartCompare(string left, string right)
|
||||
{
|
||||
if (!long.TryParse(left, out long x))
|
||||
return NaturalComparerUtil.ComparePaths(left, right);
|
||||
|
||||
if (!long.TryParse(right, out long y))
|
||||
return NaturalComparerUtil.ComparePaths(left, right);
|
||||
|
||||
// If we have an equal part, then make sure that "longer" ones are taken into account
|
||||
if (x.CompareTo(y) == 0)
|
||||
return left.Length - right.Length;
|
||||
|
||||
return x.CompareTo(y);
|
||||
}
|
||||
}
|
||||
}
|
||||
96
SabreTools.Matching/Compare/NaturalComparerUtil.cs
Normal file
96
SabreTools.Matching/Compare/NaturalComparerUtil.cs
Normal file
@@ -0,0 +1,96 @@
|
||||
namespace SabreTools.Matching.Compare
|
||||
{
|
||||
public static class NaturalComparerUtil
|
||||
{
|
||||
/// <summary>
|
||||
/// Compare two strings by path parts
|
||||
/// </summary>
|
||||
public static int ComparePaths(string? left, string? right)
|
||||
{
|
||||
// If both strings are null, return
|
||||
if (left == null && right == null)
|
||||
return 0;
|
||||
|
||||
// If one is null, then say that's less than
|
||||
if (left == null)
|
||||
return -1;
|
||||
if (right == null)
|
||||
return 1;
|
||||
|
||||
// Normalize the path seperators
|
||||
left = left.Replace('\\', '/');
|
||||
right = right.Replace('\\', '/');
|
||||
|
||||
// Save the orginal adjusted strings
|
||||
string leftOrig = left;
|
||||
string rightOrig = right;
|
||||
|
||||
// Normalize strings by lower-case
|
||||
left = leftOrig.ToLowerInvariant();
|
||||
right = rightOrig.ToLowerInvariant();
|
||||
|
||||
// If the strings are the same exactly, return
|
||||
if (left == right)
|
||||
return leftOrig.CompareTo(rightOrig);
|
||||
|
||||
// Now split into path parts
|
||||
string[] leftParts = left.Split('/');
|
||||
string[] rightParts = right.Split('/');
|
||||
|
||||
// Then compare each part in turn
|
||||
for (int i = 0; i < leftParts.Length && i < rightParts.Length; i++)
|
||||
{
|
||||
int partCompare = ComparePathSegment(leftParts[i], rightParts[i]);
|
||||
if (partCompare != 0)
|
||||
return partCompare;
|
||||
}
|
||||
|
||||
// If we got out here, then it looped through at least one of the strings
|
||||
if (leftParts.Length > rightParts.Length)
|
||||
return 1;
|
||||
if (leftParts.Length < rightParts.Length)
|
||||
return -1;
|
||||
|
||||
return leftOrig.CompareTo(rightOrig);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Compare two path segments deterministically
|
||||
/// </summary>
|
||||
private static int ComparePathSegment(string left, string right)
|
||||
{
|
||||
// If the lengths are both zero, they're equal
|
||||
if (left.Length == 0 && right.Length == 0)
|
||||
return 0;
|
||||
|
||||
// Shorter strings are sorted before
|
||||
if (left.Length == 0)
|
||||
return -1;
|
||||
if (right.Length == 0)
|
||||
return 1;
|
||||
|
||||
// Otherwise, loop through until we have an answer
|
||||
for (int i = 0; i < left.Length && i < right.Length; i++)
|
||||
{
|
||||
// Get the next characters from the inputs as integers
|
||||
int leftChar = left[i];
|
||||
int rightChar = right[i];
|
||||
|
||||
// If the characters are the same, continue
|
||||
if (leftChar == rightChar)
|
||||
continue;
|
||||
|
||||
// If they're different, check which one was larger
|
||||
return leftChar > rightChar ? 1 : -1;
|
||||
}
|
||||
|
||||
// If we got out here, then it looped through at least one of the strings
|
||||
if (left.Length > right.Length)
|
||||
return 1;
|
||||
if (left.Length < right.Length)
|
||||
return -1;
|
||||
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
92
SabreTools.Matching/Compare/NaturalReversedComparer.cs
Normal file
92
SabreTools.Matching/Compare/NaturalReversedComparer.cs
Normal file
@@ -0,0 +1,92 @@
|
||||
/*
|
||||
*
|
||||
* Links for info and original source code:
|
||||
*
|
||||
* https://blog.codinghorror.com/sorting-for-humans-natural-sort-order/
|
||||
* http://www.codeproject.com/Articles/22517/Natural-Sort-Comparer
|
||||
*
|
||||
* Exact code implementation used with permission, originally by motoschifo
|
||||
*
|
||||
*/
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text.RegularExpressions;
|
||||
|
||||
namespace SabreTools.Matching.Compare
|
||||
{
|
||||
public class NaturalReversedComparer : Comparer<string>, IDisposable
|
||||
{
|
||||
private readonly Dictionary<string, string[]> _table;
|
||||
|
||||
public NaturalReversedComparer()
|
||||
{
|
||||
_table = [];
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_table.Clear();
|
||||
}
|
||||
|
||||
public override int Compare(string? x, string? y)
|
||||
{
|
||||
if (x == null || y == null)
|
||||
{
|
||||
if (x == null && y != null)
|
||||
return -1;
|
||||
else if (x != null && y == null)
|
||||
return 1;
|
||||
else
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (y.ToLowerInvariant() == x.ToLowerInvariant())
|
||||
return y.CompareTo(x);
|
||||
|
||||
if (!_table.TryGetValue(x, out string[]? x1))
|
||||
{
|
||||
//x1 = Regex.Split(x.Replace(" ", string.Empty), "([0-9]+)");
|
||||
x1 = Regex.Split(x.ToLowerInvariant(), "([0-9]+)");
|
||||
x1 = Array.FindAll(x1, s => !string.IsNullOrEmpty(s));
|
||||
_table.Add(x, x1);
|
||||
}
|
||||
|
||||
if (!_table.TryGetValue(y, out string[]? y1))
|
||||
{
|
||||
//y1 = Regex.Split(y.Replace(" ", string.Empty), "([0-9]+)");
|
||||
y1 = Regex.Split(y.ToLowerInvariant(), "([0-9]+)");
|
||||
y1 = Array.FindAll(y1, s => !string.IsNullOrEmpty(s));
|
||||
_table.Add(y, y1);
|
||||
}
|
||||
|
||||
for (int i = 0; i < x1.Length && i < y1.Length; i++)
|
||||
{
|
||||
if (x1[i] != y1[i])
|
||||
return PartCompare(x1[i], y1[i]);
|
||||
}
|
||||
|
||||
if (y1.Length > x1.Length)
|
||||
return 1;
|
||||
else if (x1.Length > y1.Length)
|
||||
return -1;
|
||||
else
|
||||
return y.CompareTo(x);
|
||||
}
|
||||
|
||||
private static int PartCompare(string left, string right)
|
||||
{
|
||||
if (!long.TryParse(left, out long x))
|
||||
return NaturalComparerUtil.ComparePaths(right, left);
|
||||
|
||||
if (!long.TryParse(right, out long y))
|
||||
return NaturalComparerUtil.ComparePaths(right, left);
|
||||
|
||||
// If we have an equal part, then make sure that "longer" ones are taken into account
|
||||
if (y.CompareTo(x) == 0)
|
||||
return right.Length - left.Length;
|
||||
|
||||
return y.CompareTo(x);
|
||||
}
|
||||
}
|
||||
}
|
||||
332
SabreTools.Matching/Content/ContentMatch.cs
Normal file
332
SabreTools.Matching/Content/ContentMatch.cs
Normal file
@@ -0,0 +1,332 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
|
||||
namespace SabreTools.Matching.Content
|
||||
{
|
||||
/// <summary>
|
||||
/// Content matching criteria
|
||||
/// </summary>
|
||||
public class ContentMatch : IMatch<byte?[]>
|
||||
{
|
||||
/// <summary>
|
||||
/// Content to match
|
||||
/// </summary>
|
||||
public byte?[] Needle { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Starting index for matching
|
||||
/// </summary>
|
||||
private readonly int _start;
|
||||
|
||||
/// <summary>
|
||||
/// Ending index for matching
|
||||
/// </summary>
|
||||
private readonly int _end;
|
||||
|
||||
/// <summary>
|
||||
/// Constructor
|
||||
/// </summary>
|
||||
/// <param name="needle">Byte array representing the search</param>
|
||||
/// <param name="start">Optional starting position in the stack, defaults to 0</param>
|
||||
/// <param name="end">Optional ending position in the stack, defaults to -1 (length of stack)</param>
|
||||
public ContentMatch(byte[] needle, int start = 0, int end = -1)
|
||||
{
|
||||
// Validate the inputs
|
||||
if (needle.Length == 0)
|
||||
throw new InvalidDataException(nameof(needle));
|
||||
if (start < 0)
|
||||
throw new ArgumentOutOfRangeException(nameof(start));
|
||||
if (end < -1)
|
||||
throw new ArgumentOutOfRangeException(nameof(end));
|
||||
|
||||
Needle = Array.ConvertAll(needle, b => (byte?)b);
|
||||
_start = start;
|
||||
_end = end;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Constructor
|
||||
/// </summary>
|
||||
/// <param name="needle">Nullable byte array representing the search</param>
|
||||
/// <param name="start">Optional starting position in the stack, defaults to 0</param>
|
||||
/// <param name="end">Optional ending position in the stack, defaults to -1 (length of stack)</param>
|
||||
public ContentMatch(byte?[] needle, int start = 0, int end = -1)
|
||||
{
|
||||
// Validate the inputs
|
||||
if (needle.Length == 0)
|
||||
throw new InvalidDataException(nameof(needle));
|
||||
if (start < 0)
|
||||
throw new ArgumentOutOfRangeException(nameof(start));
|
||||
if (end < -1)
|
||||
throw new ArgumentOutOfRangeException(nameof(end));
|
||||
|
||||
Needle = needle;
|
||||
_start = start;
|
||||
_end = end;
|
||||
}
|
||||
|
||||
#region Conversion
|
||||
|
||||
/// <summary>
|
||||
/// Allow conversion from byte array to ContentMatch
|
||||
/// </summary>
|
||||
public static implicit operator ContentMatch(byte[] needle) => new ContentMatch(needle);
|
||||
|
||||
/// <summary>
|
||||
/// Allow conversion from nullable byte array to ContentMatch
|
||||
/// </summary>
|
||||
public static implicit operator ContentMatch(byte?[] needle) => new ContentMatch(needle);
|
||||
|
||||
#endregion
|
||||
|
||||
#region Array Matching
|
||||
|
||||
/// <summary>
|
||||
/// Get if this match can be found in a stack
|
||||
/// </summary>
|
||||
/// <param name="stack">Array to search for the given content</param>
|
||||
/// <param name="reverse">True to search from the end of the array, false from the start</param>
|
||||
/// <returns>Found position on success, -1 otherwise</returns>
|
||||
public int Match(byte[]? stack, bool reverse = false)
|
||||
{
|
||||
// If either set is null or empty
|
||||
if (stack == null || stack.Length == 0 || Needle.Length == 0)
|
||||
return -1;
|
||||
|
||||
// Get the adjusted end value for comparison
|
||||
int end = _end < 0 ? stack.Length : _end;
|
||||
end = end > stack.Length ? stack.Length : end;
|
||||
|
||||
// If the stack window is invalid
|
||||
if (end < _start)
|
||||
return -1;
|
||||
|
||||
// If the needle is larger than the stack window, it can't be contained within
|
||||
if (Needle.Length > stack.Length - _start)
|
||||
return -1;
|
||||
|
||||
// If the needle and stack window are identically sized, short-circuit
|
||||
if (Needle.Length == stack.Length - _start)
|
||||
return EqualAt(stack, _start) ? _start : -1;
|
||||
|
||||
// Return based on the direction of search
|
||||
return reverse ? MatchReverse(stack) : MatchForward(stack);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Match within a stack starting from the smallest index
|
||||
/// </summary>
|
||||
/// <param name="stack">Array to search for the given content</param>
|
||||
/// <returns>Found position on success, -1 otherwise</returns>
|
||||
private int MatchForward(byte[] stack)
|
||||
{
|
||||
// Set the default start and end values
|
||||
int start = _start < 0 ? 0 : _start;
|
||||
int end = _end < 0 ? stack.Length - Needle.Length : _end;
|
||||
|
||||
// Loop starting from the smallest index
|
||||
for (int i = start; i < end; i++)
|
||||
{
|
||||
// If we somehow have an invalid end and we haven't matched, return
|
||||
if (i > stack.Length)
|
||||
return -1;
|
||||
|
||||
// Check to see if the values are equal
|
||||
if (EqualAt(stack, i))
|
||||
return i;
|
||||
}
|
||||
|
||||
return -1;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Match within a stack starting from the largest index
|
||||
/// </summary>
|
||||
/// <param name="stack">Array to search for the given content</param>
|
||||
/// <returns>Found position on success, -1 otherwise</returns>
|
||||
private int MatchReverse(byte[] stack)
|
||||
{
|
||||
// Set the default start and end values
|
||||
int start = _start < 0 ? 0 : _start;
|
||||
int end = _end < 0 ? stack.Length - Needle.Length : _end;
|
||||
|
||||
// Loop starting from the largest index
|
||||
for (int i = end; i > start; i--)
|
||||
{
|
||||
// If we somehow have an invalid end and we haven't matched, return
|
||||
if (i > stack.Length)
|
||||
return -1;
|
||||
|
||||
// Check to see if the values are equal
|
||||
if (EqualAt(stack, i))
|
||||
return i;
|
||||
}
|
||||
|
||||
return -1;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get if a stack at a certain index is equal to a needle
|
||||
/// </summary>
|
||||
/// <param name="stack">Array to search for the given content</param>
|
||||
/// <param name="index">Starting index to check equality</param>
|
||||
/// <returns>True if the needle matches the stack at a given index</returns>
|
||||
private bool EqualAt(byte[] stack, int index)
|
||||
{
|
||||
// If the index is invalid, we can't do anything
|
||||
if (index < 0)
|
||||
return false;
|
||||
|
||||
// If we're too close to the end of the stack, return false
|
||||
if (Needle.Length > stack.Length - index)
|
||||
return false;
|
||||
|
||||
// Loop through and check the value
|
||||
for (int i = 0; i < Needle.Length; i++)
|
||||
{
|
||||
// A null value is a wildcard
|
||||
if (Needle[i] == null)
|
||||
continue;
|
||||
else if (stack[i + index] != Needle[i])
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Stream Matching
|
||||
|
||||
/// <summary>
|
||||
/// Get if this match can be found in a stack
|
||||
/// </summary>
|
||||
/// <param name="stack">Stream to search for the given content</param>
|
||||
/// <param name="reverse">True to search from the end of the array, false from the start</param>
|
||||
/// <returns>Found position on success, -1 otherwise</returns>
|
||||
public int Match(Stream? stack, bool reverse = false)
|
||||
{
|
||||
// If either set is null or empty
|
||||
if (stack == null || stack.Length == 0 || Needle.Length == 0)
|
||||
return -1;
|
||||
|
||||
// Get the adjusted end value for comparison
|
||||
int end = _end < 0 ? (int)stack.Length : _end;
|
||||
end = end > (int)stack.Length ? (int)stack.Length : end;
|
||||
|
||||
// If the stack window is invalid
|
||||
if (end < _start)
|
||||
return -1;
|
||||
|
||||
// If the needle is larger than the stack window, it can't be contained within
|
||||
if (Needle.Length > stack.Length - _start)
|
||||
return -1;
|
||||
|
||||
// If the needle and stack window are identically sized, short-circuit
|
||||
if (Needle.Length == stack.Length - _start)
|
||||
return EqualAt(stack, _start) ? _start : -1;
|
||||
|
||||
// Return based on the direction of search
|
||||
return reverse ? MatchReverse(stack) : MatchForward(stack);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Match within a stack starting from the smallest index
|
||||
/// </summary>
|
||||
/// <param name="stack">Stream to search for the given content</param>
|
||||
/// <returns>Found position on success, -1 otherwise</returns>
|
||||
private int MatchForward(Stream stack)
|
||||
{
|
||||
// Set the default start and end values
|
||||
int start = _start < 0 ? 0 : _start;
|
||||
int end = _end < 0 ? (int)stack.Length - Needle.Length : _end;
|
||||
|
||||
// Loop starting from the smallest index
|
||||
for (int i = start; i < end; i++)
|
||||
{
|
||||
// If we somehow have an invalid end and we haven't matched, return
|
||||
if (i > stack.Length)
|
||||
return -1;
|
||||
|
||||
// Check to see if the values are equal
|
||||
if (EqualAt(stack, i))
|
||||
return i;
|
||||
}
|
||||
|
||||
return -1;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Match within a stack starting from the largest index
|
||||
/// </summary>
|
||||
/// <param name="stack">Stream to search for the given content</param>
|
||||
/// <returns>Found position on success, -1 otherwise</returns>
|
||||
private int MatchReverse(Stream stack)
|
||||
{
|
||||
// Set the default start and end values
|
||||
int start = _start < 0 ? 0 : _start;
|
||||
int end = _end < 0 ? (int)stack.Length - Needle.Length : _end;
|
||||
|
||||
// Loop starting from the largest index
|
||||
for (int i = end; i > start; i--)
|
||||
{
|
||||
// If we somehow have an invalid end and we haven't matched, return
|
||||
if (i > stack.Length)
|
||||
return -1;
|
||||
|
||||
// Check to see if the values are equal
|
||||
if (EqualAt(stack, i))
|
||||
return i;
|
||||
}
|
||||
|
||||
return -1;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get if a stack at a certain index is equal to a needle
|
||||
/// </summary>
|
||||
/// <param name="stack">Stream to search for the given content</param>
|
||||
/// <param name="index">Starting index to check equality</param>
|
||||
/// <returns>True if the needle matches the stack at a given index</returns>
|
||||
private bool EqualAt(Stream stack, int index)
|
||||
{
|
||||
// If the index is invalid, we can't do anything
|
||||
if (index < 0)
|
||||
return false;
|
||||
|
||||
// If we're too close to the end of the stack, return false
|
||||
if (Needle.Length > stack.Length - index)
|
||||
return false;
|
||||
|
||||
// Save the current position and move to the index
|
||||
long currentPosition = stack.Position;
|
||||
stack.Seek(index, SeekOrigin.Begin);
|
||||
|
||||
// Set the return value
|
||||
bool matched = true;
|
||||
|
||||
// Loop through and check the value
|
||||
for (int i = 0; i < Needle.Length; i++)
|
||||
{
|
||||
byte stackValue = (byte)stack.ReadByte();
|
||||
|
||||
// A null value is a wildcard
|
||||
if (Needle[i] == null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
else if (stackValue != Needle[i])
|
||||
{
|
||||
matched = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Reset the position and return the value
|
||||
stack.Seek(currentPosition, SeekOrigin.Begin);
|
||||
return matched;
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
238
SabreTools.Matching/Content/ContentMatchSet.cs
Normal file
238
SabreTools.Matching/Content/ContentMatchSet.cs
Normal file
@@ -0,0 +1,238 @@
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
|
||||
namespace SabreTools.Matching.Content
|
||||
{
|
||||
/// <summary>
|
||||
/// A set of content matches that work together
|
||||
/// </summary>
|
||||
public class ContentMatchSet : IMatchSet<ContentMatch, byte?[]>
|
||||
{
|
||||
/// <inheritdoc/>
|
||||
public List<ContentMatch> Matchers { get; }
|
||||
|
||||
/// <inheritdoc/>
|
||||
public string SetName { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Function to get a content version
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// A content version method takes the file path, the file contents,
|
||||
/// and a list of found positions and returns a single string. That
|
||||
/// string is either a version string, in which case it will be appended
|
||||
/// to the match name, or `null`, in which case it will cause
|
||||
/// the match name to be omitted.
|
||||
/// </remarks>
|
||||
public GetArrayVersion? GetArrayVersion { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Function to get a content version
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// A content version method takes the file path, the file contents,
|
||||
/// and a list of found positions and returns a single string. That
|
||||
/// string is either a version string, in which case it will be appended
|
||||
/// to the match name, or `null`, in which case it will cause
|
||||
/// the match name to be omitted.
|
||||
/// </remarks>
|
||||
public GetStreamVersion? GetStreamVersion { get; }
|
||||
|
||||
#region Generic Constructors
|
||||
|
||||
/// <summary>
|
||||
/// Constructor
|
||||
/// </summary>
|
||||
/// <param name="needle">ContentMatch representing the comparisons</param>
|
||||
/// <param name="setName">Unique name for the set</param>
|
||||
public ContentMatchSet(ContentMatch needle, string setName)
|
||||
: this([needle], setName) { }
|
||||
|
||||
/// <summary>
|
||||
/// Constructor
|
||||
/// </summary>
|
||||
/// <param name="needles">List of ContentMatch objects representing the comparisons</param>
|
||||
/// <param name="setName">Unique name for the set</param>
|
||||
public ContentMatchSet(List<ContentMatch> needles, string setName)
|
||||
{
|
||||
// Validate the inputs
|
||||
if (needles.Count == 0)
|
||||
throw new InvalidDataException(nameof(needles));
|
||||
|
||||
Matchers = needles;
|
||||
SetName = setName;
|
||||
GetArrayVersion = null;
|
||||
GetStreamVersion = null;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Array Constructors
|
||||
|
||||
/// <summary>
|
||||
/// Constructor
|
||||
/// </summary>
|
||||
/// <param name="needle">ContentMatch representing the comparisons</param>
|
||||
/// <param name="getVersion">Delegate for deriving a version on match of an array</param>
|
||||
/// <param name="setName">Unique name for the set</param>
|
||||
public ContentMatchSet(ContentMatch needle, GetArrayVersion getVersion, string setName)
|
||||
: this([needle], getVersion, setName) { }
|
||||
|
||||
/// <summary>
|
||||
/// Constructor
|
||||
/// </summary>
|
||||
/// <param name="needles">List of ContentMatch objects representing the comparisons</param>
|
||||
/// <param name="getVersion">Delegate for deriving a version on match of an array</param>
|
||||
/// <param name="setName">Unique name for the set</param>
|
||||
public ContentMatchSet(List<ContentMatch> needles, GetArrayVersion getVersion, string setName)
|
||||
{
|
||||
// Validate the inputs
|
||||
if (needles.Count == 0)
|
||||
throw new InvalidDataException(nameof(needles));
|
||||
|
||||
Matchers = needles;
|
||||
SetName = setName;
|
||||
GetArrayVersion = getVersion;
|
||||
GetStreamVersion = null;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Stream Constructors
|
||||
|
||||
/// <summary>
|
||||
/// Constructor
|
||||
/// </summary>
|
||||
/// <param name="needle">ContentMatch representing the comparisons</param>
|
||||
/// <param name="getVersion">Delegate for deriving a version on match of a Stream</param>
|
||||
/// <param name="setName">Unique name for the set</param>
|
||||
public ContentMatchSet(ContentMatch needle, GetStreamVersion getVersion, string setName)
|
||||
: this([needle], getVersion, setName) { }
|
||||
|
||||
/// <summary>
|
||||
/// Constructor
|
||||
/// </summary>
|
||||
/// <param name="needles">List of ContentMatch objects representing the comparisons</param>
|
||||
/// <param name="getVersion">Delegate for deriving a version on match of a Stream</param>
|
||||
/// <param name="setName">Unique name for the set</param>
|
||||
public ContentMatchSet(List<ContentMatch> needles, GetStreamVersion getVersion, string setName)
|
||||
{
|
||||
// Validate the inputs
|
||||
if (needles.Count == 0)
|
||||
throw new InvalidDataException(nameof(needles));
|
||||
|
||||
Matchers = needles;
|
||||
SetName = setName;
|
||||
GetArrayVersion = null;
|
||||
GetStreamVersion = getVersion;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Array Matching
|
||||
|
||||
/// <summary>
|
||||
/// Determine whether all content matches pass
|
||||
/// </summary>
|
||||
/// <param name="stack">Array to search</param>
|
||||
/// <returns>List of matching positions, if any</returns>
|
||||
public List<int> MatchesAll(byte[]? stack)
|
||||
{
|
||||
// If either set is null or empty
|
||||
if (stack == null || stack.Length == 0 || Matchers.Count == 0)
|
||||
return [];
|
||||
|
||||
// Initialize the position list
|
||||
var positions = new List<int>();
|
||||
|
||||
// Loop through all content matches and make sure all pass
|
||||
foreach (var contentMatch in Matchers)
|
||||
{
|
||||
int position = contentMatch.Match(stack);
|
||||
if (position < 0)
|
||||
return [];
|
||||
|
||||
positions.Add(position);
|
||||
}
|
||||
|
||||
return positions;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Determine whether any content matches pass
|
||||
/// </summary>
|
||||
/// <param name="stack">Array to search</param>
|
||||
/// <returns>First matching position on success, -1 on error</returns>
|
||||
public int MatchesAny(byte[]? stack)
|
||||
{
|
||||
// If either set is null or empty
|
||||
if (stack == null || stack.Length == 0 || Matchers.Count == 0)
|
||||
return -1;
|
||||
|
||||
// Loop through all content matches and make sure all pass
|
||||
foreach (var contentMatch in Matchers)
|
||||
{
|
||||
int position = contentMatch.Match(stack);
|
||||
if (position >= 0)
|
||||
return position;
|
||||
}
|
||||
|
||||
return -1;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Stream Matching
|
||||
|
||||
/// <summary>
|
||||
/// Determine whether all content matches pass
|
||||
/// </summary>
|
||||
/// <param name="stack">Stream to search</param>
|
||||
/// <returns>List of matching positions, if any</returns>
|
||||
public List<int> MatchesAll(Stream? stack)
|
||||
{
|
||||
// If either set is null or empty
|
||||
if (stack == null || stack.Length == 0 || Matchers.Count == 0)
|
||||
return [];
|
||||
|
||||
// Initialize the position list
|
||||
var positions = new List<int>();
|
||||
|
||||
// Loop through all content matches and make sure all pass
|
||||
foreach (var contentMatch in Matchers)
|
||||
{
|
||||
int position = contentMatch.Match(stack);
|
||||
if (position < 0)
|
||||
return [];
|
||||
|
||||
positions.Add(position);
|
||||
}
|
||||
|
||||
return positions;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Determine whether any content matches pass
|
||||
/// </summary>
|
||||
/// <param name="stack">Stream to search</param>
|
||||
/// <returns>First matching position on success, -1 on error</returns>
|
||||
public int MatchesAny(Stream? stack)
|
||||
{
|
||||
// If either set is null or empty
|
||||
if (stack == null || stack.Length == 0 || Matchers.Count == 0)
|
||||
return -1;
|
||||
|
||||
// Loop through all content matches and make sure all pass
|
||||
foreach (var contentMatch in Matchers)
|
||||
{
|
||||
int position = contentMatch.Match(stack);
|
||||
if (position >= 0)
|
||||
return position;
|
||||
}
|
||||
|
||||
return -1;
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
31
SabreTools.Matching/Delegates.cs
Normal file
31
SabreTools.Matching/Delegates.cs
Normal file
@@ -0,0 +1,31 @@
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
|
||||
namespace SabreTools.Matching
|
||||
{
|
||||
/// <summary>
|
||||
/// Get a version number from a file
|
||||
/// </summary>
|
||||
/// <param name="path">File path to get the version from</param>
|
||||
/// <param name="content">Optional file contents as a byte array</param>
|
||||
/// <param name="positions">List of positions in the array that were matched</param>
|
||||
/// <returns>Version string on success, null on failure</returns>
|
||||
public delegate string? GetArrayVersion(string path, byte[]? content, List<int> positions);
|
||||
|
||||
/// <summary>
|
||||
/// Get a version number from an input path
|
||||
/// </summary>
|
||||
/// <param name="path">File or directory path to get the version from</param>
|
||||
/// <param name="files">Optional set of files in the directory</param>
|
||||
/// <returns>Version string on success, null on failure</returns>
|
||||
public delegate string? GetPathVersion(string path, List<string>? files);
|
||||
|
||||
/// <summary>
|
||||
/// Get a version number from a file
|
||||
/// </summary>
|
||||
/// <param name="path">File path to get the version from</param>
|
||||
/// <param name="content">Optional file contents as a Stream</param>
|
||||
/// <param name="positions">List of positions in the Stream that were matched</param>
|
||||
/// <returns>Version string on success, null on failure</returns>
|
||||
public delegate string? GetStreamVersion(string path, Stream? content, List<int> positions);
|
||||
}
|
||||
9
SabreTools.Matching/ExtensionAttribute.cs
Normal file
9
SabreTools.Matching/ExtensionAttribute.cs
Normal file
@@ -0,0 +1,9 @@
|
||||
#if NET20
|
||||
|
||||
namespace System.Runtime.CompilerServices
|
||||
{
|
||||
[AttributeUsage(AttributeTargets.Assembly | AttributeTargets.Class | AttributeTargets.Method)]
|
||||
internal sealed class ExtensionAttribute : Attribute {}
|
||||
}
|
||||
|
||||
#endif
|
||||
226
SabreTools.Matching/Extensions.cs
Normal file
226
SabreTools.Matching/Extensions.cs
Normal file
@@ -0,0 +1,226 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using SabreTools.Matching.Content;
|
||||
|
||||
namespace SabreTools.Matching
|
||||
{
|
||||
public static class Extensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Find all positions of one array in another, if possible
|
||||
/// </summary>
|
||||
/// <param name="stack">Byte array to search within</param>
|
||||
/// <param name="needle">Byte array representing the search value</param>
|
||||
/// <param name="start">Optional starting position in the stack, defaults to 0</param>
|
||||
/// <param name="end">Optional ending position in the stack, defaults to -1 (length of stack)</param>
|
||||
public static List<int> FindAllPositions(this byte[] stack, byte[] needle, int start = 0, int end = -1)
|
||||
{
|
||||
byte?[] nullableNeedle = Array.ConvertAll(needle, b => (byte?)b);
|
||||
return FindAllPositions(stack, nullableNeedle, start, end);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Find all positions of one array in another, if possible
|
||||
/// </summary>
|
||||
/// <param name="stack">Byte array to search within</param>
|
||||
/// <param name="needle">Byte array representing the search value</param>
|
||||
/// <param name="start">Optional starting position in the stack, defaults to 0</param>
|
||||
/// <param name="end">Optional ending position in the stack, defaults to -1 (length of stack)</param>
|
||||
public static List<int> FindAllPositions(this byte[] stack, byte?[] needle, int start = 0, int end = -1)
|
||||
{
|
||||
// Get the outgoing list
|
||||
List<int> positions = [];
|
||||
|
||||
// If either set is null or empty
|
||||
if (stack.Length == 0 || needle.Length == 0)
|
||||
return positions;
|
||||
|
||||
// If the needle is longer than the stack
|
||||
if (needle.Length > stack.Length)
|
||||
return positions;
|
||||
|
||||
// Normalize the end value, if necessary
|
||||
if (end == -1)
|
||||
end = stack.Length;
|
||||
|
||||
// Validate the start and end values
|
||||
if (start < 0 || start >= stack.Length)
|
||||
return positions;
|
||||
if (end < -1 || end < start || end > stack.Length)
|
||||
return positions;
|
||||
|
||||
// Loop while there is data to check
|
||||
while (start < end)
|
||||
{
|
||||
// Create a new matcher for this segment
|
||||
var matcher = new ContentMatch(needle, start, end);
|
||||
|
||||
// Get the next matching position
|
||||
int position = matcher.Match(stack, reverse: false);
|
||||
if (position < 0)
|
||||
break;
|
||||
|
||||
// Append the position and reset the start index
|
||||
positions.Add(position);
|
||||
start = position + 1;
|
||||
}
|
||||
|
||||
return positions;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Find the first position of one array in another, if possible
|
||||
/// </summary>
|
||||
/// <param name="stack">Byte array to search within</param>
|
||||
/// <param name="needle">Byte array representing the search value</param>
|
||||
/// <param name="start">Optional starting position in the stack, defaults to 0</param>
|
||||
/// <param name="end">Optional ending position in the stack, defaults to -1 (length of stack)</param>
|
||||
public static int FirstPosition(this byte[] stack, byte[] needle, int start = 0, int end = -1)
|
||||
{
|
||||
byte?[] nullableNeedle = Array.ConvertAll(needle, b => (byte?)b);
|
||||
return FirstPosition(stack, nullableNeedle, start, end);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Find the first position of one array in another, if possible
|
||||
/// </summary>
|
||||
/// <param name="stack">Byte array to search within</param>
|
||||
/// <param name="needle">Byte array representing the search value</param>
|
||||
/// <param name="start">Optional starting position in the stack, defaults to 0</param>
|
||||
/// <param name="end">Optional ending position in the stack, defaults to -1 (length of stack)</param>
|
||||
public static int FirstPosition(this byte[] stack, byte?[] needle, int start = 0, int end = -1)
|
||||
{
|
||||
// If either set is null or empty
|
||||
if (stack.Length == 0 || needle.Length == 0)
|
||||
return -1;
|
||||
|
||||
// If the needle is longer than the stack
|
||||
if (needle.Length > stack.Length)
|
||||
return -1;
|
||||
|
||||
var matcher = new ContentMatch(needle, start, end);
|
||||
return matcher.Match(stack, reverse: false);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Find the last position of one array in another, if possible
|
||||
/// </summary>
|
||||
/// <param name="stack">Byte array to search within</param>
|
||||
/// <param name="needle">Byte array representing the search value</param>
|
||||
/// <param name="start">Optional starting position in the stack, defaults to 0</param>
|
||||
/// <param name="end">Optional ending position in the stack, defaults to -1 (length of stack)</param>
|
||||
public static int LastPosition(this byte[] stack, byte[] needle, int start = 0, int end = -1)
|
||||
{
|
||||
byte?[] nullableNeedle = Array.ConvertAll(needle, b => (byte?)b);
|
||||
return LastPosition(stack, nullableNeedle, start, end);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Find the last position of one array in another, if possible
|
||||
/// </summary>
|
||||
/// <param name="stack">Byte array to search within</param>
|
||||
/// <param name="needle">Byte array representing the search value</param>
|
||||
/// <param name="start">Optional starting position in the stack, defaults to 0</param>
|
||||
/// <param name="end">Optional ending position in the stack, defaults to -1 (length of stack)</param>
|
||||
public static int LastPosition(this byte[] stack, byte?[] needle, int start = 0, int end = -1)
|
||||
{
|
||||
// If either set is null or empty
|
||||
if (stack.Length == 0 || needle.Length == 0)
|
||||
return -1;
|
||||
|
||||
// If the needle is longer than the stack
|
||||
if (needle.Length > stack.Length)
|
||||
return -1;
|
||||
|
||||
var matcher = new ContentMatch(needle, start, end);
|
||||
return matcher.Match(stack, reverse: true);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Check if a byte array exactly matches another
|
||||
/// </summary>
|
||||
/// <param name="stack">Byte array to search within</param>
|
||||
/// <param name="needle">Byte array representing the search value</param>
|
||||
public static bool EqualsExactly(this byte[] stack, byte[] needle)
|
||||
{
|
||||
byte?[] nullableNeedle = Array.ConvertAll(needle, b => (byte?)b);
|
||||
return EqualsExactly(stack, nullableNeedle);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Check if a byte array exactly matches another
|
||||
/// </summary>
|
||||
/// <param name="stack">Byte array to search within</param>
|
||||
/// <param name="needle">Byte array representing the search value</param>
|
||||
public static bool EqualsExactly(this byte[] stack, byte?[] needle)
|
||||
{
|
||||
// If either set is null or empty
|
||||
if (stack.Length == 0 || needle.Length == 0)
|
||||
return false;
|
||||
|
||||
// If the needle is not the exact length of the stack
|
||||
if (needle.Length != stack.Length)
|
||||
return false;
|
||||
|
||||
return FirstPosition(stack, needle, start: 0, end: 1) == 0;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Check if a byte array starts with another
|
||||
/// </summary>
|
||||
/// <param name="stack">Byte array to search within</param>
|
||||
/// <param name="needle">Byte array representing the search value</param>
|
||||
public static bool StartsWith(this byte[] stack, byte[] needle)
|
||||
{
|
||||
byte?[] nullableNeedle = Array.ConvertAll(needle, b => (byte?)b);
|
||||
return StartsWith(stack, nullableNeedle);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Check if a byte array starts with another
|
||||
/// </summary>
|
||||
/// <param name="stack">Byte array to search within</param>
|
||||
/// <param name="needle">Byte array representing the search value</param>
|
||||
public static bool StartsWith(this byte[] stack, byte?[] needle)
|
||||
{
|
||||
// If either set is null or empty
|
||||
if (stack.Length == 0 || needle.Length == 0)
|
||||
return false;
|
||||
|
||||
// If the needle is longer than the stack
|
||||
if (needle.Length > stack.Length)
|
||||
return false;
|
||||
|
||||
return FirstPosition(stack, needle, start: 0, end: 1) > -1;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Check if a byte array ends with another
|
||||
/// </summary>
|
||||
/// <param name="stack">Byte array to search within</param>
|
||||
/// <param name="needle">Byte array representing the search value</param>
|
||||
public static bool EndsWith(this byte[] stack, byte[] needle)
|
||||
{
|
||||
byte?[] nullableNeedle = Array.ConvertAll(needle, b => (byte?)b);
|
||||
return EndsWith(stack, nullableNeedle);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Check if a byte array ends with another
|
||||
/// </summary>
|
||||
/// <param name="stack">Byte array to search within</param>
|
||||
/// <param name="needle">Byte array representing the search value</param>
|
||||
public static bool EndsWith(this byte[] stack, byte?[] needle)
|
||||
{
|
||||
// If either set is null or empty
|
||||
if (stack.Length == 0 || needle.Length == 0)
|
||||
return false;
|
||||
|
||||
// If the needle is longer than the stack
|
||||
if (needle.Length > stack.Length)
|
||||
return false;
|
||||
|
||||
return FirstPosition(stack, needle, start: stack.Length - needle.Length) > -1;
|
||||
}
|
||||
}
|
||||
}
|
||||
13
SabreTools.Matching/IMatch.cs
Normal file
13
SabreTools.Matching/IMatch.cs
Normal file
@@ -0,0 +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; }
|
||||
}
|
||||
}
|
||||
@@ -5,16 +5,16 @@ namespace SabreTools.Matching
|
||||
/// <summary>
|
||||
/// Wrapper for a single set of matching criteria
|
||||
/// </summary>
|
||||
public abstract class MatchSet<T, U> where T : IMatch<U>
|
||||
public interface IMatchSet<T, U> where T : IMatch<U>
|
||||
{
|
||||
/// <summary>
|
||||
/// Set of all matchers
|
||||
/// </summary>
|
||||
public IEnumerable<T>? Matchers { get; set; }
|
||||
public List<T> Matchers { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Name of the protection to show
|
||||
/// Unique name for the match set
|
||||
/// </summary>
|
||||
public string? ProtectionName { get; set; }
|
||||
public string SetName { get; }
|
||||
}
|
||||
}
|
||||
}
|
||||
364
SabreTools.Matching/MatchUtil.cs
Normal file
364
SabreTools.Matching/MatchUtil.cs
Normal file
@@ -0,0 +1,364 @@
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Text;
|
||||
using SabreTools.Matching.Content;
|
||||
using SabreTools.Matching.Paths;
|
||||
|
||||
namespace SabreTools.Matching
|
||||
{
|
||||
/// <summary>
|
||||
/// Helper class for matching
|
||||
/// </summary>
|
||||
public static class MatchUtil
|
||||
{
|
||||
#region Array Content Matching
|
||||
|
||||
/// <summary>
|
||||
/// Get all content matches for a given list of matchers
|
||||
/// </summary>
|
||||
/// <param name="file">File to check for matches</param>
|
||||
/// <param name="stack">Array to search</param>
|
||||
/// <param name="matchSets">List of ContentMatchSets to be run on the file</param>
|
||||
/// <param name="any">True if any content match is a success, false if all have to match</param>
|
||||
/// <param name="includeDebug">True to include positional data, false otherwise</param>
|
||||
/// <returns>List of strings representing the matches, null or empty otherwise</returns>
|
||||
public static List<string> GetAllMatches(string file,
|
||||
byte[]? stack,
|
||||
List<ContentMatchSet> matchSets,
|
||||
bool any = false,
|
||||
bool includeDebug = false)
|
||||
=> FindAllMatches(file, stack, matchSets, any, includeDebug, false);
|
||||
|
||||
/// <summary>
|
||||
/// Get first content match for a given list of matchers
|
||||
/// </summary>
|
||||
/// <param name="file">File to check for matches</param>
|
||||
/// <param name="stack">Array to search</param>
|
||||
/// <param name="matchSets">List of ContentMatchSets to be run on the file</param>
|
||||
/// <param name="any">True if any content match is a success, false if all have to match</param>
|
||||
/// <param name="includeDebug">True to include positional data, false otherwise</param>
|
||||
/// <returns>String representing the match, null otherwise</returns>
|
||||
public static string? GetFirstMatch(string file,
|
||||
byte[]? stack,
|
||||
List<ContentMatchSet> matchSets,
|
||||
bool any = false,
|
||||
bool includeDebug = false)
|
||||
{
|
||||
var contentMatches = FindAllMatches(file, stack, matchSets, any, includeDebug, true);
|
||||
if (contentMatches == null || contentMatches.Count == 0)
|
||||
return null;
|
||||
|
||||
return contentMatches[0];
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get the required set of content matches on a per Matcher basis
|
||||
/// </summary>
|
||||
/// <param name="file">File to check for matches</param>
|
||||
/// <param name="stack">Array to search</param>
|
||||
/// <param name="matchSets">List of ContentMatchSets to be run on the file</param>
|
||||
/// <param name="any">True if any content match is a success, false if all have to match</param>
|
||||
/// <param name="includeDebug">True to include positional data, false otherwise</param>
|
||||
/// <param name="stopAfterFirst">True to stop after the first match, false otherwise</param>
|
||||
/// <returns>List of strings representing the matches, empty otherwise</returns>
|
||||
private static List<string> FindAllMatches(string file,
|
||||
byte[]? stack,
|
||||
List<ContentMatchSet> matchSets,
|
||||
bool any,
|
||||
bool includeDebug,
|
||||
bool stopAfterFirst)
|
||||
{
|
||||
// If either set is null or empty
|
||||
if (stack == null || stack.Length == 0 || matchSets.Count == 0)
|
||||
return [];
|
||||
|
||||
// Initialize the list of matches
|
||||
var matchesList = new List<string>();
|
||||
|
||||
// Loop through and try everything otherwise
|
||||
foreach (var matcher in matchSets)
|
||||
{
|
||||
// Determine if the matcher passes
|
||||
List<int> positions = any
|
||||
? [matcher.MatchesAny(stack)]
|
||||
: matcher.MatchesAll(stack);
|
||||
|
||||
// If we don't have a pass, just continue
|
||||
if (positions.Count == 0 || positions[0] == -1)
|
||||
continue;
|
||||
|
||||
// Build the output string
|
||||
var matchString = new StringBuilder();
|
||||
matchString.Append(matcher.SetName);
|
||||
|
||||
// Invoke the version delegate, if it exists
|
||||
if (matcher.GetArrayVersion != null)
|
||||
{
|
||||
// A null version returned means the check didn't pass at the version step
|
||||
var version = matcher.GetArrayVersion(file, stack, positions);
|
||||
if (version == null)
|
||||
continue;
|
||||
|
||||
// Trim and add the version
|
||||
version = version.Trim();
|
||||
if (version.Length > 0)
|
||||
matchString.Append($" {version}");
|
||||
}
|
||||
|
||||
// Append the positional data if required
|
||||
if (includeDebug)
|
||||
{
|
||||
string positionsString = string.Join(", ", [.. positions.ConvertAll(p => p.ToString())]);
|
||||
matchString.Append($" (Index {positionsString})");
|
||||
}
|
||||
|
||||
// Append the match to the list
|
||||
matchesList.Add(matchString.ToString());
|
||||
|
||||
// If we're stopping after the first match, bail out here
|
||||
if (stopAfterFirst)
|
||||
return matchesList;
|
||||
}
|
||||
|
||||
return matchesList;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Stream Content Matching
|
||||
|
||||
/// <summary>
|
||||
/// Get all content matches for a given list of matchers
|
||||
/// </summary>
|
||||
/// <param name="file">File to check for matches</param>
|
||||
/// <param name="stack">Stream to search</param>
|
||||
/// <param name="matchSets">List of ContentMatchSets to be run on the file</param>
|
||||
/// <param name="any">True if any content match is a success, false if all have to match</param>
|
||||
/// <param name="includeDebug">True to include positional data, false otherwise</param>
|
||||
/// <returns>List of strings representing the matches, null or empty otherwise</returns>
|
||||
public static List<string> GetAllMatches(string file,
|
||||
Stream? stack,
|
||||
List<ContentMatchSet> matchSets,
|
||||
bool any = false,
|
||||
bool includeDebug = false)
|
||||
=> FindAllMatches(file, stack, matchSets, any, includeDebug, false);
|
||||
|
||||
/// <summary>
|
||||
/// Get first content match for a given list of matchers
|
||||
/// </summary>
|
||||
/// <param name="file">File to check for matches</param>
|
||||
/// <param name="stack">Stream to search</param>
|
||||
/// <param name="matchSets">List of ContentMatchSets to be run on the file</param>
|
||||
/// <param name="any">True if any content match is a success, false if all have to match</param>
|
||||
/// <param name="includeDebug">True to include positional data, false otherwise</param>
|
||||
/// <returns>String representing the match, null otherwise</returns>
|
||||
public static string? GetFirstMatch(string file,
|
||||
Stream? stack,
|
||||
List<ContentMatchSet> matchSets,
|
||||
bool any = false,
|
||||
bool includeDebug = false)
|
||||
{
|
||||
var contentMatches = FindAllMatches(file, stack, matchSets, any, includeDebug, true);
|
||||
if (contentMatches == null || contentMatches.Count == 0)
|
||||
return null;
|
||||
|
||||
return contentMatches[0];
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get the required set of content matches on a per Matcher basis
|
||||
/// </summary>
|
||||
/// <param name="file">File to check for matches</param>
|
||||
/// <param name="stack">Stream to search</param>
|
||||
/// <param name="matchSets">List of ContentMatchSets to be run on the file</param>
|
||||
/// <param name="any">True if any content match is a success, false if all have to match</param>
|
||||
/// <param name="includeDebug">True to include positional data, false otherwise</param>
|
||||
/// <param name="stopAfterFirst">True to stop after the first match, false otherwise</param>
|
||||
/// <returns>List of strings representing the matches, empty otherwise</returns>
|
||||
private static List<string> FindAllMatches(string file,
|
||||
Stream? stack,
|
||||
List<ContentMatchSet> matchSets,
|
||||
bool any,
|
||||
bool includeDebug,
|
||||
bool stopAfterFirst)
|
||||
{
|
||||
// If either set is null or empty
|
||||
if (stack == null || stack.Length == 0 || matchSets.Count == 0)
|
||||
return [];
|
||||
|
||||
// Initialize the list of matches
|
||||
var matchesList = new List<string>();
|
||||
|
||||
// Loop through and try everything otherwise
|
||||
foreach (var matcher in matchSets)
|
||||
{
|
||||
// Determine if the matcher passes
|
||||
List<int> positions = any
|
||||
? [matcher.MatchesAny(stack)]
|
||||
: matcher.MatchesAll(stack);
|
||||
|
||||
// If we don't have a pass, just continue
|
||||
if (positions.Count == 0 || positions[0] == -1)
|
||||
continue;
|
||||
|
||||
// Build the output string
|
||||
var matchString = new StringBuilder();
|
||||
matchString.Append(matcher.SetName);
|
||||
|
||||
// Invoke the version delegate, if it exists
|
||||
if (matcher.GetStreamVersion != null)
|
||||
{
|
||||
// A null version returned means the check didn't pass at the version step
|
||||
var version = matcher.GetStreamVersion(file, stack, positions);
|
||||
if (version == null)
|
||||
continue;
|
||||
|
||||
// Trim and add the version
|
||||
version = version.Trim();
|
||||
if (version.Length > 0)
|
||||
matchString.Append($" {version}");
|
||||
}
|
||||
|
||||
// Append the positional data if required
|
||||
if (includeDebug)
|
||||
{
|
||||
string positionsString = string.Join(", ", [.. positions.ConvertAll(p => p.ToString())]);
|
||||
matchString.Append($" (Index {positionsString})");
|
||||
}
|
||||
|
||||
// Append the match to the list
|
||||
matchesList.Add(matchString.ToString());
|
||||
|
||||
// If we're stopping after the first match, bail out here
|
||||
if (stopAfterFirst)
|
||||
return matchesList;
|
||||
}
|
||||
|
||||
return matchesList;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Path Matching
|
||||
|
||||
/// <summary>
|
||||
/// Get all path matches for a given list of matchers
|
||||
/// </summary>
|
||||
/// <param name="stack">File path to check for matches</param>
|
||||
/// <param name="matchSets">List of PathMatchSets to be run on the file</param>
|
||||
/// <param name="any">True if any path match is a success, false if all have to match</param>
|
||||
/// <returns>List of strings representing the matches, null or empty otherwise</returns>
|
||||
public static List<string> GetAllMatches(string stack, List<PathMatchSet> matchSets, bool any = false)
|
||||
=> FindAllMatches([stack], matchSets, any, false);
|
||||
|
||||
/// <summary>
|
||||
/// Get all path matches for a given list of matchers
|
||||
/// </summary>
|
||||
/// <param name="files">File paths to check for matches</param>
|
||||
/// <param name="matchSets">List of PathMatchSets to be run on the file</param>
|
||||
/// <param name="any">True if any path match is a success, false if all have to match</param>
|
||||
/// <returns>List of strings representing the matches, null or empty otherwise</returns>
|
||||
public static List<string> GetAllMatches(List<string>? stack, List<PathMatchSet> matchSets, bool any = false)
|
||||
=> FindAllMatches(stack, matchSets, any, false);
|
||||
|
||||
/// <summary>
|
||||
/// Get first path match for a given list of matchers
|
||||
/// </summary>
|
||||
/// <param name="stack">File path to check for matches</param>
|
||||
/// <param name="matchSets">List of PathMatchSets to be run on the file</param>
|
||||
/// <param name="any">True if any path match is a success, false if all have to match</param>
|
||||
/// <returns>String representing the match, null otherwise</returns>
|
||||
public static string? GetFirstMatch(string stack, List<PathMatchSet> matchSets, bool any = false)
|
||||
{
|
||||
var contentMatches = FindAllMatches([stack], matchSets, any, true);
|
||||
if (contentMatches == null || contentMatches.Count == 0)
|
||||
return null;
|
||||
|
||||
return contentMatches[0];
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get first path match for a given list of matchers
|
||||
/// </summary>
|
||||
/// <param name="stack">File paths to check for matches</param>
|
||||
/// <param name="matchSets">List of PathMatchSets to be run on the file</param>
|
||||
/// <param name="any">True if any path match is a success, false if all have to match</param>
|
||||
/// <returns>String representing the match, null otherwise</returns>
|
||||
public static string? GetFirstMatch(List<string> stack, List<PathMatchSet> matchSets, bool any = false)
|
||||
{
|
||||
var contentMatches = FindAllMatches(stack, matchSets, any, true);
|
||||
if (contentMatches == null || contentMatches.Count == 0)
|
||||
return null;
|
||||
|
||||
return contentMatches[0];
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get the required set of path matches on a per Matcher basis
|
||||
/// </summary>
|
||||
/// <param name="stack">File paths to check for matches</param>
|
||||
/// <param name="matchSets">List of PathMatchSets to be run on the file</param>
|
||||
/// <param name="any">True if any path match is a success, false if all have to match</param>
|
||||
/// <param name="stopAfterFirst">True to stop after the first match, false otherwise</param>
|
||||
/// <returns>List of strings representing the matches, null or empty otherwise</returns>
|
||||
private static List<string> FindAllMatches(List<string>? stack, List<PathMatchSet> matchSets, bool any, bool stopAfterFirst)
|
||||
{
|
||||
// If either set is null or empty
|
||||
if (stack == null || stack.Count == 0 || matchSets.Count == 0)
|
||||
return [];
|
||||
|
||||
// Initialize the list of matches
|
||||
var matchesList = new List<string>();
|
||||
|
||||
// Loop through and try everything otherwise
|
||||
foreach (var matcher in matchSets)
|
||||
{
|
||||
// Determine if the matcher passes
|
||||
List<string> matches = [];
|
||||
if (any)
|
||||
{
|
||||
string? anyMatch = matcher.MatchesAny(stack);
|
||||
if (anyMatch != null)
|
||||
matches = [anyMatch];
|
||||
}
|
||||
else
|
||||
{
|
||||
matches = matcher.MatchesAll(stack);
|
||||
}
|
||||
|
||||
// If we don't have a pass, just continue
|
||||
if (matches.Count == 0)
|
||||
continue;
|
||||
|
||||
// Build the output string
|
||||
var matchString = new StringBuilder();
|
||||
matchString.Append(matcher.SetName);
|
||||
|
||||
// Invoke the version delegate, if it exists
|
||||
if (matcher.GetVersion != null)
|
||||
{
|
||||
// A null version returned means the check didn't pass at the version step
|
||||
var version = matcher.GetVersion(matches[0], stack);
|
||||
if (version == null)
|
||||
continue;
|
||||
|
||||
// Trim and add the version
|
||||
version = version.Trim();
|
||||
if (version.Length > 0)
|
||||
matchString.Append($" {version}");
|
||||
}
|
||||
|
||||
// Append the match to the list
|
||||
matchesList.Add(matchString.ToString());
|
||||
|
||||
// If we're stopping after the first match, bail out here
|
||||
if (stopAfterFirst)
|
||||
return matchesList;
|
||||
}
|
||||
|
||||
return matchesList;
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
using System.IO;
|
||||
|
||||
namespace SabreTools.Matching
|
||||
namespace SabreTools.Matching.Paths
|
||||
{
|
||||
/// <summary>
|
||||
/// File path matching criteria
|
||||
@@ -11,6 +11,8 @@ namespace SabreTools.Matching
|
||||
/// 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) { }
|
||||
}
|
||||
}
|
||||
}
|
||||
92
SabreTools.Matching/Paths/PathMatch.cs
Normal file
92
SabreTools.Matching/Paths/PathMatch.cs
Normal file
@@ -0,0 +1,92 @@
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
|
||||
namespace SabreTools.Matching.Paths
|
||||
{
|
||||
/// <summary>
|
||||
/// Path matching criteria
|
||||
/// </summary>
|
||||
public class PathMatch : IMatch<string>
|
||||
{
|
||||
/// <summary>
|
||||
/// String to match
|
||||
/// </summary>
|
||||
public string Needle { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Match casing instead of invariant
|
||||
/// </summary>
|
||||
private readonly bool _matchCase;
|
||||
|
||||
/// <summary>
|
||||
/// Match that values end with the needle and not just contains
|
||||
/// </summary>
|
||||
private readonly bool _useEndsWith;
|
||||
|
||||
/// <summary>
|
||||
/// Constructor
|
||||
/// </summary>
|
||||
/// <param name="needle">String representing the search</param>
|
||||
/// <param name="matchCase">True to match exact casing, false otherwise</param>
|
||||
/// <param name="useEndsWith">True to match the end only, false for contains</param>
|
||||
public PathMatch(string needle, bool matchCase = false, bool useEndsWith = false)
|
||||
{
|
||||
// Validate the inputs
|
||||
if (needle.Length == 0)
|
||||
throw new InvalidDataException(nameof(needle));
|
||||
|
||||
Needle = needle;
|
||||
_matchCase = matchCase;
|
||||
_useEndsWith = useEndsWith;
|
||||
}
|
||||
|
||||
#region Conversion
|
||||
|
||||
/// <summary>
|
||||
/// Allow conversion from string to PathMatch
|
||||
/// </summary>
|
||||
public static implicit operator PathMatch(string needle) => new PathMatch(needle);
|
||||
|
||||
#endregion
|
||||
|
||||
#region Matching
|
||||
|
||||
/// <summary>
|
||||
/// Get if this match can be found in a stack
|
||||
/// </summary>
|
||||
/// <param name="stack">Array of strings to search for the given content</param>
|
||||
/// <returns>Matched item on success, null on error</returns>
|
||||
public string? Match(string[]? stack)
|
||||
=> Match(stack == null ? null : new List<string>(stack));
|
||||
|
||||
/// <summary>
|
||||
/// Get if this match can be found in a stack
|
||||
/// </summary>
|
||||
/// <param name="stack">List of strings to search for the given content</param>
|
||||
/// <returns>Matched item on success, null on error</returns>
|
||||
public string? Match(List<string>? stack)
|
||||
{
|
||||
// If either set is null or empty
|
||||
if (stack == null || stack.Count == 0 || Needle.Length == 0)
|
||||
return null;
|
||||
|
||||
// Preprocess the needle, if necessary
|
||||
string procNeedle = _matchCase ? Needle : Needle.ToLowerInvariant();
|
||||
|
||||
foreach (string stackItem in stack)
|
||||
{
|
||||
// Preprocess the stack item, if necessary
|
||||
string procStackItem = _matchCase ? stackItem : stackItem.ToLowerInvariant();
|
||||
|
||||
if (_useEndsWith && procStackItem.EndsWith(procNeedle))
|
||||
return stackItem;
|
||||
else if (!_useEndsWith && procStackItem.Contains(procNeedle))
|
||||
return stackItem;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
155
SabreTools.Matching/Paths/PathMatchSet.cs
Normal file
155
SabreTools.Matching/Paths/PathMatchSet.cs
Normal file
@@ -0,0 +1,155 @@
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
|
||||
namespace SabreTools.Matching.Paths
|
||||
{
|
||||
/// <summary>
|
||||
/// A set of path matches that work together
|
||||
/// </summary>
|
||||
public class PathMatchSet : IMatchSet<PathMatch, string>
|
||||
{
|
||||
/// <inheritdoc/>
|
||||
public List<PathMatch> Matchers { get; }
|
||||
|
||||
/// <inheritdoc/>
|
||||
public string SetName { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Function to get a path version for this Matcher
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// A path version method takes the matched path and an enumerable of files
|
||||
/// and returns a single string. That string is either a version string,
|
||||
/// in which case it will be appended to the match name, or `null`,
|
||||
/// in which case it will cause the match name to be omitted.
|
||||
/// </remarks>
|
||||
public GetPathVersion? GetVersion { get; }
|
||||
|
||||
#region Generic Constructors
|
||||
|
||||
/// <summary>
|
||||
/// Constructor
|
||||
/// </summary>
|
||||
/// <param name="needle">PathMatch representing the comparisons</param>
|
||||
/// <param name="setName">Unique name for the set</param>
|
||||
public PathMatchSet(PathMatch needle, string setName)
|
||||
: this([needle], setName) { }
|
||||
|
||||
/// <summary>
|
||||
/// Constructor
|
||||
/// </summary>
|
||||
/// <param name="needles">List of PathMatch objects representing the comparisons</param>
|
||||
/// <param name="setName">Unique name for the set</param>
|
||||
public PathMatchSet(List<PathMatch> needles, string setName)
|
||||
{
|
||||
// Validate the inputs
|
||||
if (needles.Count == 0)
|
||||
throw new InvalidDataException(nameof(needles));
|
||||
|
||||
Matchers = needles;
|
||||
SetName = setName;
|
||||
GetVersion = null;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Version Constructors
|
||||
|
||||
/// <summary>
|
||||
/// Constructor
|
||||
/// </summary>
|
||||
/// <param name="needle">PathMatch representing the comparisons</param>
|
||||
/// <param name="getVersion">Delegate for deriving a version on match</param>
|
||||
/// <param name="setName">Unique name for the set</param>
|
||||
public PathMatchSet(PathMatch needle, GetPathVersion getVersion, string setName)
|
||||
: this([needle], getVersion, setName) { }
|
||||
|
||||
/// <summary>
|
||||
/// Constructor
|
||||
/// </summary>
|
||||
/// <param name="needles">List of PathMatch objects representing the comparisons</param>
|
||||
/// <param name="getVersion">Delegate for deriving a version on match</param>
|
||||
/// <param name="setName">Unique name for the set</param>
|
||||
public PathMatchSet(List<PathMatch> needles, GetPathVersion getVersion, string setName)
|
||||
{
|
||||
// Validate the inputs
|
||||
if (needles.Count == 0)
|
||||
throw new InvalidDataException(nameof(needles));
|
||||
|
||||
Matchers = needles;
|
||||
SetName = setName;
|
||||
GetVersion = getVersion;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Matching
|
||||
|
||||
/// <summary>
|
||||
/// Get if this match can be found in a stack
|
||||
/// </summary>
|
||||
/// <param name="stack">List of strings to search for the given content</param>
|
||||
/// <returns>Matched item on success, null on error</returns>
|
||||
public List<string> MatchesAll(string[]? stack)
|
||||
=> MatchesAll(stack == null ? null : new List<string>(stack));
|
||||
|
||||
/// <summary>
|
||||
/// Determine whether all path matches pass
|
||||
/// </summary>
|
||||
/// <param name="stack">List of strings to try to match</param>
|
||||
/// <returns>List of matching values, if any</returns>
|
||||
public List<string> MatchesAll(List<string>? stack)
|
||||
{
|
||||
// If either set is null or empty, we can't do anything
|
||||
if (stack == null || stack.Count == 0 || Matchers.Count == 0)
|
||||
return [];
|
||||
|
||||
// Initialize the value list
|
||||
List<string> values = [];
|
||||
|
||||
// Loop through all path matches and make sure all pass
|
||||
foreach (var pathMatch in Matchers)
|
||||
{
|
||||
string? value = pathMatch.Match(stack);
|
||||
if (value == null)
|
||||
return [];
|
||||
else
|
||||
values.Add(value);
|
||||
}
|
||||
|
||||
return values;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get if this match can be found in a stack
|
||||
/// </summary>
|
||||
/// <param name="stack">List of strings to search for the given content</param>
|
||||
/// <returns>Matched item on success, null on error</returns>
|
||||
public string? MatchesAny(string[]? stack)
|
||||
=> MatchesAny(stack == null ? null : new List<string>(stack));
|
||||
|
||||
/// <summary>
|
||||
/// Determine whether any path matches pass
|
||||
/// </summary>
|
||||
/// <param name="stack">List of strings to try to match</param>
|
||||
/// <returns>First matching value on success, null on error</returns>
|
||||
public string? MatchesAny(List<string>? stack)
|
||||
{
|
||||
// If either set is null or empty, we can't do anything
|
||||
if (stack == null || stack.Count == 0 || Matchers.Count == 0)
|
||||
return null;
|
||||
|
||||
// Loop through all path matches and make sure all pass
|
||||
foreach (var pathMatch in Matchers)
|
||||
{
|
||||
string? value = pathMatch.Match(stack);
|
||||
if (value != null)
|
||||
return value;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
34
SabreTools.Matching/SabreTools.Matching.csproj
Normal file
34
SabreTools.Matching/SabreTools.Matching.csproj
Normal file
@@ -0,0 +1,34 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<!-- Assembly Properties -->
|
||||
<TargetFrameworks>net20;net35;net40;net452;net462;net472;net48;netcoreapp3.1;net5.0;net6.0;net7.0;net8.0;net9.0;netstandard2.0;netstandard2.1</TargetFrameworks>
|
||||
<IncludeSymbols>true</IncludeSymbols>
|
||||
<LangVersion>latest</LangVersion>
|
||||
<NoWarn>NU1903</NoWarn>
|
||||
<Nullable>enable</Nullable>
|
||||
<SymbolPackageFormat>snupkg</SymbolPackageFormat>
|
||||
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
|
||||
<Version>1.6.0</Version>
|
||||
|
||||
<!-- Package Properties -->
|
||||
<Authors>Matt Nadareski</Authors>
|
||||
<Description>Byte array and stream matching library</Description>
|
||||
<Copyright>Copyright (c) Matt Nadareski 2018-2025</Copyright>
|
||||
<PackageProjectUrl>https://github.com/SabreTools/</PackageProjectUrl>
|
||||
<PackageReadmeFile>README.md</PackageReadmeFile>
|
||||
<RepositoryUrl>https://github.com/SabreTools/SabreTools.Matching</RepositoryUrl>
|
||||
<RepositoryType>git</RepositoryType>
|
||||
<PackageTags>byte array stream match matching</PackageTags>
|
||||
<PackageLicenseExpression>MIT</PackageLicenseExpression>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<InternalsVisibleTo Include="SabreTools.Matching.Test" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<None Include="../README.md" Pack="true" PackagePath="" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
36
publish-nix.sh
Executable file
36
publish-nix.sh
Executable file
@@ -0,0 +1,36 @@
|
||||
#! /bin/bash
|
||||
|
||||
# This batch file assumes the following:
|
||||
# - .NET 9.0 (or newer) SDK is installed and in PATH
|
||||
#
|
||||
# If any of these are not satisfied, the operation may fail
|
||||
# in an unpredictable way and result in an incomplete output.
|
||||
|
||||
# Optional parameters
|
||||
NO_BUILD=false
|
||||
while getopts "b" OPTION
|
||||
do
|
||||
case $OPTION in
|
||||
b)
|
||||
NO_BUILD=true
|
||||
;;
|
||||
*)
|
||||
echo "Invalid option provided"
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
# Set the current directory as a variable
|
||||
BUILD_FOLDER=$PWD
|
||||
|
||||
# Only build if requested
|
||||
if [ $NO_BUILD = false ]
|
||||
then
|
||||
# Restore Nuget packages for all builds
|
||||
echo "Restoring Nuget packages"
|
||||
dotnet restore
|
||||
|
||||
# Create Nuget Package
|
||||
dotnet pack SabreTools.Matching/SabreTools.Matching.csproj --output $BUILD_FOLDER
|
||||
fi
|
||||
26
publish-win.ps1
Normal file
26
publish-win.ps1
Normal file
@@ -0,0 +1,26 @@
|
||||
# This batch file assumes the following:
|
||||
# - .NET 9.0 (or newer) SDK is installed and in PATH
|
||||
#
|
||||
# If any of these are not satisfied, the operation may fail
|
||||
# in an unpredictable way and result in an incomplete output.
|
||||
|
||||
# Optional parameters
|
||||
param(
|
||||
[Parameter(Mandatory = $false)]
|
||||
[Alias("NoBuild")]
|
||||
[switch]$NO_BUILD
|
||||
)
|
||||
|
||||
# Set the current directory as a variable
|
||||
$BUILD_FOLDER = $PSScriptRoot
|
||||
|
||||
# Only build if requested
|
||||
if (!$NO_BUILD.IsPresent)
|
||||
{
|
||||
# Restore Nuget packages for all builds
|
||||
Write-Host "Restoring Nuget packages"
|
||||
dotnet restore
|
||||
|
||||
# Create Nuget Package
|
||||
dotnet pack SabreTools.Matching\SabreTools.Matching.csproj --output $BUILD_FOLDER
|
||||
}
|
||||
Reference in New Issue
Block a user