Port metadata functionality from ST

This commit is contained in:
Matt Nadareski
2026-03-24 18:03:01 -04:00
parent 5f0fdcfd8d
commit e11a08b587
138 changed files with 37585 additions and 0 deletions

View File

@@ -0,0 +1,41 @@
namespace SabreTools.Metadata.Filter
{
/// <summary>
/// Determines how a filter group should be applied
/// </summary>
public enum GroupType
{
/// <summary>
/// Default value, does nothing
/// </summary>
NONE,
/// <summary>
/// All must pass for the group to pass
/// </summary>
AND,
/// <summary>
/// Any must pass for the group to pass
/// </summary>
OR,
}
/// <summary>
/// Determines what operation is being done
/// </summary>
public enum Operation
{
/// <summary>
/// Default value, does nothing
/// </summary>
NONE,
Equals,
NotEquals,
GreaterThan,
GreaterThanOrEqual,
LessThan,
LessThanOrEqual,
}
}

View File

@@ -0,0 +1,126 @@
using System;
using System.Collections.Generic;
using System.IO;
using SabreTools.IO.Logging;
using SabreTools.IO.Readers;
namespace SabreTools.Metadata.Filter
{
public class ExtraIniItem
{
#region Fields
/// <summary>
/// Item type and field to update with INI information
/// </summary>
public readonly FilterKey Key;
/// <summary>
/// Mappings from machine names to value
/// </summary>
public readonly Dictionary<string, string> Mappings = [];
#endregion
#region Constructors
public ExtraIniItem(string key, string iniPath)
{
Key = new FilterKey(key);
if (!PopulateFromFile(iniPath))
Mappings.Clear();
}
public ExtraIniItem(string itemName, string fieldName, string iniPath)
{
Key = new FilterKey(itemName, fieldName);
if (!PopulateFromFile(iniPath))
Mappings.Clear();
}
#endregion
#region Extras Population
/// <summary>
/// Populate the dictionary from an INI file
/// </summary>
/// <param name="iniPath">Path to INI file to populate from</param>
/// <remarks>
/// The INI file format that is supported here is not exactly the same
/// as a traditional one. This expects a MAME extras format, which usually
/// doesn't contain key value pairs and always at least contains one section
/// called `ROOT_FOLDER`. If that's the name of a section, then we assume
/// the value is boolean. If there's another section name, then that is set
/// as the value instead.
/// </remarks>
private bool PopulateFromFile(string iniPath)
{
// Validate the path
if (iniPath.Length == 0)
return false;
else if (!File.Exists(iniPath))
return false;
// Prepare all internal variables
var ir = new IniReader(iniPath) { ValidateRows = false };
bool foundRootFolder = false;
// If we got a null reader, just return
if (ir is null)
return false;
// Otherwise, read the file to the end
try
{
while (!ir.EndOfStream)
{
// Read in the next line and process
ir.ReadNextLine();
// We don't care about whitespace or comments
if (ir.RowType == IniRowType.None || ir.RowType == IniRowType.Comment)
continue;
// If we have a section, just read it in
if (ir.RowType == IniRowType.SectionHeader)
{
// If we've found the start of the extras, set the flag
if (string.Equals(ir.Section, "ROOT_FOLDER", StringComparison.OrdinalIgnoreCase))
foundRootFolder = true;
continue;
}
// If we have a value, then we start populating the dictionary
else if (foundRootFolder)
{
// Get the value and machine name
string? value = ir.Section;
string? machineName = ir.CurrentLine?.Trim();
// If the section is "ROOT_FOLDER", then we use the value "true" instead.
// This is done because some INI files use the name of the file as the
// category to be assigned to the items included.
if (value == "ROOT_FOLDER")
value = "true";
// Add the new mapping
if (machineName is not null && value is not null)
Mappings[machineName] = value;
}
}
}
catch (Exception ex)
{
LoggerImpl.Warning(ex, $"Exception found while parsing '{iniPath}'");
return false;
}
ir.Dispose();
return true;
}
#endregion
}
}

