using System.Collections.Generic; using System.IO; using System.Text; namespace SabreTools.Matching { /// /// Helper class for matching /// public static class MatchUtil { #region Array Content Matching /// /// Get all content matches for a given list of matchers /// /// File to check for matches /// Array to search /// List of ContentMatchSets to be run on the file /// True if any content match is a success, false if all have to match /// True to include positional data, false otherwise /// List of strings representing the matches, null or empty otherwise public static List GetAllMatches(string file, byte[]? stack, List matchSets, bool any = false, bool includeDebug = false) => FindAllMatches(file, stack, matchSets, any, includeDebug, false); /// /// Get first content match for a given list of matchers /// /// File to check for matches /// Array to search /// List of ContentMatchSets to be run on the file /// True if any content match is a success, false if all have to match /// True to include positional data, false otherwise /// String representing the match, null otherwise public static string? GetFirstMatch(string file, byte[]? stack, List matchSets, bool any = false, bool includeDebug = false) { var contentMatches = FindAllMatches(file, stack, matchSets, any, includeDebug, true); if (contentMatches is null || contentMatches.Count == 0) return null; return contentMatches[0]; } /// /// Get the required set of content matches on a per Matcher basis /// /// File to check for matches /// Array to search /// List of ContentMatchSets to be run on the file /// True if any content match is a success, false if all have to match /// True to include positional data, false otherwise /// True to stop after the first match, false otherwise /// List of strings representing the matches, empty otherwise private static List FindAllMatches(string file, byte[]? stack, List matchSets, bool any, bool includeDebug, bool stopAfterFirst) { // If either set is null or empty if (stack is null || stack.Length == 0 || matchSets.Count == 0) return []; // Initialize the list of matches var matchesList = new List(); // Loop through and try everything otherwise foreach (var matcher in matchSets) { // Determine if the matcher passes List 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 is not null) { // A null version returned means the check didn't pass at the version step var version = matcher.GetArrayVersion(file, stack, positions); if (version is 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 /// /// Get all content matches for a given list of matchers /// /// File to check for matches /// Stream to search /// List of ContentMatchSets to be run on the file /// True if any content match is a success, false if all have to match /// True to include positional data, false otherwise /// List of strings representing the matches, null or empty otherwise public static List GetAllMatches(string file, Stream? stack, List matchSets, bool any = false, bool includeDebug = false) => FindAllMatches(file, stack, matchSets, any, includeDebug, false); /// /// Get first content match for a given list of matchers /// /// File to check for matches /// Stream to search /// List of ContentMatchSets to be run on the file /// True if any content match is a success, false if all have to match /// True to include positional data, false otherwise /// String representing the match, null otherwise public static string? GetFirstMatch(string file, Stream? stack, List matchSets, bool any = false, bool includeDebug = false) { var contentMatches = FindAllMatches(file, stack, matchSets, any, includeDebug, true); if (contentMatches is null || contentMatches.Count == 0) return null; return contentMatches[0]; } /// /// Get the required set of content matches on a per Matcher basis /// /// File to check for matches /// Stream to search /// List of ContentMatchSets to be run on the file /// True if any content match is a success, false if all have to match /// True to include positional data, false otherwise /// True to stop after the first match, false otherwise /// List of strings representing the matches, empty otherwise private static List FindAllMatches(string file, Stream? stack, List matchSets, bool any, bool includeDebug, bool stopAfterFirst) { // If either set is null or empty if (stack is null || stack.Length == 0 || matchSets.Count == 0) return []; // Initialize the list of matches var matchesList = new List(); // Loop through and try everything otherwise foreach (var matcher in matchSets) { // Determine if the matcher passes List 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 is not null) { // A null version returned means the check didn't pass at the version step var version = matcher.GetStreamVersion(file, stack, positions); if (version is 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 /// /// Get all path matches for a given list of matchers /// /// File path to check for matches /// List of PathMatchSets to be run on the file /// True if any path match is a success, false if all have to match /// List of strings representing the matches, null or empty otherwise public static List GetAllMatches(string stack, List matchSets, bool any = false) => FindAllMatches([stack], matchSets, any, false); /// /// Get all path matches for a given list of matchers /// /// File paths to check for matches /// List of PathMatchSets to be run on the file /// True if any path match is a success, false if all have to match /// List of strings representing the matches, null or empty otherwise public static List GetAllMatches(List? stack, List matchSets, bool any = false) => FindAllMatches(stack, matchSets, any, false); /// /// Get first path match for a given list of matchers /// /// File path to check for matches /// List of PathMatchSets to be run on the file /// True if any path match is a success, false if all have to match /// String representing the match, null otherwise public static string? GetFirstMatch(string stack, List matchSets, bool any = false) { var contentMatches = FindAllMatches([stack], matchSets, any, true); if (contentMatches is null || contentMatches.Count == 0) return null; return contentMatches[0]; } /// /// Get first path match for a given list of matchers /// /// File paths to check for matches /// List of PathMatchSets to be run on the file /// True if any path match is a success, false if all have to match /// String representing the match, null otherwise public static string? GetFirstMatch(List stack, List matchSets, bool any = false) { var contentMatches = FindAllMatches(stack, matchSets, any, true); if (contentMatches is null || contentMatches.Count == 0) return null; return contentMatches[0]; } /// /// Get the required set of path matches on a per Matcher basis /// /// File paths to check for matches /// List of PathMatchSets to be run on the file /// True if any path match is a success, false if all have to match /// True to stop after the first match, false otherwise /// List of strings representing the matches, null or empty otherwise private static List FindAllMatches(List? stack, List matchSets, bool any, bool stopAfterFirst) { // If either set is null or empty if (stack is null || stack.Count == 0 || matchSets.Count == 0) return []; // Initialize the list of matches var matchesList = new List(); // Loop through and try everything otherwise foreach (var matcher in matchSets) { // Determine if the matcher passes List matches = []; if (any) { string? anyMatch = matcher.MatchesAny(stack); if (anyMatch is not 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 is not null) { // A null version returned means the check didn't pass at the version step var version = matcher.GetVersion(matches[0], stack); if (version is 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 } }