27 Commits

Author SHA1 Message Date
Matt Nadareski
655214503f Bump version 2024-11-05 20:22:40 -05:00
Matt Nadareski
5801424db7 Remove all external package requirements 2024-11-05 20:21:27 -05:00
Matt Nadareski
afb9ab3e4e Define delegates instead of anonymous funcs 2024-11-05 20:10:33 -05:00
Matt Nadareski
85006a71bf Replace Linq calls with cheaper methods 2024-11-05 20:01:59 -05:00
Matt Nadareski
90bb82306b Properly support all frameworks 2024-11-05 19:48:08 -05:00
Matt Nadareski
c38e10e55b Simplify enumerable types 2024-11-04 11:38:46 -05:00
Matt Nadareski
e96790d875 Bump version 2024-10-26 19:38:35 -04:00
Matt Nadareski
bf23967f74 Remove mentions of protection 2024-10-26 00:27:51 -04:00
Matt Nadareski
8debf24a44 Fix comparer tests 2024-10-26 00:26:07 -04:00
Matt Nadareski
333b5be76c Various cleanup 2024-10-26 00:21:36 -04:00
Matt Nadareski
b0fdff2d6e Find and fix equal size issue in streams 2024-10-26 00:14:29 -04:00
Matt Nadareski
3b0e74b9f4 Find and fix equal size issue 2024-10-26 00:04:35 -04:00
Matt Nadareski
883dd00e8f Add publish scripts 2024-10-01 12:47:11 -04:00
Matt Nadareski
aa8506e0ec Bump version 2024-10-01 12:45:15 -04:00
Matt Nadareski
e25fb02f03 Reduce usage of LinqBridge package 2024-10-01 03:13:46 -04:00
Matt Nadareski
aa16a83426 Remove Linq requirement from old .NET 2024-10-01 03:12:00 -04:00
Matt Nadareski
cbf6523a8f Remove ValueTuple requirement 2024-10-01 02:47:16 -04:00
Matt Nadareski
e947687f42 Update MinValueTupleBridge to 0.2.1 2024-09-25 10:49:40 -04:00
Matt Nadareski
b4510f00e1 Sync test project formatting 2024-04-22 00:24:06 -04:00
Matt Nadareski
058923a39d Add comparison tests 2024-03-05 12:14:55 -05:00
Matt Nadareski
113e8c9151 Add skeleton test project 2024-03-05 11:51:55 -05:00
Matt Nadareski
cb3a6f8992 Move library to subfolder to prepare for tests 2024-03-05 11:49:54 -05:00
Matt Nadareski
5a3de8e124 Move some classes to subfolders 2024-03-05 11:48:29 -05:00
Matt Nadareski
08ef69e7c1 Bump version 2024-02-29 20:32:12 -05:00
Matt Nadareski
19971ec62c Combine ArrayExtensions into Extensions 2024-02-29 00:31:07 -05:00
Matt Nadareski
4650399bc1 Migrate ArrayExtensions from SabreTools.Core 2024-02-29 00:23:57 -05:00
Matt Nadareski
aad97d03fd Port NaturalSort classes from SabreTools 2024-02-28 19:54:50 -05:00
26 changed files with 1222 additions and 645 deletions

View File

@@ -28,13 +28,13 @@ jobs:
uses: actions/upload-artifact@v4
with:
name: 'Nuget Package'
path: 'bin/Release/*.nupkg'
path: 'SabreTools.Matching/bin/Release/*.nupkg'
- name: Upload to rolling
uses: ncipollo/release-action@v1.14.0
with:
allowUpdates: True
artifacts: 'bin/Release/*.nupkg'
artifacts: 'SabreTools.Matching/bin/Release/*.nupkg'
body: 'Last built commit: ${{ github.sha }}'
name: 'Rolling Release'
prerelease: True

View File

@@ -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
}
}

View File

@@ -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);
}
}
}

View File

@@ -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
}
}

View 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 NaturalComparerListSortTest()
{
// 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 NaturalReversedComparerListSortTest()
{
// 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));
}
}
}

View File