View File

@@ -0,0 +1,198 @@
using System.Collections.Generic;
using System.Text.RegularExpressions;
using SabreTools.Data.Models.Metadata;
namespace SabreTools.Metadata.Filter
{
/// <summary>
/// Represents a set of filters and groups
/// </summary>
public class FilterGroup
{
/// <summary>
/// How to apply the group filters
/// </summary>
public readonly GroupType GroupType;
/// <summary>
/// All standalone filters in the group
/// </summary>
private readonly List<FilterObject> _subfilters = [];
/// <summary>
/// All filter groups contained in the group
/// </summary>
private readonly List<FilterGroup> _subgroups = [];
public FilterGroup(GroupType groupType)
{
GroupType = groupType;
}
public FilterGroup(FilterObject[] filters, GroupType groupType)
{
_subfilters.AddRange(filters);
GroupType = groupType;
}
public FilterGroup(FilterGroup[] groups, GroupType groupType)
{
_subgroups.AddRange(groups);
GroupType = groupType;
}
public FilterGroup(FilterObject[] filters, FilterGroup[] groups, GroupType groupType)
{
_subfilters.AddRange(filters);
_subgroups.AddRange(groups);
GroupType = groupType;
}
#region Accessors
/// <summary>
/// Add a FilterObject to the set
/// </summary>
public void AddFilter(FilterObject filter) => _subfilters.Add(filter);
/// <summary>
/// Add a FilterGroup to the set
/// </summary>
public void AddGroup(FilterGroup group) => _subgroups.Add(group);
#endregion
#region Matching
/// <summary>
/// Determine if a DictionaryBase object matches the group
/// </summary>
public bool Matches(DictionaryBase dictionaryBase)
{
return GroupType switch
{
GroupType.AND => MatchesAnd(dictionaryBase),
GroupType.OR => MatchesOr(dictionaryBase),
GroupType.NONE => false,
_ => false,
};
}
/// <summary>
/// Determines if a value matches all filters
/// </summary>
private bool MatchesAnd(DictionaryBase dictionaryBase)
{
// Run standalone filters
foreach (var filter in _subfilters)
{
// One failed match fails the group
if (!filter.Matches(dictionaryBase))
return false;
}
// Run filter subgroups
foreach (var group in _subgroups)
{
// One failed match fails the group
if (!group.Matches(dictionaryBase))
return false;
}
return true;
}
/// <summary>
/// Determines if a value matches any filters
/// </summary>
private bool MatchesOr(DictionaryBase dictionaryBase)
{
// Run standalone filters
foreach (var filter in _subfilters)
{
// One successful match passes the group
if (filter.Matches(dictionaryBase))
return true;
}
// Run filter subgroups
foreach (var group in _subgroups)
{
// One successful match passes the group
if (group.Matches(dictionaryBase))
return true;
}
return false;
}
#endregion
#region Helpers
#pragma warning disable IDE0051
/// <summary>
/// Derive a group type from the input string, if possible
/// </summary>
private static GroupType GetGroupType(string? groupType)
{
return groupType?.ToLowerInvariant() switch
{
"&" => GroupType.AND,
"&&" => GroupType.AND,
"|" => GroupType.OR,
"||" => GroupType.OR,
_ => GroupType.NONE,
};
}
#pragma warning restore IDE0051
#pragma warning disable IDE0051
/// <summary>
/// Parse an input string into a filter group
/// </summary>
private static void Parse(string? input)
{
// Tokenize the string
string[] tokens = Tokenize(input);
if (tokens.Length == 0)
return;
// Loop through the tokens and parse
for (int i = 0; i < tokens.Length; i++)
{
// TODO: Implement parsing
// - Opening parenthesis means a new group
// - Closing parenthesis means finalize group and return it
// - Current starting and ending with a parenthesis strips them off
// - Unbalanced parenthesis can only be found on parse
// - Failed parsing of FilterObjects(?)
// - Invalid FilterObjects(?)
}
}
#pragma warning restore IDE0051
/// <summary>
/// Tokenize an input string for parsing
/// </summary>
private static string[] Tokenize(string? input)
{
// Null inputs are ignored
if (input is null)
return [];
// Split the string into parseable pieces
// - Left and right parenthesis are separate
// - Operators & and | are separate
// - Key-value pairs are enforced for statements
// - Numbers can be a value without quotes
// - All other values require quotes
return Regex.Split(input, @"(\(|\)|[&|]{1,2}|[^\s()""]+[:!=]\d+|[^\s()""]+[:!=]{1,2}""[^""]*"")", RegexOptions.Compiled);
}
#endregion
}
}

