fix(search): reject parentheses nested deeper than 64 levels
Deep nesting overflowed the stack of the recursive Pidgin parser (around 400-700 levels), an uncatchable crash. The depth is now checked on the token stream before parsing and reported as a syntax error. Listed in PLAN 6.2 and USAGE.
This commit is contained in:
1 parent
8ae9780c0e
commit
d2a5d36bd2
5 files changed
+70
-2
No files matched your search
@@ -221,6 +221,7 @@ These are invalid:
|
||||
| Problem | Example |
|
||||
|---|---|
|
||||
| Unbalanced parentheses | `(tag:bug`, `tag:bug)` |
|
||||
| Parentheses nested more than 64 levels deep | `((((…a…))))` with 65 `(` |
|
||||
| An operator with a missing side | `bug AND`, `OR bug` |
|
||||
| Consecutive operators | `a AND OR b` |
|
||||
| A qualifier without a value | `tag:` |
|
||||
|
||||
@@ -227,6 +227,33 @@ public class SearchQueryParserTests
|
||||
AssertSyntaxError("a)");
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void NestingAtTheDepthLimitParses()
|
||||
{
|
||||
int depth = SearchQueryParser.MaxNestingDepth;
|
||||
AssertParsesTo(new SearchNode.FreeText("a"), new string('(', depth) + "a" + new string(')', depth));
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void NestingBeyondTheDepthLimitIsAnError()
|
||||
{
|
||||
int depth = SearchQueryParser.MaxNestingDepth + 1;
|
||||
AssertSyntaxError(new string('(', depth) + "a" + new string(')', depth));
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void SequentialGroupsDoNotCountTowardsTheDepthLimit()
|
||||
{
|
||||
string groups = string.Join(" ", Enumerable.Repeat("(a)", SearchQueryParser.MaxNestingDepth + 1));
|
||||
Assert.IsNotNull(SearchQueryParser.Parse(groups));
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void VeryDeepNestingIsAnErrorRatherThanACrash()
|
||||
{
|
||||
AssertSyntaxError(new string('(', 100_000) + "a" + new string(')', 100_000));
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void EmptyParensAreAnError()
|
||||
{
|
||||
|
||||
@@ -8,8 +8,9 @@ namespace YKanBan.Search;
|
||||
/// (unbalanced parentheses, dangling or consecutive operators, unterminated
|
||||
/// phrases, invalid escapes, a backslash in a bare word, unknown qualifier
|
||||
/// keys, qualifiers without a value, unquoted values containing ':' or equal
|
||||
/// to AND/OR, and id values that are not an unquoted positive integer with an
|
||||
/// optional '#') raises <see cref="SearchSyntaxException"/>.
|
||||
/// to AND/OR, id values that are not an unquoted positive integer with an
|
||||
/// optional '#', and parentheses nested deeper than <see cref="MaxNestingDepth"/>)
|
||||
/// raises <see cref="SearchSyntaxException"/>.
|
||||
///
|
||||
/// Only uppercase AND/OR are operators (lowercase ones are plain words); AND —
|
||||
/// explicit or by adjacency — binds tighter than OR; parentheses group. Blank
|
||||
@@ -18,6 +19,12 @@ namespace YKanBan.Search;
|
||||
/// </summary>
|
||||
public static class SearchQueryParser
|
||||
{
|
||||
/// <summary>
|
||||
/// Deepest accepted parenthesis nesting. The parser recurses once per level,
|
||||
/// so an unbounded depth would overflow the stack and kill the process.
|
||||
/// </summary>
|
||||
public const int MaxNestingDepth = 64;
|
||||
|
||||
/// <summary>The recognized qualifier keys; add new entries here to extend the grammar.</summary>
|
||||
private static readonly (string Key, QualifierKind Kind)[] QualifierTable =
|
||||
[
|
||||
@@ -114,6 +121,7 @@ public static class SearchQueryParser
|
||||
try
|
||||
{
|
||||
List<SearchToken> tokens = SearchLexer.Tokenize(text).ToList();
|
||||
EnsureNestingWithinLimit(tokens);
|
||||
return Query.ParseOrThrow(tokens);
|
||||
}
|
||||
catch (ParseException exception)
|
||||
@@ -122,6 +130,28 @@ public static class SearchQueryParser
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Rejects parenthesis nesting deeper than <see cref="MaxNestingDepth"/>
|
||||
/// before the recursive parser sees it. Balance itself is left to the parser.
|
||||
/// </summary>
|
||||
/// <param name="tokens">The token stream.</param>
|
||||
/// <exception cref="SearchSyntaxException">The nesting is too deep.</exception>
|
||||
private static void EnsureNestingWithinLimit(IEnumerable<SearchToken> tokens)
|
||||
{
|
||||
int depth = 0;
|
||||
foreach (SearchToken token in tokens)
|
||||
{
|
||||
if (token is SearchToken.OpenParen && ++depth > MaxNestingDepth)
|
||||
{
|
||||
throw new SearchSyntaxException($"Parentheses are nested more than {MaxNestingDepth} levels deep.");
|
||||
}
|
||||
if (token is SearchToken.CloseParen && depth > 0)
|
||||
{
|
||||
depth--;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#region Atom construction
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -8,6 +8,15 @@ namespace YKanBan.Search;
|
||||
/// </summary>
|
||||
public sealed class SearchSyntaxException : Exception
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes the exception with a descriptive English message.
|
||||
/// </summary>
|
||||
/// <param name="message">The English diagnostic message.</param>
|
||||
public SearchSyntaxException(string message)
|
||||
: base(message)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes the exception with a descriptive English message.
|
||||
/// </summary>
|
||||
|
||||
@@ -291,6 +291,7 @@ phrase := '"' 任意文本 '"' (内部支持转义:\" → 字面引号,
|
||||
- 短语内部转义:`\"` → 字面引号,`\\` → 字面反斜杠;搜索含空格/保留字符/字面 `AND` 等特殊标签名时,用(必要时转义的)引号值(写入文档)
|
||||
- **严格校验**:非法表达式弹出错误对话框,不执行搜索。非法集合:
|
||||
- 括号不配对
|
||||
- 括号嵌套超过 64 层(防止递归解析栈溢出;顺序并列的括号组不计入深度)
|
||||
- 运算符悬空(`bug AND`)、连续运算符(`a AND OR b`)
|
||||
- 限定符无值(`tag:`)、限定符值为裸 `AND`/`OR`(`tag:AND`,须写 `tag:"AND"`)
|
||||
- 短语引号未闭合、无效转义(`tag:"a\x"`)、裸词中出现 `\`
|
||||
|
||||
Reference in new issue
Block a user