@@ -0,0 +1,59 @@
using SabreTools.Matching.Compare;
using Xunit;
namespace SabreTools.Matching.Test.Compare
{
public class NaturalComparerUtilTests
{
[Fact]
public void CompareNumericBothNullTest()
{
int actual = NaturalComparerUtil.CompareNumeric(null, null);
Assert.Equal(0, actual);
}
[Fact]
public void CompareNumericSingleNullTest()
{
int actual = NaturalComparerUtil.CompareNumeric(null, "notnull");
Assert.Equal(-1, actual);
actual = NaturalComparerUtil.CompareNumeric("notnull", null);
Assert.Equal(1, actual);
}
[Fact]
public void CompareNumericBothEqualTest()
{
int actual = NaturalComparerUtil.CompareNumeric("notnull", "notnull");
Assert.Equal(0, actual);
}
[Fact]
public void CompareNumericBothEqualWithPathTest()
{
int actual = NaturalComparerUtil.CompareNumeric("notnull/file.ext", "notnull/file.ext");
Assert.Equal(0, actual);
}
[Fact]
public void CompareNumericNumericNonDecimalStringTest()
{
int actual = NaturalComparerUtil.CompareNumeric("100", "10");
Assert.Equal(1, actual);
actual = NaturalComparerUtil.CompareNumeric("10", "100");
Assert.Equal(-1, actual);
}
[Fact]
public void CompareNumericNumericDecimalStringTest()
{
int actual = NaturalComparerUtil.CompareNumeric("100.100", "100.10");
Assert.Equal(1, actual);
actual = NaturalComparerUtil.CompareNumeric("100.10", "100.100");
Assert.Equal(-1, actual);
}
}
}

View File

@@ -0,0 +1,44 @@
using System.Collections.Generic;
using System.IO;
using SabreTools.Matching.Content;
using Xunit;
namespace SabreTools.Matching.Test
{
public class MatchUtilTests
{
[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);
Assert.Equal(expected, actual);
}
[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);
Assert.Equal(expected, actual);
}
}
}

View File

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

View File

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

View File

@@ -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

View File

@@ -0,0 +1,92 @@
/*
*
* Links for info and original source code:
*
* https://blog.codinghorror.com/sorting-for-humans-natural-sort-order/
* http://www.codeproject.com/Articles/22517/Natural-Sort-Comparer
*
* Exact code implementation used with permission, originally by motoschifo
*
*/
using System;
using System.Collections.Generic;
using System.Text.RegularExpressions;
namespace SabreTools.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(y.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 x.CompareTo(y);
}
private static int PartCompare(string left, string right)
{
if (!long.TryParse(left, out long x))
return NaturalComparerUtil.CompareNumeric(left, right);
if (!long.TryParse(right, out long y))
return NaturalComparerUtil.CompareNumeric(left, right);
// If we have an equal part, then make sure that "longer" ones are taken into account
if (x.CompareTo(y) == 0)
return left.Length - right.Length;
return x.CompareTo(y);
}
}
}

View File

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

View File

@@ -0,0 +1,92 @@
/*
*
* Links for info and original source code:
*
* https://blog.codinghorror.com/sorting-for-humans-natural-sort-order/
* http://www.codeproject.com/Articles/22517/Natural-Sort-Comparer
*
* Exact code implementation used with permission, originally by motoschifo
*
*/
using System;
using System.Collections.Generic;
using System.Text.RegularExpressions;
namespace SabreTools.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(y.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.CompareNumeric(right, left);
if (!long.TryParse(right, out long y))
return NaturalComparerUtil.CompareNumeric(right, left);
// If we have an equal part, then make sure that "longer" ones are taken into account
if (y.CompareTo(x) == 0)
return right.Length - left.Length;
return y.CompareTo(x);
}
}
}

View File

