mirror of
https://github.com/SabreTools/SabreTools.Matching.git
synced 2026-02-05 13:49:48 +00:00
Compare commits
86 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
77280e8da1 | ||
|
|
3d05135a81 | ||
|
|
b2dfffbc92 | ||
|
|
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 | ||
|
|
85bc696ba9 | ||
|
|
8fcce5b450 |
40
.github/workflows/build_and_test.yml
vendored
Normal file
40
.github/workflows/build_and_test.yml
vendored
Normal file
@@ -0,0 +1,40 @@
|
||||
name: Build and Test
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ "main" ]
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
submodules: recursive
|
||||
|
||||
- name: Setup .NET
|
||||
uses: actions/setup-dotnet@v4
|
||||
with:
|
||||
dotnet-version: |
|
||||
6.0.x
|
||||
8.0.x
|
||||
9.0.x
|
||||
|
||||
- name: Run tests
|
||||
run: dotnet test
|
||||
|
||||
- name: Run publish script
|
||||
run: ./publish-nix.sh
|
||||
|
||||
- name: Upload to rolling
|
||||
uses: ncipollo/release-action@v1.14.0
|
||||
with:
|
||||
allowUpdates: True
|
||||
artifacts: "*.nupkg,*.snupkg"
|
||||
body: 'Last built commit: ${{ github.sha }}'
|
||||
name: 'Rolling Release'
|
||||
prerelease: True
|
||||
replacesArtifacts: True
|
||||
tag: "rolling"
|
||||
updateOnlyUnreleased: True
|
||||
23
.github/workflows/check_pr.yml
vendored
Normal file
23
.github/workflows/check_pr.yml
vendored
Normal file
@@ -0,0 +1,23 @@
|
||||
name: Build PR
|
||||
|
||||
on: [pull_request]
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Setup .NET
|
||||
uses: actions/setup-dotnet@v4
|
||||
with:
|
||||
dotnet-version: |
|
||||
6.0.x
|
||||
8.0.x
|
||||
9.0.x
|
||||
|
||||
- name: 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; }
|
||||
}
|
||||
}
|
||||
7
LICENSE
Normal file
7
LICENSE
Normal file
@@ -0,0 +1,7 @@
|
||||
Copyright (c) 2018-2025 Matt Nadareski
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice (including the next paragraph) shall be included in all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
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
|
||||
}
|
||||
}
|
||||
10
README.MD
10
README.MD
@@ -1,5 +1,15 @@
|
||||
# SabreTools.Matching
|
||||
|
||||
[](https://github.com/SabreTools/SabreTools.Matching/actions/workflows/build_and_test.yml)
|
||||
|
||||
**NOTICE:** This library has been deprecated. All functionality formerly in this library is in [SabreTools.IO](https://github.com/SabreTools/SabreTools.IO) as of version 1.7.5.
|
||||
|
||||
This library comprises of code to perform search operations on both byte arrays and streams. There is also an implementation that allows searching for strings in a set of strings, usually used in the context of directory contents.
|
||||
|
||||
Find the link to the Nuget package [here](https://www.nuget.org/packages/SabreTools.Matching).
|
||||
|
||||
## Releases
|
||||
|
||||
For the most recent stable build, download the latest release here: [Releases Page](https://github.com/SabreTools/SabreTools.Matching/releases)
|
||||
|
||||
For the latest WIP build here: [Rolling Release](https://github.com/SabreTools/SabreTools.Matching/releases/rolling)
|
||||
|
||||
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-2023</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