View File

@@ -0,0 +1,214 @@
using System;
using SabreTools.Data.Models.Metadata;
using SabreTools.Metadata.Tools;
namespace SabreTools.Metadata.Filter
{
/// <summary>
/// Represents a single filter key
/// </summary>
public class FilterKey
{
/// <summary>
/// Item name associated with the filter
/// </summary>
public readonly string ItemName;
/// <summary>
/// Field name associated with the filter
/// </summary>
public readonly string FieldName;
/// <summary>
/// Validating combined key constructor
/// </summary>
public FilterKey(string? key)
{
if (!ParseFilterId(key, out string itemName, out string fieldName))
throw new ArgumentException($"{nameof(key)} could not be parsed", nameof(key));
ItemName = itemName;
FieldName = fieldName;
}
/// <summary>
/// Validating discrete value constructor
/// </summary>
public FilterKey(string itemName, string fieldName)
{
if (!ParseFilterId(ref itemName, ref fieldName))
throw new ArgumentException($"{nameof(itemName)} was not recognized", nameof(itemName));
ItemName = itemName;
FieldName = fieldName;
}
/// <inheritdoc/>
public override string ToString() => $"{ItemName}.{FieldName}";
/// <summary>
/// Parse a filter ID string into the item name and field name, if possible
/// </summary>
private static bool ParseFilterId(string? itemFieldString, out string itemName, out string fieldName)
{
// Set default values
itemName = string.Empty; fieldName = string.Empty;
// If we don't have a filter ID, we can't do anything
if (string.IsNullOrEmpty(itemFieldString))
return false;
// If we only have one part, we can't do anything
string[] splitFilter = itemFieldString!.Split('.');
if (splitFilter.Length != 2)
return false;
// Set and sanitize the filter ID
itemName = splitFilter[0];
fieldName = splitFilter[1];
return ParseFilterId(ref itemName, ref fieldName);
}
/// <summary>
/// Parse a filter ID string into the item name and field name, if possible
/// </summary>
private static bool ParseFilterId(ref string itemName, ref string fieldName)
{
// If we don't have a filter ID, we can't do anything
if (string.IsNullOrEmpty(itemName) || string.IsNullOrEmpty(fieldName))
return false;
// Return santized values based on the split ID
return itemName.ToLowerInvariant() switch
{
// Header
"header" => ParseHeaderFilterId(ref itemName, ref fieldName),
// Machine
"game" => ParseMachineFilterId(ref itemName, ref fieldName),
"machine" => ParseMachineFilterId(ref itemName, ref fieldName),
"resource" => ParseMachineFilterId(ref itemName, ref fieldName),
"set" => ParseMachineFilterId(ref itemName, ref fieldName),
// DatItem
"datitem" => ParseDatItemFilterId(ref itemName, ref fieldName),
"item" => ParseDatItemFilterId(ref itemName, ref fieldName),
_ => ParseDatItemFilterId(ref itemName, ref fieldName),
};
}
/// <summary>
/// Parse and validate header fields
/// </summary>
private static bool ParseHeaderFilterId(ref string itemName, ref string fieldName)
{
// Get the set of constants
var constants = TypeHelper.GetConstants(typeof(Header));
if (constants is null)
return false;
// Get if there's a match to the constant
string localFieldName = fieldName;
string? constantMatch = Array.Find(constants, c => string.Equals(c, localFieldName, StringComparison.OrdinalIgnoreCase));
if (constantMatch is null)
return false;
// Return the sanitized ID
itemName = MetadataFile.HeaderKey;
fieldName = constantMatch;
return true;
}
/// <summary>
/// Parse and validate machine/game fields
/// </summary>
private static bool ParseMachineFilterId(ref string itemName, ref string fieldName)
{
// Get the set of constants
var constants = TypeHelper.GetConstants(typeof(Machine));
if (constants is null)
return false;
// Get if there's a match to the constant
string localFieldName = fieldName;
string? constantMatch = Array.Find(constants, c => string.Equals(c, localFieldName, StringComparison.OrdinalIgnoreCase));
if (constantMatch is null)
return false;
// Return the sanitized ID
itemName = MetadataFile.MachineKey;
fieldName = constantMatch;
return true;
}
/// <summary>
/// Parse and validate item fields
/// </summary>
private static bool ParseDatItemFilterId(ref string itemName, ref string fieldName)
{
// Special case if the item name is reserved
if (string.Equals(itemName, "datitem", StringComparison.OrdinalIgnoreCase)
|| string.Equals(itemName, "item", StringComparison.OrdinalIgnoreCase))
{
// Get all item types
var itemTypes = TypeHelper.GetDatItemTypeNames();
// If we get any matches
string localFieldName = fieldName;
string? matchedType = Array.Find(itemTypes, t => DatItemContainsField(t, localFieldName));
if (matchedType is not null)
{
// Check for a matching field
string? matchedField = GetMatchingField(matchedType, fieldName);
if (matchedField is null)
return false;
itemName = "item";
fieldName = matchedField;
return true;
}
}
else
{
// Check for a matching field
string? matchedField = GetMatchingField(itemName, fieldName);
if (matchedField is null)
return false;
itemName = itemName.ToLowerInvariant();
fieldName = matchedField;
return true;
}
// Nothing was found
return false;
}
/// <summary>
/// Determine if an item type contains a field
/// </summary>
private static bool DatItemContainsField(string itemName, string fieldName)
=> GetMatchingField(itemName, fieldName) is not null;
/// <summary>
/// Determine if an item type contains a field
/// </summary>
private static string? GetMatchingField(string itemName, string fieldName)
{
// Get the correct item type
var itemType = TypeHelper.GetDatItemType(itemName.ToLowerInvariant());
if (itemType is null)
return null;
// Get the set of constants
var constants = TypeHelper.GetConstants(itemType);
if (constants is null)
return null;
// Get if there's a match to the constant
string localFieldName = fieldName;
string? constantMatch = Array.Find(constants, c => string.Equals(c, localFieldName, StringComparison.OrdinalIgnoreCase));
return constantMatch;
}
}
}