@@ -1,6 +1,6 @@
using System.IO;
namespace SabreTools.Matching
namespace SabreTools.Matching.Content
{
/// <summary>
/// Content matching criteria
@@ -10,11 +10,7 @@ namespace SabreTools.Matching
/// <summary>
/// Content to match
/// </summary>
#if NETFRAMEWORK || NETCOREAPP
public byte?[]? Needle { get; private set; }
#else
public byte?[]? Needle { get; init; }
#endif
public byte?[]? Needle { get; }
/// <summary>
/// Starting index for matching
@@ -34,9 +30,9 @@ namespace SabreTools.Matching
/// <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;
Needle = needle;
Start = start;
End = end;
}
#region Array Matching
@@ -46,39 +42,43 @@ namespace SabreTools.Matching
/// </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)
/// <returns>Found position on success, -1 on error</returns>
public int Match(byte[]? stack, bool reverse = false)
{
// If either array is null or empty, we can't do anything
if (stack == null || stack.Length == 0 || this.Needle == null || this.Needle.Length == 0)
return (false, -1);
if (stack == null || stack.Length == 0 || Needle == null || Needle.Length == 0)
return -1;
// If the needle array is larger than the stack array, it can't be contained within
if (this.Needle.Length > stack.Length)
return (false, -1);
if (Needle.Length > stack.Length)
return -1;
// If the needle and stack are identically sized, short-circuit
if (Needle.Length == stack.Length)
return EqualAt(stack, 0) ? 0 : -1;
// Set the default start and end values
int start = this.Start;
int end = this.End;
int start = Start;
int end = 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;
end = stack.Length - 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);
return -1;
// Check to see if the values are equal
if (EqualAt(stack, i))
return (true, i);
return i;
}
return (false, -1);
return -1;
}
/// <summary>
@@ -90,7 +90,7 @@ namespace SabreTools.Matching
private bool EqualAt(byte[] stack, int index)
{
// If the needle is invalid, we can't do anything
if (this.Needle == null)
if (Needle == null)
return false;
// If the index is invalid, we can't do anything
@@ -98,16 +98,16 @@ namespace SabreTools.Matching
return false;
// If we're too close to the end of the stack, return false
if (this.Needle.Length > stack.Length - index)
if (Needle.Length > stack.Length - index)
return false;
// Loop through and check the value
for (int i = 0; i < this.Needle.Length; i++)
for (int i = 0; i < Needle.Length; i++)
{
// A null value is a wildcard
if (this.Needle[i] == null)
if (Needle[i] == null)
continue;
else if (stack[i + index] != this.Needle[i])
else if (stack[i + index] != Needle[i])
return false;
}
@@ -123,39 +123,43 @@ namespace SabreTools.Matching
/// </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)
/// <returns>Found position on success, -1 on error</returns>
public int Match(Stream? stack, bool reverse = false)
{
// If either array is null or empty, we can't do anything
if (stack == null || stack.Length == 0 || this.Needle == null || this.Needle.Length == 0)
return (false, -1);
if (stack == null || stack.Length == 0 || Needle == null || Needle.Length == 0)
return -1;
// If the needle array is larger than the stack array, it can't be contained within
if (this.Needle.Length > stack.Length)
return (false, -1);
if (Needle.Length > stack.Length)
return -1;
// If the needle and stack are identically sized, short-circuit
if (Needle.Length == stack.Length)
return EqualAt(stack, 0) ? 0 : -1;
// Set the default start and end values
int start = this.Start;
int end = this.End;
int start = Start;
int end = 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);
end = (int)(stack.Length - 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);
return -1;
// Check to see if the values are equal
if (EqualAt(stack, i))
return (true, i);
return i;
}
return (false, -1);
return -1;
}
/// <summary>
@@ -167,7 +171,7 @@ namespace SabreTools.Matching
private bool EqualAt(Stream stack, int index)
{
// If the needle is invalid, we can't do anything
if (this.Needle == null)
if (Needle == null)
return false;
// If the index is invalid, we can't do anything
@@ -175,7 +179,7 @@ namespace SabreTools.Matching
return false;
// If we're too close to the end of the stack, return false
if (this.Needle.Length > stack.Length - index)
if (Needle.Length > stack.Length - index)
return false;
// Save the current position and move to the index
@@ -186,16 +190,16 @@ namespace SabreTools.Matching
bool matched = true;
// Loop through and check the value
for (int i = 0; i < this.Needle.Length; i++)
for (int i = 0; i < Needle.Length; i++)
{
byte stackValue = (byte)stack.ReadByte();
// A null value is a wildcard
if (this.Needle[i] == null)
if (Needle[i] == null)
{
continue;
}
else if (stackValue != this.Needle[i])
else if (stackValue != Needle[i])
{
matched = false;
break;

View File

@@ -0,0 +1,197 @@
using System.Collections.Generic;
using System.IO;
namespace SabreTools.Matching.Content
{
/// <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 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
public ContentMatchSet(byte?[] needle, string matchName)
: this([needle], getArrayVersion: null, matchName) { }
public ContentMatchSet(List<byte?[]> needles, string matchName)
: this(needles, getArrayVersion: null, matchName) { }
public ContentMatchSet(ContentMatch needle, string matchName)
: this([needle], getArrayVersion: null, matchName) { }
public ContentMatchSet(List<ContentMatch> needles, string matchName)
: this(needles, getArrayVersion: null, matchName) { }
#endregion
#region Array Constructors
public ContentMatchSet(byte?[] needle, GetArrayVersion? getArrayVersion, string matchName)
: this([needle], getArrayVersion, matchName) { }
public ContentMatchSet(List<byte?[]> needles, GetArrayVersion? getArrayVersion, string matchName)
: this(needles.ConvertAll(n => new ContentMatch(n)), getArrayVersion, matchName) { }
public ContentMatchSet(ContentMatch needle, GetArrayVersion? getArrayVersion, string matchName)
: this([needle], getArrayVersion, matchName) { }
public ContentMatchSet(List<ContentMatch> needles, GetArrayVersion? getArrayVersion, string matchName)
{
Matchers = needles;
GetArrayVersion = getArrayVersion;
MatchName = matchName;
}
#endregion
#region Stream Constructors
public ContentMatchSet(byte?[] needle, GetStreamVersion? getStreamVersion, string matchName)
: this([needle], getStreamVersion, matchName) { }
public ContentMatchSet(List<byte?[]> needles, GetStreamVersion? getStreamVersion, string matchName)
: this(needles.ConvertAll(n => new ContentMatch(n)), getStreamVersion, matchName) { }
public ContentMatchSet(ContentMatch needle, GetStreamVersion? getStreamVersion, string matchName)
: this([needle], getStreamVersion, matchName) { }
public ContentMatchSet(List<ContentMatch> needles, GetStreamVersion? getStreamVersion, string matchName)
{
Matchers = needles;
GetStreamVersion = getStreamVersion;
MatchName = matchName;
}
#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 no content matches are defined, we fail out
if (Matchers == null)
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 [];
else
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 no content matches are defined, we fail out
if (Matchers == null)
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 no content matches are defined, we fail out
if (Matchers == null)
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 [];
else
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 no content matches are defined, we fail out
if (Matchers == null)
return -1;
// Loop through all content matches and make sure all pass
foreach (var contentMatch in Matchers)
{
int position = contentMatch.Match(stack);
if (position >= 0)
return position;
}
return -1;
}
#endregion
}
}

View File

@@ -0,0 +1,32 @@
using System;
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, IEnumerable<string>? files);
/// <summary>
/// Get a version number from a file
/// </summary>
/// <param name="path">File path to get the version from</param>
/// <param name="content">Optional file contents as a Stream</param>
/// <param name="positions">List of positions in the Stream that were matched</param>
/// <returns>Version string on success, null on failure</returns>
public delegate string? GetStreamVersion(string path, Stream? content, List<int> positions);
}

