mirror of
https://github.com/SabreTools/SabreTools.Serialization.git
synced 2026-07-09 02:16:55 +00:00
Port metadata functionality from ST
This commit is contained in:
41
SabreTools.Metadata.Filter/Enums.cs
Normal file
41
SabreTools.Metadata.Filter/Enums.cs
Normal 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,
|
||||
}
|
||||
}
|
||||
126
SabreTools.Metadata.Filter/ExtraIniItem.cs
Normal file
126
SabreTools.Metadata.Filter/ExtraIniItem.cs
Normal 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
|
||||
}
|
||||
}
|
||||
198
SabreTools.Metadata.Filter/FilterGroup.cs
Normal file
198
SabreTools.Metadata.Filter/FilterGroup.cs
Normal 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
|
||||
}
|
||||
}
|
||||
214
SabreTools.Metadata.Filter/FilterKey.cs
Normal file
214
SabreTools.Metadata.Filter/FilterKey.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
404
SabreTools.Metadata.Filter/FilterObject.cs
Normal file
404
SabreTools.Metadata.Filter/FilterObject.cs
Normal 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
|
||||
}
|
||||
}
|
||||
105
SabreTools.Metadata.Filter/FilterRunner.cs
Normal file
105
SabreTools.Metadata.Filter/FilterRunner.cs
Normal 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 { }
|
||||
}
|
||||
}
|
||||
}
|
||||
41
SabreTools.Metadata.Filter/SabreTools.Metadata.Filter.csproj
Normal file
41
SabreTools.Metadata.Filter/SabreTools.Metadata.Filter.csproj
Normal 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>
|
||||
Reference in New Issue
Block a user