From 005529f959777fc4cd9a8641ea4df7f9bfbd9b2e Mon Sep 17 00:00:00 2001 From: Matt Nadareski Date: Thu, 8 Dec 2022 22:27:17 -0800 Subject: [PATCH] Add stream support to Matching --- BurnOutSharp.Matching/ContentMatch.cs | 96 +++++++++++++++- BurnOutSharp.Matching/ContentMatchSet.cs | 136 +++++++++++++++++++---- BurnOutSharp.Matching/MatchUtil.cs | 126 +++++++++++++++++++-- 3 files changed, 321 insertions(+), 37 deletions(-) diff --git a/BurnOutSharp.Matching/ContentMatch.cs b/BurnOutSharp.Matching/ContentMatch.cs index 36602a03..95e53431 100644 --- a/BurnOutSharp.Matching/ContentMatch.cs +++ b/BurnOutSharp.Matching/ContentMatch.cs @@ -1,3 +1,5 @@ +using System.IO; + namespace BurnOutSharp.Matching { /// @@ -33,7 +35,7 @@ namespace BurnOutSharp.Matching End = end; } - #region Matching + #region Array Matching /// /// Get if this match can be found in a stack @@ -91,6 +93,7 @@ namespace BurnOutSharp.Matching 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 @@ -102,7 +105,96 @@ namespace BurnOutSharp.Matching return true; } - + + #endregion + + #region Stream Matching + + /// + /// Get if this match can be found in a stack + /// + /// Stream to search for the given content + /// True to search from the end of the array, false from the start + /// Tuple of success and found position + 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 || Needle == null || Needle.Length == 0) + return (false, -1); + + // If the needle array is larger than the stack array, it can't be contained within + if (Needle.Length > stack.Length) + return (false, -1); + + // Set the default start and end values + 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 - 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); + } + + /// + /// Get if a stack at a certain index is equal to a needle + /// + /// Stream to search for the given content + /// Starting index to check equality + /// True if the needle matches the stack at a given index + 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 } } \ No newline at end of file diff --git a/BurnOutSharp.Matching/ContentMatchSet.cs b/BurnOutSharp.Matching/ContentMatchSet.cs index 8930852c..9b17427e 100644 --- a/BurnOutSharp.Matching/ContentMatchSet.cs +++ b/BurnOutSharp.Matching/ContentMatchSet.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.IO; using System.Linq; namespace BurnOutSharp.Matching @@ -19,48 +20,84 @@ namespace BurnOutSharp.Matching /// to the protection name, or `null`, in which case it will cause /// the protection to be omitted. /// - public Func, string> GetVersion { get; set; } + public Func, string> GetArrayVersion { get; set; } - #region Constructors + /// + /// Function to get a content version + /// + /// + /// 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. + /// + public Func, string> GetStreamVersion { get; set; } + + #region Generic Constructors public ContentMatchSet(byte?[] needle, string protectionName) - : this(new List { needle }, null, protectionName) { } + : this(new List { needle }, getArrayVersion: null, protectionName) { } public ContentMatchSet(List needles, string protectionName) - : this(needles, null, protectionName) { } - - public ContentMatchSet(byte?[] needle, Func, string> getVersion, string protectionName) - : this(new List { needle }, getVersion, protectionName) { } - - public ContentMatchSet(List needles, Func, string> getVersion, string protectionName) - : this(needles.Select(n => new ContentMatch(n)).ToList(), getVersion, protectionName) { } + : this(needles, getArrayVersion: null, protectionName) { } public ContentMatchSet(ContentMatch needle, string protectionName) - : this(new List() { needle }, null, protectionName) { } + : this(new List() { needle }, getArrayVersion: null, protectionName) { } public ContentMatchSet(List needles, string protectionName) - : this(needles, null, protectionName) { } + : this(needles, getArrayVersion: null, protectionName) { } - public ContentMatchSet(ContentMatch needle, Func, string> getVersion, string protectionName) - : this(new List() { needle }, getVersion, protectionName) { } + #endregion - public ContentMatchSet(List needles, Func, string> getVersion, string protectionName) + #region Array Constructors + + public ContentMatchSet(byte?[] needle, Func, string> getArrayVersion, string protectionName) + : this(new List { needle }, getArrayVersion, protectionName) { } + + public ContentMatchSet(List needles, Func, string> getArrayVersion, string protectionName) + : this(needles.Select(n => new ContentMatch(n)).ToList(), getArrayVersion, protectionName) { } + + public ContentMatchSet(ContentMatch needle, Func, string> getArrayVersion, string protectionName) + : this(new List() { needle }, getArrayVersion, protectionName) { } + + public ContentMatchSet(List needles, Func, string> getArrayVersion, string protectionName) { Matchers = needles; - GetVersion = getVersion; + GetArrayVersion = getArrayVersion; ProtectionName = protectionName; } #endregion - #region Matching + #region Stream Constructors + + public ContentMatchSet(byte?[] needle, Func, string> getStreamVersion, string protectionName) + : this(new List { needle }, getStreamVersion, protectionName) { } + + public ContentMatchSet(List needles, Func, string> getStreamVersion, string protectionName) + : this(needles.Select(n => new ContentMatch(n)).ToList(), getStreamVersion, protectionName) { } + + public ContentMatchSet(ContentMatch needle, Func, string> getStreamVersion, string protectionName) + : this(new List() { needle }, getStreamVersion, protectionName) { } + + public ContentMatchSet(List needles, Func, string> getStreamVersion, string protectionName) + { + Matchers = needles; + GetStreamVersion = getStreamVersion; + ProtectionName = protectionName; + } + + #endregion + + #region Array Matching /// /// Determine whether all content matches pass /// - /// Byte array representing the file contents + /// Array to search /// Tuple of passing status and matching positions - public (bool, List) MatchesAll(byte[] fileContent) + public (bool, List) MatchesAll(byte[] stack) { // If no content matches are defined, we fail out if (Matchers == null || !Matchers.Any()) @@ -72,7 +109,7 @@ namespace BurnOutSharp.Matching // Loop through all content matches and make sure all pass foreach (var contentMatch in Matchers) { - (bool match, int position) = contentMatch.Match(fileContent); + (bool match, int position) = contentMatch.Match(stack); if (!match) return (false, new List()); else @@ -85,9 +122,9 @@ namespace BurnOutSharp.Matching /// /// Determine whether any content matches pass /// - /// Byte array representing the file contents + /// Array to search /// Tuple of passing status and first matching position - public (bool, int) MatchesAny(byte[] fileContent) + public (bool, int) MatchesAny(byte[] stack) { // If no content matches are defined, we fail out if (Matchers == null || !Matchers.Any()) @@ -96,7 +133,60 @@ namespace BurnOutSharp.Matching // Loop through all content matches and make sure all pass foreach (var contentMatch in Matchers) { - (bool match, int position) = contentMatch.Match(fileContent); + (bool match, int position) = contentMatch.Match(stack); + if (match) + return (true, position); + } + + return (false, -1); + } + + #endregion + + #region Stream Matching + + /// + /// Determine whether all content matches pass + /// + /// Stream to search + /// Tuple of passing status and matching positions + public (bool, List) MatchesAll(Stream stack) + { + // If no content matches are defined, we fail out + if (Matchers == null || !Matchers.Any()) + return (false, new List()); + + // Initialize the position list + List positions = new List(); + + // 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()); + else + positions.Add(position); + } + + return (true, positions); + } + + /// + /// Determine whether any content matches pass + /// + /// Stream to search + /// Tuple of passing status and first matching position + 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); } diff --git a/BurnOutSharp.Matching/MatchUtil.cs b/BurnOutSharp.Matching/MatchUtil.cs index 9dc9e537..4f116688 100644 --- a/BurnOutSharp.Matching/MatchUtil.cs +++ b/BurnOutSharp.Matching/MatchUtil.cs @@ -1,5 +1,6 @@ using System.Collections.Concurrent; using System.Collections.Generic; +using System.IO; using System.Linq; namespace BurnOutSharp.Matching @@ -9,40 +10,40 @@ namespace BurnOutSharp.Matching /// public static class MatchUtil { - #region Content Matching + #region Array Content Matching /// /// Get all content matches for a given list of matchers /// /// File to check for matches - /// Byte array representing the file contents + /// Array to search /// Enumerable of ContentMatchSets to be run on the file /// True to include positional data, false otherwise /// List of strings representing the matched protections, null or empty otherwise public static ConcurrentQueue GetAllMatches( string file, - byte[] fileContent, + byte[] stack, IEnumerable matchers, bool includeDebug = false) { - return FindAllMatches(file, fileContent, matchers, includeDebug, false); + return FindAllMatches(file, stack, matchers, includeDebug, false); } /// /// Get first content match for a given list of matchers /// /// File to check for matches - /// Byte array representing the file contents + /// Array to search /// Enumerable of ContentMatchSets to be run on the file /// True to include positional data, false otherwise /// String representing the matched protection, null otherwise public static string GetFirstMatch( string file, - byte[] fileContent, + byte[] stack, IEnumerable matchers, bool includeDebug = false) { - var contentMatches = FindAllMatches(file, fileContent, matchers, includeDebug, true); + var contentMatches = FindAllMatches(file, stack, matchers, includeDebug, true); if (contentMatches == null || !contentMatches.Any()) return null; @@ -53,14 +54,14 @@ namespace BurnOutSharp.Matching /// Get the required set of content matches on a per Matcher basis /// /// File to check for matches - /// Byte array representing the file contents + /// Array to search /// Enumerable of ContentMatchSets to be run on the file /// True to include positional data, false otherwise /// True to stop after the first match, false otherwise /// List of strings representing the matched protections, null or empty otherwise private static ConcurrentQueue FindAllMatches( string file, - byte[] fileContent, + byte[] stack, IEnumerable matchers, bool includeDebug, bool stopAfterFirst) @@ -76,7 +77,7 @@ namespace BurnOutSharp.Matching foreach (var matcher in matchers) { // Determine if the matcher passes - (bool passes, List positions) = matcher.MatchesAll(fileContent); + (bool passes, List positions) = matcher.MatchesAll(stack); if (!passes) continue; @@ -84,7 +85,7 @@ namespace BurnOutSharp.Matching string positionsString = string.Join(", ", positions); // If we there is no version method, just return the protection name - if (matcher.GetVersion == null) + if (matcher.GetArrayVersion == null) { matchedProtections.Enqueue((matcher.ProtectionName ?? "Unknown Protection") + (includeDebug ? $" (Index {positionsString})" : string.Empty)); } @@ -93,7 +94,108 @@ namespace BurnOutSharp.Matching else { // A null version returned means the check didn't pass at the version step - string version = matcher.GetVersion(file, fileContent, positions); + string version = matcher.GetArrayVersion(file, stack, positions); + if (version == null) + continue; + + matchedProtections.Enqueue($"{matcher.ProtectionName ?? "Unknown Protection"} {version}".TrimEnd() + (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 + + /// + /// Get all content matches for a given list of matchers + /// + /// File to check for matches + /// Stream to search + /// Enumerable of ContentMatchSets to be run on the file + /// True to include positional data, false otherwise + /// List of strings representing the matched protections, null or empty otherwise + public static ConcurrentQueue GetAllMatches( + string file, + Stream stack, + IEnumerable matchers, + bool includeDebug = false) + { + return FindAllMatches(file, stack, matchers, includeDebug, false); + } + + /// + /// Get first content match for a given list of matchers + /// + /// File to check for matches + /// Stream to search + /// Enumerable of ContentMatchSets to be run on the file + /// True to include positional data, false otherwise + /// String representing the matched protection, null otherwise + public static string GetFirstMatch( + string file, + Stream stack, + IEnumerable matchers, + bool includeDebug = false) + { + var contentMatches = FindAllMatches(file, stack, matchers, includeDebug, true); + if (contentMatches == null || !contentMatches.Any()) + return null; + + return contentMatches.First(); + } + + /// + /// Get the required set of content matches on a per Matcher basis + /// + /// File to check for matches + /// Stream to search + /// Enumerable of ContentMatchSets to be run on the file + /// True to include positional data, false otherwise + /// True to stop after the first match, false otherwise + /// List of strings representing the matched protections, null or empty otherwise + private static ConcurrentQueue FindAllMatches( + string file, + Stream stack, + IEnumerable matchers, + bool includeDebug, + bool stopAfterFirst) + { + // If there's no mappings, we can't match + if (matchers == null || !matchers.Any()) + return null; + + // Initialize the queue of matched protections + var matchedProtections = new ConcurrentQueue(); + + // Loop through and try everything otherwise + foreach (var matcher in matchers) + { + // Determine if the matcher passes + (bool passes, List positions) = matcher.MatchesAll(stack); + if (!passes) + continue; + + // Format the list of all positions found + string positionsString = string.Join(", ", positions); + + // 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 + string version = matcher.GetStreamVersion(file, stack, positions); if (version == null) continue;