View File

@@ -0,0 +1,197 @@
using System;
using System.Collections.Generic;
using SabreTools.Matching.Content;
namespace SabreTools.Matching
{
public static class Extensions
{
/// <summary>
/// Indicates whether the specified array is null or has a length of zero
/// </summary>
#if NET20
public static bool IsNullOrEmpty(Array? array)
#else
public static bool IsNullOrEmpty(this Array? array)
#endif
{
return array == null || array.Length == 0;
}
/// <summary>
/// Find all positions of one array in another, if possible, if possible
/// </summary>
#if NET20
public static List<int> FindAllPositions(byte[] stack, byte?[]? needle, int start = 0, int end = -1)
#else
public static List<int> FindAllPositions(this byte[] stack, byte?[]? needle, int start = 0, int end = -1)
#endif
{
// Get the outgoing list
List<int> positions = [];
// Initialize the loop variables
int lastPosition = start;
var matcher = new ContentMatch(needle, end: end);
// Loop over and get all positions
while (true)
{
matcher.Start = lastPosition;
lastPosition = matcher.Match(stack, false);
if (lastPosition < 0)
break;
positions.Add(lastPosition);
}
return positions;
}
/// <summary>
/// Find the first position of one array in another, if possible
/// </summary>
#if NET20
public static bool FirstPosition(byte[] stack, byte[]? needle, out int position, int start = 0, int end = -1)
#else
public static bool FirstPosition(this byte[] stack, byte[]? needle, out int position, int start = 0, int end = -1)
#endif
{
// Convert the needle to a nullable byte array
byte?[]? nullableNeedle = null;
if (needle != null)
nullableNeedle = Array.ConvertAll(needle, b => (byte?)b);
return FirstPosition(stack, nullableNeedle, out position, start, end);
}
/// <summary>
/// Find the first position of one array in another, if possible
/// </summary>
#if NET20
public static bool FirstPosition(byte[] stack, byte?[]? needle, out int position, int start = 0, int end = -1)
#else
public static bool FirstPosition(this byte[] stack, byte?[]? needle, out int position, int start = 0, int end = -1)
#endif
{
var matcher = new ContentMatch(needle, start, end);
position = matcher.Match(stack, false);
return position >= 0;
}
/// <summary>
/// Find the last position of one array in another, if possible
/// </summary>
#if NET20
public static bool LastPosition(byte[] stack, byte[]? needle, out int position, int start = 0, int end = -1)
#else
public static bool LastPosition(this byte[] stack, byte[]? needle, out int position, int start = 0, int end = -1)
#endif
{
// Convert the needle to a nullable byte array
byte?[]? nullableNeedle = null;
if (needle != null)
nullableNeedle = Array.ConvertAll(needle, b => (byte?)b);
return LastPosition(stack, nullableNeedle, out position, start, end);
}
/// <summary>
/// Find the last position of one array in another, if possible
/// </summary>
#if NET20
public static bool LastPosition(byte[] stack, byte?[]? needle, out int position, int start = 0, int end = -1)
#else
public static bool LastPosition(this byte[] stack, byte?[]? needle, out int position, int start = 0, int end = -1)
#endif
{
var matcher = new ContentMatch(needle, start, end);
position = matcher.Match(stack, true);
return position >= 0;
}
/// <summary>
/// See if a byte array starts with another
/// </summary>
#if NET20
public static bool StartsWith(byte[] stack, byte[]? needle, bool exact = false)
#else
public static bool StartsWith(this byte[] stack, byte[]? needle, bool exact = false)
#endif
{
// If we have any invalid inputs, we return false
if (needle == null
|| stack.Length == 0 || needle.Length == 0
|| needle.Length > stack.Length
|| (exact && stack.Length != needle.Length))
{
return false;
}
return FirstPosition(stack, needle, out int _, start: 0, end: 1);
}
/// <summary>
/// See if a byte array starts with another
/// </summary>
#if NET20
public static bool StartsWith(byte[] stack, byte?[]? needle, bool exact = false)
#else
public static bool StartsWith(this byte[] stack, byte?[]? needle, bool exact = false)
#endif
{
// If we have any invalid inputs, we return false
if (needle == null
|| stack.Length == 0 || needle.Length == 0
|| needle.Length > stack.Length
|| (exact && stack.Length != needle.Length))
{
return false;
}
return FirstPosition(stack, needle, out int _, start: 0, end: 1);
}
/// <summary>
/// See if a byte array ends with another
/// </summary>
#if NET20
public static bool EndsWith(byte[] stack, byte[]? needle, bool exact = false)
#else
public static bool EndsWith(this byte[] stack, byte[]? needle, bool exact = false)
#endif
{
// If we have any invalid inputs, we return false
if (needle == null
|| stack.Length == 0 || needle.Length == 0
|| needle.Length > stack.Length
|| (exact && stack.Length != needle.Length))
{
return false;
}
return FirstPosition(stack, needle, out int _, start: stack.Length - needle.Length);
}
/// <summary>
/// See if a byte array ends with another
/// </summary>
#if NET20
public static bool EndsWith(byte[] stack, byte?[]? needle, bool exact = false)
#else
public static bool EndsWith(this byte[] stack, byte?[]? needle, bool exact = false)
#endif
{
// If we have any invalid inputs, we return false
if (needle == null
|| stack.Length == 0 || needle.Length == 0
|| needle.Length > stack.Length
|| (exact && stack.Length != needle.Length))
{
return false;
}
return FirstPosition(stack, needle, out int _, start: stack.Length - needle.Length);
}
}
}