View File

@@ -0,0 +1,404 @@
using System;
using System.Text.RegularExpressions;
using SabreTools.Data.Models.Metadata;
using SabreTools.Metadata.Tools;
namespace SabreTools.Metadata.Filter
{
/// <summary>
/// Represents a single filtering object
/// </summary>
/// <remarks>TODO: Add ability to have a set of values that are accepted</remarks>
public class FilterObject
{
/// <summary>
/// Item key associated with the filter
/// </summary>
public readonly FilterKey Key;
/// <summary>
/// Value to match in the filter
/// </summary>
public readonly string? Value;
/// <summary>
/// Operation on how to match the filter
/// </summary>
public readonly Operation Operation;
public FilterObject(string? filterString)
{
if (!SplitFilterString(filterString, out var keyItem, out Operation operation, out var value))
throw new ArgumentException($"{nameof(filterString)} could not be parsed", nameof(filterString));
Key = new FilterKey(keyItem);
Value = value;
Operation = operation;
}
public FilterObject(string itemField, string? value, string? operation)
{
Key = new FilterKey(itemField);
Value = value;
Operation = GetOperation(operation);
}
public FilterObject(string itemField, string? value, Operation operation)
{
Key = new FilterKey(itemField);
Value = value;
Operation = operation;
}
#region Matching
/// <summary>
/// Determine if a DictionaryBase object matches the key and value
/// </summary>
public bool Matches(DictionaryBase dictionaryBase)
{
return Operation switch
{
Operation.Equals => MatchesEqual(dictionaryBase),
Operation.NotEquals => MatchesNotEqual(dictionaryBase),
Operation.GreaterThan => MatchesGreaterThan(dictionaryBase),
Operation.GreaterThanOrEqual => MatchesGreaterThanOrEqual(dictionaryBase),
Operation.LessThan => MatchesLessThan(dictionaryBase),
Operation.LessThanOrEqual => MatchesLessThanOrEqual(dictionaryBase),
Operation.NONE => false,
_ => false,
};
}
/// <summary>
/// Determines if a value matches exactly
/// </summary>
private bool MatchesEqual(DictionaryBase dictionaryBase)
{
// If the key doesn't exist, we count it as null
if (!dictionaryBase.ContainsKey(Key.FieldName))
return string.IsNullOrEmpty(Value);
// If the value in the dictionary is null
string? checkValue = dictionaryBase.ReadString(Key.FieldName);
if (checkValue is null)
return string.IsNullOrEmpty(Value);
// If we have both a potentally boolean check and value
bool? checkValueBool = checkValue.AsYesNo();
bool? matchValueBool = Value.AsYesNo();
if (checkValueBool is not null && matchValueBool is not null)
return checkValueBool == matchValueBool;
// If we have both a potentially numeric check and value
if (NumberHelper.IsNumeric(checkValue) && NumberHelper.IsNumeric(Value))
{
// Check Int64 values
long? checkValueLong = NumberHelper.ConvertToInt64(checkValue);
long? matchValueLong = NumberHelper.ConvertToInt64(Value);
if (checkValueLong is not null && matchValueLong is not null)
return checkValueLong == matchValueLong;
// Check Double values
double? checkValueDouble = NumberHelper.ConvertToDouble(checkValue);
double? matchValueDouble = NumberHelper.ConvertToDouble(Value);
if (checkValueDouble is not null && matchValueDouble is not null)
return checkValueDouble == matchValueDouble;
}
// If the value might contain valid Regex
if (Value is not null && ContainsRegex(Value))
return Regex.IsMatch(checkValue, Value);
return string.Equals(checkValue, Value, StringComparison.OrdinalIgnoreCase);
}
/// <summary>
/// Determines if a value does not match exactly
/// </summary>
private bool MatchesNotEqual(DictionaryBase dictionaryBase)
{
// If the key doesn't exist, we count it as null
if (!dictionaryBase.ContainsKey(Key.FieldName))
return !string.IsNullOrEmpty(Value);
// If the value in the dictionary is null
string? checkValue = dictionaryBase.ReadString(Key.FieldName);
if (checkValue is null)
return !string.IsNullOrEmpty(Value);
// If we have both a potentally boolean check and value
bool? checkValueBool = checkValue.AsYesNo();
bool? matchValueBool = Value.AsYesNo();
if (checkValueBool is not null && matchValueBool is not null)
return checkValueBool != matchValueBool;
// If we have both a potentially numeric check and value
if (NumberHelper.IsNumeric(checkValue) && NumberHelper.IsNumeric(Value))
{
// Check Int64 values
long? checkValueLong = NumberHelper.ConvertToInt64(checkValue);
long? matchValueLong = NumberHelper.ConvertToInt64(Value);
if (checkValueLong is not null && matchValueLong is not null)
return checkValueLong != matchValueLong;
// Check Double values
double? checkValueDouble = NumberHelper.ConvertToDouble(checkValue);
double? matchValueDouble = NumberHelper.ConvertToDouble(Value);
if (checkValueDouble is not null && matchValueDouble is not null)
return checkValueDouble != matchValueDouble;
}
// If the value might contain valid Regex
if (Value is not null && ContainsRegex(Value))
return !Regex.IsMatch(checkValue, Value);
return !string.Equals(checkValue, Value, StringComparison.OrdinalIgnoreCase);
}
/// <summary>
/// Determines if a value is strictly greater than
/// </summary>
private bool MatchesGreaterThan(DictionaryBase dictionaryBase)
{
// If the key doesn't exist, we count it as null
if (!dictionaryBase.ContainsKey(Key.FieldName))
return false;
// If the value in the dictionary is null
string? checkValue = dictionaryBase.ReadString(Key.FieldName);
if (checkValue is null)
return false;
// If we have both a potentially numeric check and value
if (NumberHelper.IsNumeric(checkValue) && NumberHelper.IsNumeric(Value))
{
// Check Int64 values
long? checkValueLong = NumberHelper.ConvertToInt64(checkValue);
long? matchValueLong = NumberHelper.ConvertToInt64(Value);
if (checkValueLong is not null && matchValueLong is not null)
return checkValueLong > matchValueLong;
// Check Double values
double? checkValueDouble = NumberHelper.ConvertToDouble(checkValue);
double? matchValueDouble = NumberHelper.ConvertToDouble(Value);
if (checkValueDouble is not null && matchValueDouble is not null)
return checkValueDouble > matchValueDouble;
}
return false;
}
/// <summary>
/// Determines if a value is greater than or equal
/// </summary>
private bool MatchesGreaterThanOrEqual(DictionaryBase dictionaryBase)
{
// If the key doesn't exist, we count it as null
if (!dictionaryBase.ContainsKey(Key.FieldName))
return false;
// If the value in the dictionary is null
string? checkValue = dictionaryBase.ReadString(Key.FieldName);
if (checkValue is null)
return false;
// If we have both a potentially numeric check and value
if (NumberHelper.IsNumeric(checkValue) && NumberHelper.IsNumeric(Value))
{
// Check Int64 values
long? checkValueLong = NumberHelper.ConvertToInt64(checkValue);
long? matchValueLong = NumberHelper.ConvertToInt64(Value);
if (checkValueLong is not null && matchValueLong is not null)
return checkValueLong >= matchValueLong;
// Check Double values
double? checkValueDouble = NumberHelper.ConvertToDouble(checkValue);
double? matchValueDouble = NumberHelper.ConvertToDouble(Value);
if (checkValueDouble is not null && matchValueDouble is not null)
return checkValueDouble >= matchValueDouble;
}
return false;
}
/// <summary>
/// Determines if a value is strictly less than
/// </summary>
private bool MatchesLessThan(DictionaryBase dictionaryBase)
{
// If the key doesn't exist, we count it as null
if (!dictionaryBase.ContainsKey(Key.FieldName))
return false;
// If the value in the dictionary is null
string? checkValue = dictionaryBase.ReadString(Key.FieldName);
if (checkValue is null)
return false;
// If we have both a potentially numeric check and value
if (NumberHelper.IsNumeric(checkValue) && NumberHelper.IsNumeric(Value))
{
// Check Int64 values
long? checkValueLong = NumberHelper.ConvertToInt64(checkValue);
long? matchValueLong = NumberHelper.ConvertToInt64(Value);
if (checkValueLong is not null && matchValueLong is not null)
return checkValueLong < matchValueLong;
// Check Double values
double? checkValueDouble = NumberHelper.ConvertToDouble(checkValue);
double? matchValueDouble = NumberHelper.ConvertToDouble(Value);
if (checkValueDouble is not null && matchValueDouble is not null)
return checkValueDouble < matchValueDouble;
}
return false;
}
/// <summary>
/// Determines if a value is less than or equal
/// </summary>
private bool MatchesLessThanOrEqual(DictionaryBase dictionaryBase)
{
// If the key doesn't exist, we count it as null
if (!dictionaryBase.ContainsKey(Key.FieldName))
return false;
// If the value in the dictionary is null
string? checkValue = dictionaryBase.ReadString(Key.FieldName);
if (checkValue is null)
return false;
// If we have both a potentially numeric check and value
if (NumberHelper.IsNumeric(checkValue) && NumberHelper.IsNumeric(Value))
{
// Check Int64 values
long? checkValueLong = NumberHelper.ConvertToInt64(checkValue);
long? matchValueLong = NumberHelper.ConvertToInt64(Value);
if (checkValueLong is not null && matchValueLong is not null)
return checkValueLong <= matchValueLong;
// Check Double values
double? checkValueDouble = NumberHelper.ConvertToDouble(checkValue);
double? matchValueDouble = NumberHelper.ConvertToDouble(Value);
if (checkValueDouble is not null && matchValueDouble is not null)
return checkValueDouble <= matchValueDouble;
}
return false;
}
#endregion
#region Helpers
/// <summary>
/// Determine if a value may contain regex for matching
/// </summary>
/// <remarks>
/// If a value contains one of the following characters:
/// ^ $ * ? +
/// Then it will attempt to check if the value is regex or not.
/// If none of those characters exist, then value will assumed
/// not to be regex.
/// </remarks>
private static bool ContainsRegex(string? value)
{
// If the value is missing, it can't be regex
if (value is null)
return false;
// If we find a special character, try parsing as regex
#if NETCOREAPP || NETSTANDARD2_1_OR_GREATER
if (value.Contains('^')
|| value.Contains('$')
|| value.Contains('*')
|| value.Contains('?')
|| value.Contains('+'))
#else
if (value.Contains("^")
|| value.Contains("$")
|| value.Contains("*")
|| value.Contains("?")
|| value.Contains("+"))
#endif
{
try
{
_ = new Regex(value);
return true;
}
catch
{
return false;
}
}
return false;
}
/// <summary>
/// Derive an operation from the input string, if possible
/// </summary>
private static Operation GetOperation(string? operation)
{
return operation?.ToLowerInvariant() switch
{
"=" => Operation.Equals,
"==" => Operation.Equals,
":" => Operation.Equals,
"::" => Operation.Equals,
"!" => Operation.NotEquals,
"!=" => Operation.NotEquals,
"!:" => Operation.NotEquals,
">" => Operation.GreaterThan,
">=" => Operation.GreaterThanOrEqual,
"<" => Operation.LessThan,
"<=" => Operation.LessThanOrEqual,
_ => Operation.NONE,
};
}
/// <summary>
/// Derive a key, operation, and value from the input string, if possible
/// </summary>
private static bool SplitFilterString(string? filterString, out string? key, out Operation operation, out string? value)
{
// Set default values
key = null; operation = Operation.NONE; value = null;
if (string.IsNullOrEmpty(filterString))
return false;
// Trim quotations, if necessary
#if NETCOREAPP || NETSTANDARD2_1_OR_GREATER
if (filterString!.StartsWith('\"'))
filterString = filterString[1..^1];
#else
if (filterString!.StartsWith("\""))
filterString = filterString.Substring(1, filterString.Length - 2);
#endif
// Split the string using regex
var match = Regex.Match(filterString, @"^(?<itemField>[a-zA-Z._]+)(?<operation>[=!:><]{1,2})(?<value>.*)$", RegexOptions.Compiled);
if (!match.Success)
return false;
key = match.Groups["itemField"].Value;
operation = GetOperation(match.Groups["operation"].Value);
// Only non-zero length values are counted as non-null
if (match.Groups["value"]?.Value?.Length > 0)
value = match.Groups["value"].Value;
return true;
}
#endregion
}
}

