188 lines
6.5 KiB
C#
188 lines
6.5 KiB
C#
namespace ParadoxSaveParser.Lib;
|
|
|
|
/// <summary>
|
|
/// The node a search expression is tested against: its key inside the parent dictionary,
|
|
/// still in the raw bytes of the save, and its position inside the parent list.
|
|
/// Keys stay undecoded so that a key rejected by the query never becomes a string.
|
|
/// </summary>
|
|
public readonly ref struct MatchCandidate
|
|
{
|
|
public readonly ReadOnlySpan<byte> Key;
|
|
public readonly int Index;
|
|
|
|
/// <summary>Encoding <see cref="Key" /> is written in.</summary>
|
|
public readonly Encoding Encoding;
|
|
|
|
/// <summary>A list item, which has a position but no key.</summary>
|
|
public MatchCandidate(int index)
|
|
{
|
|
Key = default;
|
|
Index = index;
|
|
Encoding = Encoding.Latin1;
|
|
}
|
|
|
|
public MatchCandidate(int index, ReadOnlySpan<byte> key, Encoding encoding)
|
|
{
|
|
Key = key;
|
|
Index = index;
|
|
Encoding = encoding;
|
|
}
|
|
}
|
|
|
|
public interface ISearchExpression
|
|
{
|
|
bool DoesMatch(MatchCandidate candidate, out ISearchExpression? nextSearchExpression);
|
|
}
|
|
|
|
public static class SearchExpressionCompiler
|
|
{
|
|
private static bool CharEqualsAndNotEscaped(char c, ReadOnlySpan<char> chars, int i)
|
|
=> chars[i] == c && (i < 1 || chars[i - 1] != '\\') && (i < 2 || chars[i - 2] != '\\');
|
|
|
|
public static ISearchExpression Compile(ReadOnlySpan<char> query)
|
|
{
|
|
if (query.IsEmpty)
|
|
throw new ArgumentNullException(nameof(query));
|
|
|
|
if (query[0] is '(')
|
|
{
|
|
var subExprs = new List<ISearchExpression>();
|
|
int supExprBegin = 1;
|
|
int bracketBalance = 1;
|
|
int i = supExprBegin;
|
|
for (; i < query.Length && bracketBalance != 0; i++)
|
|
{
|
|
if (CharEqualsAndNotEscaped('(', query, i))
|
|
{
|
|
bracketBalance++;
|
|
}
|
|
else if (CharEqualsAndNotEscaped(')', query, i))
|
|
{
|
|
bracketBalance--;
|
|
}
|
|
else if (bracketBalance == 1 && CharEqualsAndNotEscaped('|', query, i))
|
|
{
|
|
var subPart = query.Slice(supExprBegin, i - supExprBegin);
|
|
var subExpr = Compile(subPart);
|
|
subExprs.Add(subExpr);
|
|
supExprBegin = i + 1;
|
|
}
|
|
}
|
|
|
|
if (i != query.Length)
|
|
throw new NotImplementedException("Expressions after ')' are not supported");
|
|
|
|
if (bracketBalance > 0)
|
|
throw new Exception("Too many opening brackets");
|
|
if (bracketBalance < 0)
|
|
throw new Exception("Too many closing brackets");
|
|
|
|
var subPartLast = query.Slice(supExprBegin, i - 1 - supExprBegin);
|
|
var subExprLast = Compile(subPartLast);
|
|
subExprs.Add(subExprLast);
|
|
return new MultipleMatchExpression(subExprs);
|
|
}
|
|
|
|
int partBeforePointLength = 0;
|
|
while (partBeforePointLength < query.Length)
|
|
{
|
|
if (CharEqualsAndNotEscaped('.', query, partBeforePointLength))
|
|
break;
|
|
partBeforePointLength++;
|
|
}
|
|
|
|
var part = query.Slice(0, partBeforePointLength);
|
|
ReadOnlySpan<char> remaining = default;
|
|
if (partBeforePointLength < query.Length)
|
|
remaining = query.Slice(partBeforePointLength + 1);
|
|
if (part is "*")
|
|
return new AnyMatchExpression(remaining.IsEmpty ? null : Compile(remaining));
|
|
if (part is "~")
|
|
return new NoMatchExpression();
|
|
|
|
for (int j = 0; j < part.Length; j++)
|
|
if (CharEqualsAndNotEscaped('*', part, j))
|
|
throw new NotImplementedException("pattern matching other than '*' is not implemented yet");
|
|
|
|
if (part[0] is '[')
|
|
{
|
|
part = part.Slice(1, part.Length - 2);
|
|
return new IndexMatchExpression(int.Parse(part), remaining.IsEmpty ? null : Compile(remaining));
|
|
}
|
|
|
|
return new ExactMatchExpression(part.ToString(), remaining.IsEmpty ? null : Compile(remaining));
|
|
}
|
|
|
|
|
|
private record AnyMatchExpression(ISearchExpression? next) : ISearchExpression
|
|
{
|
|
public bool DoesMatch(MatchCandidate candidate, out ISearchExpression? nextSearchExpression)
|
|
{
|
|
nextSearchExpression = next;
|
|
return true;
|
|
}
|
|
}
|
|
|
|
private record NoMatchExpression : ISearchExpression
|
|
{
|
|
public bool DoesMatch(MatchCandidate candidate, out ISearchExpression? nextSearchExpression)
|
|
{
|
|
nextSearchExpression = null;
|
|
return false;
|
|
}
|
|
}
|
|
|
|
private record MultipleMatchExpression(List<ISearchExpression> subExprs) : ISearchExpression
|
|
{
|
|
public bool DoesMatch(MatchCandidate candidate, out ISearchExpression? nextSearchExpression)
|
|
{
|
|
foreach (var e in subExprs)
|
|
if (e.DoesMatch(candidate, out nextSearchExpression))
|
|
return true;
|
|
|
|
nextSearchExpression = null;
|
|
return false;
|
|
}
|
|
}
|
|
|
|
private record IndexMatchExpression(int index, ISearchExpression? next) : ISearchExpression
|
|
{
|
|
public bool DoesMatch(MatchCandidate candidate, out ISearchExpression? nextSearchExpression)
|
|
{
|
|
if (candidate.Index == index)
|
|
{
|
|
nextSearchExpression = next;
|
|
return true;
|
|
}
|
|
|
|
nextSearchExpression = null;
|
|
return false;
|
|
}
|
|
}
|
|
|
|
private record ExactMatchExpression(string key, ISearchExpression? next) : ISearchExpression
|
|
{
|
|
// the key is compared as bytes, so it is encoded once for whatever encoding the
|
|
// parser reads the save in
|
|
private byte[]? _keyBytes;
|
|
private Encoding? _keyEncoding;
|
|
|
|
public bool DoesMatch(MatchCandidate candidate, out ISearchExpression? nextSearchExpression)
|
|
{
|
|
if (!ReferenceEquals(_keyEncoding, candidate.Encoding))
|
|
{
|
|
_keyEncoding = candidate.Encoding;
|
|
_keyBytes = candidate.Encoding.GetBytes(key);
|
|
}
|
|
|
|
if (candidate.Key.SequenceEqual(_keyBytes))
|
|
{
|
|
nextSearchExpression = next;
|
|
return true;
|
|
}
|
|
|
|
nextSearchExpression = null;
|
|
return false;
|
|
}
|
|
}
|
|
} |