View File

@@ -13,8 +13,8 @@ namespace SabreTools.Matching
public IEnumerable<T>? Matchers { get; set; }
/// <summary>
/// Name of the protection to show
/// Unique name for the match set
/// </summary>
public string? ProtectionName { get; set; }
public string? MatchName { get; set; }
}
}

View File

@@ -1,9 +1,7 @@
#if NET40_OR_GREATER || NETCOREAPP
using System.Collections.Concurrent;
#endif
using System.Collections.Generic;
using System.IO;
using System.Linq;
using SabreTools.Matching.Content;
using SabreTools.Matching.Paths;
namespace SabreTools.Matching
{
@@ -21,15 +19,9 @@ namespace SabreTools.Matching
/// <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);
}
/// <returns>List of strings representing the matches, null or empty otherwise</returns>
public static List<string>? GetAllMatches(string file, byte[]? stack, IEnumerable<ContentMatchSet>? matchers, bool includeDebug = false)
=> FindAllMatches(file, stack, matchers, includeDebug, false);
/// <summary>
/// Get first content match for a given list of matchers
@@ -38,14 +30,14 @@ namespace SabreTools.Matching
/// <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>
/// <returns>String representing the match, 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())
if (contentMatches == null || contentMatches.Count == 0)
return null;
return contentMatches.First();
return contentMatches[0];
}
/// <summary>
@@ -56,30 +48,22 @@ namespace SabreTools.Matching
/// <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
/// <returns>List of strings representing the matches, empty otherwise</returns>
private static List<string> FindAllMatches(string file, byte[]? stack, IEnumerable<ContentMatchSet>? matchers, bool includeDebug, bool stopAfterFirst)
{
// If there's no mappings, we can't match
if (matchers == null || !matchers.Any())
return null;
if (matchers == null)
return [];
// Initialize the queue of matched protections
#if NET20 || NET35
var matchedProtections = new Queue<string>();
#else
var matchedProtections = new ConcurrentQueue<string>();
#endif
// Initialize the list of matches
var matchesList = new List<string>();
// Loop through and try everything otherwise
foreach (var matcher in matchers)
{
// Determine if the matcher passes
(bool passes, List<int> positions) = matcher.MatchesAll(stack);
if (!passes)
var positions = matcher.MatchesAll(stack);
if (positions.Count == 0)
continue;
// Format the list of all positions found
@@ -94,10 +78,10 @@ namespace SabreTools.Matching
string positionsString = string.Join(", ", positions);
#endif
// If we there is no version method, just return the protection name
// If we there is no version method, just return the match name
if (matcher.GetArrayVersion == null)
{
matchedProtections.Enqueue((matcher.ProtectionName ?? "Unknown Protection") + (includeDebug ? $" (Index {positionsString})" : string.Empty));
matchesList.Add((matcher.MatchName ?? "Unknown") + (includeDebug ? $" (Index {positionsString})" : string.Empty));
}
// Otherwise, invoke the version method
@@ -108,15 +92,15 @@ namespace SabreTools.Matching
if (version == null)
continue;
matchedProtections.Enqueue($"{matcher.ProtectionName ?? "Unknown Protection"} {version}".Trim() + (includeDebug ? $" (Index {positionsString})" : string.Empty));
matchesList.Add($"{matcher.MatchName ?? "Unknown"} {version}".Trim() + (includeDebug ? $" (Index {positionsString})" : string.Empty));
}
// If we're stopping after the first protection, bail out here
// If we're stopping after the first match, bail out here
if (stopAfterFirst)
return matchedProtections;
return matchesList;
}
return matchedProtections;
return matchesList;
}
#endregion
@@ -130,15 +114,9 @@ namespace SabreTools.Matching
/// <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);
}
/// <returns>List of strings representing the matches, null or empty otherwise</returns>
public static List<string>? GetAllMatches(string file, Stream? stack, IEnumerable<ContentMatchSet>? matchers, bool includeDebug = false)
=> FindAllMatches(file, stack, matchers, includeDebug, false);
/// <summary>
/// Get first content match for a given list of matchers
@@ -147,14 +125,14 @@ namespace SabreTools.Matching
/// <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>
/// <returns>String representing the match, 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())
if (contentMatches == null || contentMatches.Count == 0)
return null;
return contentMatches.First();
return contentMatches[0];
}
/// <summary>
@@ -165,30 +143,22 @@ namespace SabreTools.Matching
/// <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
/// <returns>List of strings representing the matches, empty otherwise</returns>
private static List<string> FindAllMatches(string file, Stream? stack, IEnumerable<ContentMatchSet>? matchers, bool includeDebug, bool stopAfterFirst)
{
// If there's no mappings, we can't match
if (matchers == null || !matchers.Any())
return null;
if (matchers == null)
return [];
// Initialize the queue of matched protections
#if NET20 || NET35
var matchedProtections = new Queue<string>();
#else
var matchedProtections = new ConcurrentQueue<string>();
#endif
// Initialize the list of matches
var matchesList = new List<string>();
// Loop through and try everything otherwise
foreach (var matcher in matchers)
{
// Determine if the matcher passes
(bool passes, List<int> positions) = matcher.MatchesAll(stack);
if (!passes)
var positions = matcher.MatchesAll(stack);
if (positions.Count == 0)
continue;
// Format the list of all positions found
@@ -203,10 +173,10 @@ namespace SabreTools.Matching
string positionsString = string.Join(", ", positions);
#endif
// If we there is no version method, just return the protection name
// If we there is no version method, just return the match name
if (matcher.GetStreamVersion == null)
{
matchedProtections.Enqueue((matcher.ProtectionName ?? "Unknown Protection") + (includeDebug ? $" (Index {positionsString})" : string.Empty));
matchesList.Add((matcher.MatchName ?? "Unknown") + (includeDebug ? $" (Index {positionsString})" : string.Empty));
}
// Otherwise, invoke the version method
@@ -217,15 +187,15 @@ namespace SabreTools.Matching
if (version == null)
continue;
matchedProtections.Enqueue($"{matcher.ProtectionName ?? "Unknown Protection"} {version}".Trim() + (includeDebug ? $" (Index {positionsString})" : string.Empty));
matchesList.Add($"{matcher.MatchName ?? "Unknown"} {version}".Trim() + (includeDebug ? $" (Index {positionsString})" : string.Empty));
}
// If we're stopping after the first protection, bail out here
// If we're stopping after the first match, bail out here
if (stopAfterFirst)
return matchedProtections;
return matchesList;
}
return matchedProtections;
return matchesList;
}
#endregion
@@ -238,15 +208,9 @@ namespace SabreTools.Matching
/// <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);
}
/// <returns>List of strings representing the matches, null or empty otherwise</returns>
public static List<string> GetAllMatches(string file, IEnumerable<PathMatchSet>? matchers, bool any = false)
=> FindAllMatches([file], matchers, any, false);
// <summary>
/// Get all path matches for a given list of matchers
@@ -254,15 +218,9 @@ namespace SabreTools.Matching
/// <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);
}
/// <returns>List of strings representing the matches, null or empty otherwise</returns>
public static List<string> GetAllMatches(IEnumerable<string>? files, IEnumerable<PathMatchSet>? matchers, bool any = false)
=> FindAllMatches(files, matchers, any, false);
/// <summary>
/// Get first path match for a given list of matchers
@@ -270,14 +228,14 @@ namespace SabreTools.Matching
/// <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>
/// <returns>String representing the match, 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())
var contentMatches = FindAllMatches([file], matchers, any, true);
if (contentMatches == null || contentMatches.Count == 0)
return null;
return contentMatches.First();
return contentMatches[0];
}
/// <summary>
@@ -286,14 +244,14 @@ namespace SabreTools.Matching
/// <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>
/// <returns>String representing the match, 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())
if (contentMatches == null || contentMatches.Count == 0)
return null;
return contentMatches.First();
return contentMatches[0];
}
/// <summary>
@@ -303,23 +261,15 @@ namespace SabreTools.Matching
/// <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
/// <returns>List of strings representing the matches, null or empty otherwise</returns>
private static List<string> FindAllMatches(IEnumerable<string>? files, IEnumerable<PathMatchSet>? matchers, bool any, bool stopAfterFirst)
{
// If there's no mappings, we can't match
if (matchers == null || !matchers.Any())
return new();
if (matchers == null)
return [];
// Initialize the list of matched protections
#if NET20 || NET35
var matchedProtections = new Queue<string>();
#else
var matchedProtections = new ConcurrentQueue<string>();
#endif
// Initialize the list of matches
var matchesList = new List<string>();
// Loop through and try everything otherwise
foreach (var matcher in matchers)
@@ -329,25 +279,25 @@ namespace SabreTools.Matching
string? firstMatchedString;
if (any)
{
(bool anyPasses, var matchedString) = matcher.MatchesAny(files);
passes = anyPasses;
string? matchedString = matcher.MatchesAny(files);
passes = matchedString != null;
firstMatchedString = matchedString;
}
else
{
(bool allPasses, List<string> matchedStrings) = matcher.MatchesAll(files);
passes = allPasses;
firstMatchedString = matchedStrings.FirstOrDefault();
List<string> matchedStrings = matcher.MatchesAll(files);
passes = matchedStrings.Count > 0;
firstMatchedString = passes ? matchedStrings[0] : null;
}
// 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 we there is no version method, just return the match name
if (matcher.GetVersion == null)
{
matchedProtections.Enqueue(matcher.ProtectionName ?? "Unknown Protection");
matchesList.Add(matcher.MatchName ?? "Unknown");
}
// Otherwise, invoke the version method
@@ -358,15 +308,15 @@ namespace SabreTools.Matching
if (version == null)
continue;
matchedProtections.Enqueue($"{matcher.ProtectionName ?? "Unknown Protection"} {version}".Trim());
matchesList.Add($"{matcher.MatchName ?? "Unknown"} {version}".Trim());
}
// If we're stopping after the first protection, bail out here
// If we're stopping after the first match, bail out here
if (stopAfterFirst)
return matchedProtections;
return matchesList;
}
return matchedProtections;
return matchesList;
}
#endregion