View File

@@ -0,0 +1,105 @@
using System;
using System.Collections.Generic;
using SabreTools.Data.Models.Metadata;
using SabreTools.Metadata.Tools;
namespace SabreTools.Metadata.Filter
{
/// <summary>
/// Represents a set of filters that can be run against an object
/// </summary>
public class FilterRunner
{
/// <summary>
/// Set of filters to be run against an object
/// </summary>
public readonly Dictionary<string, FilterGroup> Filters = [];
/// <summary>
/// Cached item type names for filter selection
/// </summary>
private readonly string[] _datItemTypeNames = TypeHelper.GetDatItemTypeNames();
public FilterRunner(FilterObject[] filters)
{
Array.ForEach(filters, AddFilter);
}
public FilterRunner(string[] filterStrings)
{
Array.ForEach(filterStrings, AddFilter);
}
/// <summary>
/// Run filtering on a DictionaryBase item
/// </summary>
public bool Run(DictionaryBase dictionaryBase)
{
string? itemName = dictionaryBase switch
{
Header => MetadataFile.HeaderKey,
Machine => MetadataFile.MachineKey,
DatItem => TypeHelper.GetXmlRootAttributeElementName(dictionaryBase.GetType()),
_ => null,
};
// Null is invalid
if (itemName is null)
return false;
// Loop through and run each filter in order
foreach (var filterKey in Filters.Keys)
{
// Skip filters not applicable to the item
if (filterKey.StartsWith("item.") && Array.IndexOf(_datItemTypeNames, itemName) == -1)
continue;
else if (!filterKey.StartsWith("item.") && !filterKey.StartsWith(itemName))
continue;
// If we don't get a match, it's a failure
bool matchOne = Filters[filterKey].Matches(dictionaryBase);
if (!matchOne)
return false;
}
return true;
}
/// <summary>
/// Add a single filter to the runner in a group by key
/// </summary>
private void AddFilter(FilterObject filter)
{
// Get the key as a string
string key = filter.Key.ToString();
// Special case for machine types
if (filter.Key.ItemName == MetadataFile.MachineKey && filter.Key.FieldName == Machine.IsBiosKey)
key = $"{MetadataFile.MachineKey}.COMBINEDTYPE";
else if (filter.Key.ItemName == MetadataFile.MachineKey && filter.Key.FieldName == Machine.IsDeviceKey)
key = $"{MetadataFile.MachineKey}.COMBINEDTYPE";
else if (filter.Key.ItemName == MetadataFile.MachineKey && filter.Key.FieldName == Machine.IsMechanicalKey)
key = $"{MetadataFile.MachineKey}.COMBINEDTYPE";
// Ensure the key exists
if (!Filters.ContainsKey(key))
Filters[key] = new FilterGroup(GroupType.OR);
// Add the filter to the set
Filters[key].AddFilter(filter);
}
/// <summary>
/// Add a single filter to the runner in a group by key
/// </summary>
private void AddFilter(string filterString)
{
try
{
var filter = new FilterObject(filterString);
AddFilter(filter);
}
catch { }
}
}
}

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;net9.0;net10.0;netstandard2.0;netstandard2.1</TargetFrameworks>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
<CheckEolTargetFramework>false</CheckEolTargetFramework>
<IncludeSourceRevisionInInformationalVersion>false</IncludeSourceRevisionInInformationalVersion>
<IncludeSymbols>true</IncludeSymbols>
<LangVersion>latest</LangVersion>
<Nullable>enable</Nullable>
<SuppressTfmSupportBuildWarnings>true</SuppressTfmSupportBuildWarnings>
<SymbolPackageFormat>snupkg</SymbolPackageFormat>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
<Version>2.3.0</Version>
<!-- Package Properties -->
<Authors>Matt Nadareski</Authors>
<Description>Filter functionality for metadata file processing</Description>
<Copyright>Copyright (c) Matt Nadareski 2016-2026</Copyright>
<PackageProjectUrl>https://github.com/SabreTools/</PackageProjectUrl>
<PackageReadmeFile>README.md</PackageReadmeFile>
<RepositoryUrl>https://github.com/SabreTools/SabreTools.Serialization</RepositoryUrl>
<RepositoryType>git</RepositoryType>
<PackageTags>metadata dat datfile</PackageTags>
<PackageLicenseExpression>MIT</PackageLicenseExpression>
</PropertyGroup>
<ItemGroup>
<None Include="README.md" Pack="true" PackagePath="" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\SabreTools.Metadata\SabreTools.Metadata.csproj" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="SabreTools.IO" Version="[2.0.0]" />
</ItemGroup>
</Project>