namespace ParadoxSaveParser.Lib; /// /// 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. /// public readonly ref struct MatchCandidate { public readonly ReadOnlySpan Key; public readonly int Index; /// Encoding is written in. public readonly Encoding Encoding; /// A list item, which has a position but no key. public MatchCandidate(int index) { Key = default; Index = index; Encoding = Encoding.Latin1; } public MatchCandidate(int index, ReadOnlySpan 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 chars, int i) => chars[i] == c && (i < 1 || chars[i - 1] != '\\') && (i < 2 || chars[i - 2] != '\\'); public static ISearchExpression Compile(ReadOnlySpan query) { if (query.IsEmpty) throw new ArgumentNullException(nameof(query)); if (query[0] is '(') { var subExprs = new List(); 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 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 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; } } }