View File

@@ -1,6 +1,6 @@
using System.IO;
namespace SabreTools.Matching
namespace SabreTools.Matching.Paths
{
/// <summary>
/// File path matching criteria

View File

@@ -1,7 +1,6 @@
using System.Collections.Generic;
using System.Linq;
namespace SabreTools.Matching
namespace SabreTools.Matching.Paths
{
/// <summary>
/// Path matching criteria
@@ -11,11 +10,7 @@ namespace SabreTools.Matching
/// <summary>
/// String to match
/// </summary>
#if NETFRAMEWORK || NETCOREAPP
public string? Needle { get; private set; }
#else
public string? Needle { get; init; }
#endif
public string? Needle { get; }
/// <summary>
/// Match exact casing instead of invariant
@@ -35,9 +30,9 @@ namespace SabreTools.Matching
/// <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;
Needle = needle;
MatchExact = matchExact;
UseEndsWith = useEndsWith;
}
#region Matching
@@ -46,30 +41,30 @@ namespace SabreTools.Matching
/// 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)
/// <returns>Matched item on success, null on error</returns>
public 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);
if (stack == null || Needle == null || Needle.Length == 0)
return null;
// Preprocess the needle, if necessary
string procNeedle = this.MatchExact ? this.Needle : this.Needle.ToLowerInvariant();
string procNeedle = MatchExact ? Needle : Needle.ToLowerInvariant();
foreach (string stackItem in stack)
{
// Preprocess the stack item, if necessary
string procStackItem = this.MatchExact ? stackItem : stackItem.ToLowerInvariant();
string procStackItem = MatchExact ? stackItem : stackItem.ToLowerInvariant();
if (this.UseEndsWith && procStackItem.EndsWith(procNeedle))
return (true, stackItem);
else if (!this.UseEndsWith && procStackItem.Contains(procNeedle))
return (true, stackItem);
if (UseEndsWith && procStackItem.EndsWith(procNeedle))
return stackItem;
else if (!UseEndsWith && procStackItem.Contains(procNeedle))
return stackItem;
}
return (false, null);
return null;
}
#endregion
}
}

View File

@@ -0,0 +1,106 @@
using System.Collections.Generic;
namespace SabreTools.Matching.Paths
{
/// <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 match name, or `null`,
/// in which case it will cause the match name to be omitted.
/// </remarks>
public GetPathVersion? GetVersion { get; }
#region Constructors
public PathMatchSet(string needle, string matchName)
: this([needle], null, matchName) { }
public PathMatchSet(List<string> needles, string matchName)
: this(needles, null, matchName) { }
public PathMatchSet(string needle, GetPathVersion? getVersion, string matchName)
: this([needle], getVersion, matchName) { }
public PathMatchSet(List<string> needles, GetPathVersion? getVersion, string matchName)
: this(needles.ConvertAll(n => new PathMatch(n)), getVersion, matchName) { }
public PathMatchSet(PathMatch needle, string matchName)
: this([needle], null, matchName) { }
public PathMatchSet(List<PathMatch> needles, string matchName)
: this(needles, null, matchName) { }
public PathMatchSet(PathMatch needle, GetPathVersion? getVersion, string matchName)
: this([needle], getVersion, matchName) { }
public PathMatchSet(List<PathMatch> needles, GetPathVersion? getVersion, string matchName)
{
Matchers = needles;
GetVersion = getVersion;
MatchName = matchName;
}
#endregion
#region Matching
/// <summary>
/// Determine whether all path matches pass
/// </summary>
/// <param name="stack">List of strings to try to match</param>
/// <returns>List of matching values, if any</returns>
public List<string> MatchesAll(IEnumerable<string>? stack)
{
// If no path matches are defined, we fail out
if (Matchers == null)
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>
/// Determine whether any path matches pass
/// </summary>
/// <param name="stack">List of strings to try to match</param>
/// <returns>First matching value on success, null on error</returns>
public string? MatchesAny(IEnumerable<string>? stack)
{
// If no path matches are defined, we fail out
if (Matchers == null)
return null;
// Loop through all path matches and make sure all pass
foreach (var pathMatch in Matchers)
{
string? value = pathMatch.Match(stack);
if (value != null)
return value;
}
return null;
}
#endregion
}
}

View File

@@ -0,0 +1,41 @@
<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>
<LangVersion>latest</LangVersion>
<Nullable>enable</Nullable>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
<Version>1.3.4</Version>
<!-- Package Properties -->
<Authors>Matt Nadareski</Authors>
<Description>Byte array and stream matching library</Description>
<Copyright>Copyright (c) Matt Nadareski 2018-2024</Copyright>
<PackageProjectUrl>https://github.com/SabreTools/</PackageProjectUrl>
<PackageReadmeFile>README.md</PackageReadmeFile>
<RepositoryUrl>https://github.com/SabreTools/SabreTools.Matching</RepositoryUrl>
<RepositoryType>git</RepositoryType>
<PackageTags>byte array stream match matching</PackageTags>
<PackageLicenseExpression>MIT</PackageLicenseExpression>
</PropertyGroup>
<!-- Support All Frameworks -->
<PropertyGroup Condition="$(TargetFramework.StartsWith(`net2`)) OR $(TargetFramework.StartsWith(`net3`)) OR $(TargetFramework.StartsWith(`net4`))">
<RuntimeIdentifiers>win-x86;win-x64</RuntimeIdentifiers>
</PropertyGroup>
<PropertyGroup Condition="$(TargetFramework.StartsWith(`netcoreapp`)) OR $(TargetFramework.StartsWith(`net5`))">
<RuntimeIdentifiers>win-x86;win-x64;win-arm64;linux-x64;linux-arm64;osx-x64</RuntimeIdentifiers>
</PropertyGroup>
<PropertyGroup Condition="$(TargetFramework.StartsWith(`net6`)) OR $(TargetFramework.StartsWith(`net7`)) OR $(TargetFramework.StartsWith(`net8`))">
<RuntimeIdentifiers>win-x86;win-x64;win-arm64;linux-x64;linux-arm64;osx-x64;osx-arm64</RuntimeIdentifiers>
</PropertyGroup>
<PropertyGroup Condition="$(RuntimeIdentifier.StartsWith(`osx-arm`))">
<TargetFrameworks>net6.0;net7.0;net8.0</TargetFrameworks>
</PropertyGroup>
<ItemGroup>
<None Include="../README.md" Pack="true" PackagePath="" />
</ItemGroup>
</Project>

36
publish-nix.sh Normal file
View File

@@ -0,0 +1,36 @@
#! /bin/bash
# This batch file assumes the following:
# - .NET 8.0 (or newer) SDK is installed and in PATH
#
# If any of these are not satisfied, the operation may fail
# in an unpredictable way and result in an incomplete output.
# Optional parameters
NO_BUILD=false
while getopts "uba" OPTION
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
View File

@@ -0,0 +1,26 @@
# This batch file assumes the following:
# - .NET 8.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
}