diff --git a/changelog.d/unreleased/2904.fixed.md b/changelog.d/unreleased/2904.fixed.md new file mode 100644 index 0000000000..05f3fa5ea6 --- /dev/null +++ b/changelog.d/unreleased/2904.fixed.md @@ -0,0 +1,16 @@ +--- +category: fixed +issues: + - 2904 +affected: + - src/CodeIndex/Cli/IndexCommandRunner.Parse.cs + - tests/CodeIndex.Tests/IndexCommandRunnerTests.cs +--- + +## English + +- **index parallelism now clamps oversized worker counts (#2904)** — `--parallelism` and `CDIDX_INDEX_PARALLELISM` values above the documented maximum warn and use the bounded worker count. + +## 日本語 + +- **index parallelism が過大な worker 数を clamp するようになりました (#2904)** — `--parallelism` と `CDIDX_INDEX_PARALLELISM` が上限を超えた場合は warning を出し、制限された worker 数を使用します。 diff --git a/changelog.d/unreleased/2905.fixed.md b/changelog.d/unreleased/2905.fixed.md new file mode 100644 index 0000000000..65bdb9177e --- /dev/null +++ b/changelog.d/unreleased/2905.fixed.md @@ -0,0 +1,16 @@ +--- +category: fixed +issues: + - 2905 +affected: + - src/CodeIndex/Mcp/McpToolFilter.cs + - tests/CodeIndex.Tests/McpServerTests.cs +--- + +## English + +- **MCP tool allow/deny filters now cap environment CSV input (#2905)** — overlong or overlarge MCP tool filter values are rejected before splitting; invalid `CDIDX_MCP_TOOLS_ALLOW` fails closed with no tools enabled, while invalid `CDIDX_MCP_TOOLS_DENY` leaves the default enabled set unchanged. + +## 日本語 + +- **MCP tool allow/deny filter が環境変数 CSV 入力を制限するようになりました (#2905)** — 長すぎる、または entry 数が多すぎる MCP tool filter 値は split 前に拒否されます。無効な `CDIDX_MCP_TOOLS_ALLOW` は全 tool 無効の fail-closed となり、無効な `CDIDX_MCP_TOOLS_DENY` は既定の有効集合を維持します。 diff --git a/changelog.d/unreleased/2906.fixed.md b/changelog.d/unreleased/2906.fixed.md new file mode 100644 index 0000000000..eac80271f9 --- /dev/null +++ b/changelog.d/unreleased/2906.fixed.md @@ -0,0 +1,16 @@ +--- +category: fixed +issues: + - 2906 +affected: + - src/CodeIndex/Cli/IndexCommandRunner.Parse.cs + - tests/CodeIndex.Tests/IndexCommandRunnerTests.cs +--- + +## English + +- **index symbol-kind filters now cap CSV input before splitting (#2906)** — CLI and environment include/exclude symbol-kind lists reject oversized values before materializing entries. + +## 日本語 + +- **index の symbol-kind filter が split 前に CSV 入力を制限するようになりました (#2906)** — CLI と環境変数の include/exclude symbol-kind list は、entry を materialize する前に過大な値を拒否します。 diff --git a/changelog.d/unreleased/2911.fixed.md b/changelog.d/unreleased/2911.fixed.md new file mode 100644 index 0000000000..0900d9718c --- /dev/null +++ b/changelog.d/unreleased/2911.fixed.md @@ -0,0 +1,16 @@ +--- +category: fixed +issues: + - 2911 +affected: + - src/CodeIndex/Cli/QueryCommandRunner.cs + - tests/CodeIndex.Tests/QueryCommandRunnerTests.cs +--- + +## English + +- **CLI path filters now cap pattern count and length before SQL construction (#2911)** — `--path` and `--exclude-path` reject excessive repeated patterns or overlong values during argument validation. + +## 日本語 + +- **CLI path filter が SQL 構築前に pattern 数と長さを制限するようになりました (#2911)** — `--path` と `--exclude-path` は、過剰な繰り返し pattern や長すぎる値を引数検証時に拒否します。 diff --git a/changelog.d/unreleased/2912.fixed.md b/changelog.d/unreleased/2912.fixed.md new file mode 100644 index 0000000000..9e6d406cd6 --- /dev/null +++ b/changelog.d/unreleased/2912.fixed.md @@ -0,0 +1,16 @@ +--- +category: fixed +issues: + - 2912 +affected: + - src/CodeIndex/Cli/QueryCommandRunner.cs + - tests/CodeIndex.Tests/QueryCommandRunnerTests.cs +--- + +## English + +- **CLI visibility filters now cap CSV input before splitting (#2912)** — `--visibility` and `--exclude-visibility` reject overlong or overlarge lists before deduplication. + +## 日本語 + +- **CLI visibility filter が split 前に CSV 入力を制限するようになりました (#2912)** — `--visibility` と `--exclude-visibility` は、長すぎる、または entry 数が多すぎる list を dedup 前に拒否します。 diff --git a/changelog.d/unreleased/2913.fixed.md b/changelog.d/unreleased/2913.fixed.md new file mode 100644 index 0000000000..6cc0aab392 --- /dev/null +++ b/changelog.d/unreleased/2913.fixed.md @@ -0,0 +1,16 @@ +--- +category: fixed +issues: + - 2913 +affected: + - src/CodeIndex/Cli/QueryCommandRunner.cs + - tests/CodeIndex.Tests/QueryCommandRunnerTests.cs +--- + +## English + +- **`status --check=` now caps CSV input before splitting (#2913)** — overlong or overlarge scope lists fail with a usage error before allocating individual scope entries. + +## 日本語 + +- **`status --check=` が split 前に CSV 入力を制限するようになりました (#2913)** — 長すぎる、または entry 数が多すぎる scope list は個別 entry を割り当てる前に usage error として失敗します。 diff --git a/changelog.d/unreleased/2914.fixed.md b/changelog.d/unreleased/2914.fixed.md new file mode 100644 index 0000000000..69770c12e4 --- /dev/null +++ b/changelog.d/unreleased/2914.fixed.md @@ -0,0 +1,16 @@ +--- +category: fixed +issues: + - 2914 +affected: + - src/CodeIndex/Cli/QueryCommandRunner.cs + - tests/CodeIndex.Tests/QueryCommandRunnerTests.cs +--- + +## English + +- **repo map `--sections` now caps CSV input before splitting (#2914)** — oversized section lists are rejected with a usage error before allocating entries beyond the documented bound. + +## 日本語 + +- **repo map の `--sections` が split 前に CSV 入力を制限するようになりました (#2914)** — 過大な section list は、上限を超える entry を割り当てる前に usage error として拒否されます。 diff --git a/src/CodeIndex/Cli/IndexCommandRunner.Parse.cs b/src/CodeIndex/Cli/IndexCommandRunner.Parse.cs index 763483a696..259073abd1 100644 --- a/src/CodeIndex/Cli/IndexCommandRunner.Parse.cs +++ b/src/CodeIndex/Cli/IndexCommandRunner.Parse.cs @@ -22,6 +22,9 @@ public static partial class IndexCommandRunner ]; internal const string IndexParallelismEnvironmentVariable = "CDIDX_INDEX_PARALLELISM"; + internal const int MaxIndexParallelism = 16; + internal const int MaxSymbolKindFilterCsvLength = 2048; + internal const int MaxSymbolKindFilterCsvEntries = 128; public static IndexCommandOptions ParseArgs(string[] args) { @@ -356,6 +359,8 @@ private static void AddSymbolKindFilterValues(string source, string? value, List { if (value == null) return; + if (!ValidateCsvBounds(source, value, MaxSymbolKindFilterCsvLength, MaxSymbolKindFilterCsvEntries, ref parseError)) + return; foreach (var raw in value.Split(',', StringSplitOptions.TrimEntries)) { @@ -369,8 +374,46 @@ private static void AddSymbolKindFilterValues(string source, string? value, List } } + private static bool ValidateCsvBounds( + string source, + string value, + int maxLength, + int maxEntries, + ref string? parseError) + { + if (value.Length > maxLength) + { + parseError ??= $"{source} value is too long ({value.Length} characters; max {maxLength})"; + return false; + } + + var entries = CountCsvEntries(value); + if (entries > maxEntries) + { + parseError ??= $"{source} accepts at most {maxEntries} comma-separated entries"; + return false; + } + + return true; + } + + private static int CountCsvEntries(string value) + { + if (value.Length == 0) + return 0; + + var count = 1; + foreach (var ch in value) + { + if (ch == ',') + count++; + } + + return count; + } + internal static int DefaultIndexParallelism() - => Math.Clamp(Environment.ProcessorCount, 1, 16); + => Math.Clamp(Environment.ProcessorCount, 1, MaxIndexParallelism); private static int ReadIndexParallelismFromEnvironment() { @@ -385,7 +428,13 @@ private static int ReadIndexParallelismFromEnvironment() private static int ParseIndexParallelism(string value, int fallback, string source) { if (int.TryParse(value, System.Globalization.NumberStyles.Integer, System.Globalization.CultureInfo.InvariantCulture, out var parsed) && parsed > 0) - return parsed; + { + if (parsed <= MaxIndexParallelism) + return parsed; + + Console.Error.WriteLine($"Warning: {source} value '{value}' exceeds the maximum {MaxIndexParallelism}; using {MaxIndexParallelism} / {source} 値 '{value}' は最大 {MaxIndexParallelism} を超えています。{MaxIndexParallelism} を使用します"); + return MaxIndexParallelism; + } Console.Error.WriteLine($"Warning: invalid {source} value '{value}' (ignored; use a positive integer) / 不正な {source} 値 '{value}'(無視。正の整数を指定)"); return fallback; diff --git a/src/CodeIndex/Cli/QueryCommandRunner.cs b/src/CodeIndex/Cli/QueryCommandRunner.cs index 5cf60d1ca2..b4f8a0cb34 100644 --- a/src/CodeIndex/Cli/QueryCommandRunner.cs +++ b/src/CodeIndex/Cli/QueryCommandRunner.cs @@ -37,6 +37,14 @@ public static class QueryCommandRunner // OR 結合の `symbols` 名は SQLite の式木深さ上限 1000 を十分下回る値で頭打ちにし、 // 大量バッチを SQLite 例外ではなく明確な usage error で早期に弾く。 internal const int MaxSymbolQueryNames = 256; + internal const int MaxMapSectionsCsvLength = 256; + internal const int MaxMapSectionsCsvEntries = 16; + internal const int MaxStatusCheckScopesCsvLength = 256; + internal const int MaxStatusCheckScopesCsvEntries = 16; + internal const int MaxVisibilityFilterCsvLength = 256; + internal const int MaxVisibilityFilterCsvEntries = 16; + internal const int MaxQueryPathFilterCount = 128; + internal const int MaxQueryPathFilterLength = 1024; internal const int ExactZeroHintProbeLimit = 1; internal const int ExactZeroHintSampleLimit = 5; private const string HotspotsGroupedByNameKind = "name_kind"; @@ -4979,6 +4987,8 @@ void AddStatusCheckScopes(string rawScopes) AddParseError("Error: --check scope list cannot be empty. Use --check or --check=workspace,fold,graph,issues,hotspot,csharp,sql,newer."); return; } + if (!ValidateCsvBounds("--check", rawScopes, MaxStatusCheckScopesCsvLength, MaxStatusCheckScopesCsvEntries, AddParseError)) + return; statusCheckScopes ??= new HashSet(StringComparer.OrdinalIgnoreCase); foreach (var rawScope in rawScopes.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries)) @@ -5684,6 +5694,9 @@ void WarnIfDuplicateSingleValueOption(string canonicalName, string newValue) private static List ParseMapSections(string rawValue, Action addParseError) { var sections = new List(); + if (!ValidateCsvBounds("--sections", rawValue, MaxMapSectionsCsvLength, MaxMapSectionsCsvEntries, addParseError)) + return sections; + foreach (var rawSection in rawValue.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries)) { var section = rawSection.ToLowerInvariant(); @@ -5709,15 +5722,71 @@ private static List ParseMapSections(string rawValue, Action add return sections.Distinct(StringComparer.Ordinal).ToList(); } + private static bool ValidateCsvBounds( + string optionName, + string rawValue, + int maxLength, + int maxEntries, + Action addParseError) + { + if (rawValue.Length > maxLength) + { + addParseError($"Error: {optionName} value is too long ({rawValue.Length} characters; max {maxLength})."); + return false; + } + + var entries = CountCsvEntries(rawValue); + if (entries > maxEntries) + { + addParseError($"Error: {optionName} accepts at most {maxEntries} comma-separated entries."); + return false; + } + + return true; + } + + private static int CountCsvEntries(string rawValue) + { + if (rawValue.Length == 0) + return 0; + + var count = 1; + foreach (var ch in rawValue) + { + if (ch == ',') + count++; + } + + return count; + } + private static void ValidateQueryPathOptionValues( IReadOnlyList pathPatterns, IReadOnlyList excludePaths, Action addParseError) { - foreach (var pattern in pathPatterns) - ValidatePathGlobPattern("--path", pattern, addParseError); - foreach (var pattern in excludePaths) - ValidatePathGlobPattern("--exclude-path", pattern, addParseError); + ValidatePathOptionValues("--path", pathPatterns, addParseError); + ValidatePathOptionValues("--exclude-path", excludePaths, addParseError); + } + + private static void ValidatePathOptionValues( + string optionName, + IReadOnlyList patterns, + Action addParseError) + { + if (patterns.Count > MaxQueryPathFilterCount) + addParseError($"Error: {optionName} accepts at most {MaxQueryPathFilterCount} values."); + + foreach (var pattern in patterns) + { + if (pattern.Length > MaxQueryPathFilterLength) + { + addParseError($"Error: {optionName} value is too long ({pattern.Length} characters; max {MaxQueryPathFilterLength})."); + continue; + } + + ValidatePathGlobPattern(optionName, pattern, addParseError); + } } private static bool TryParseJsonOutputFormat(string rawValue, out string format) @@ -6442,6 +6511,9 @@ private static bool TryWriteParseError(QueryCommandOptions options, string comma private static void AddVisibilityFilterValues(string optionName, string rawValue, List target, Action addParseError) { + if (!ValidateCsvBounds(optionName, rawValue, MaxVisibilityFilterCsvLength, MaxVisibilityFilterCsvEntries, addParseError)) + return; + var values = rawValue .Split(',', StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries) .Select(value => value.ToLowerInvariant()) diff --git a/src/CodeIndex/Mcp/McpToolFilter.cs b/src/CodeIndex/Mcp/McpToolFilter.cs index ff67209120..45f99db232 100644 --- a/src/CodeIndex/Mcp/McpToolFilter.cs +++ b/src/CodeIndex/Mcp/McpToolFilter.cs @@ -20,6 +20,8 @@ public sealed class McpToolFilter { internal const string AllowEnvVarName = "CDIDX_MCP_TOOLS_ALLOW"; internal const string DenyEnvVarName = "CDIDX_MCP_TOOLS_DENY"; + internal const int MaxToolFilterCsvLength = 2048; + internal const int MaxToolFilterCsvEntries = 128; private readonly HashSet _enabled; @@ -71,10 +73,9 @@ public static McpToolFilter AllowAll() => /// /// Build a filter from `CDIDX_MCP_TOOLS_ALLOW` / `CDIDX_MCP_TOOLS_DENY`. When both are - /// unset or only contain unknown names, returns so default - /// behavior is preserved. + /// unset, returns so default behavior is preserved. /// `CDIDX_MCP_TOOLS_ALLOW` / `CDIDX_MCP_TOOLS_DENY` から filter を組み立てる。両方とも - /// 未指定、または未知の名前しか含まない場合は を返し既定挙動を保つ。 + /// 未指定の場合は を返し既定挙動を保つ。 /// public static McpToolFilter FromEnvironment() => Parse( @@ -83,9 +84,12 @@ public static McpToolFilter FromEnvironment() => internal static McpToolFilter Parse(string? allowValue, string? denyValue) { - var allow = SplitCsv(allowValue); - if (allow.Count > 0) + var allow = SplitCsv(allowValue, AllowEnvVarName, out var allowSpecified, out var allowInvalid); + if (allowSpecified) { + if (allowInvalid) + return new McpToolFilter(new HashSet(StringComparer.OrdinalIgnoreCase)); + var filtered = new HashSet(StringComparer.OrdinalIgnoreCase); foreach (var name in KnownToolNames) { @@ -96,7 +100,7 @@ internal static McpToolFilter Parse(string? allowValue, string? denyValue) } var enabled = new HashSet(KnownToolNames, StringComparer.OrdinalIgnoreCase); - var deny = SplitCsv(denyValue); + var deny = SplitCsv(denyValue, DenyEnvVarName, out _, out _); if (deny.Count > 0) { foreach (var name in deny) @@ -121,11 +125,20 @@ public static bool IsKnownTool(string? toolName) => !string.IsNullOrEmpty(toolName) && KnownToolNames.Any(known => string.Equals(known, toolName, StringComparison.OrdinalIgnoreCase)); - private static HashSet SplitCsv(string? value) + private static HashSet SplitCsv(string? value, string source, out bool specified, out bool invalid) { var set = new HashSet(StringComparer.OrdinalIgnoreCase); + specified = !string.IsNullOrWhiteSpace(value); + invalid = false; + if (string.IsNullOrWhiteSpace(value)) return set; + if (!ValidateCsvBounds(source, value)) + { + invalid = true; + return set; + } + foreach (var raw in value.Split(',')) { var trimmed = raw.Trim(); @@ -135,4 +148,37 @@ private static HashSet SplitCsv(string? value) } return set; } + + private static bool ValidateCsvBounds(string source, string value) + { + if (value.Length > MaxToolFilterCsvLength) + { + Console.Error.WriteLine($"Warning: {source} is too long ({value.Length} characters; max {MaxToolFilterCsvLength}) and was rejected."); + return false; + } + + var entries = CountCsvEntries(value); + if (entries > MaxToolFilterCsvEntries) + { + Console.Error.WriteLine($"Warning: {source} accepts at most {MaxToolFilterCsvEntries} comma-separated entries and was rejected."); + return false; + } + + return true; + } + + private static int CountCsvEntries(string value) + { + if (value.Length == 0) + return 0; + + var count = 1; + foreach (var ch in value) + { + if (ch == ',') + count++; + } + + return count; + } } diff --git a/tests/CodeIndex.Tests/IndexCommandRunnerTests.cs b/tests/CodeIndex.Tests/IndexCommandRunnerTests.cs index f458efe366..7cd54df20e 100644 --- a/tests/CodeIndex.Tests/IndexCommandRunnerTests.cs +++ b/tests/CodeIndex.Tests/IndexCommandRunnerTests.cs @@ -599,6 +599,43 @@ public void ParseArgs_SymbolKindFilters_AcceptCommaSeparatedValues() Assert.Null(options.SymbolKindFilter.ParseError); } + [Fact] + public void ParseArgs_SymbolKindFilterRejectsOverlongCsv_Issue2906() + { + var tooLong = new string('c', IndexCommandRunner.MaxSymbolKindFilterCsvLength + 1); + + var options = IndexCommandRunner.ParseArgs([".", "--include-symbol-kind", tooLong]); + + Assert.Contains("--include-symbol-kind value is too long", options.SymbolKindFilter.ParseError); + Assert.Empty(options.SymbolKindFilter.Include); + } + + [Fact] + public void ParseArgs_SymbolKindFilterRejectsTooManyCsvEntries_Issue2906() + { + var tooMany = string.Join(',', Enumerable.Repeat("class", IndexCommandRunner.MaxSymbolKindFilterCsvEntries + 1)); + + var options = IndexCommandRunner.ParseArgs([".", "--exclude-symbol-kind", tooMany]); + + Assert.Contains("--exclude-symbol-kind accepts at most", options.SymbolKindFilter.ParseError); + Assert.Empty(options.SymbolKindFilter.Exclude); + } + + [Fact] + public void ParseArgs_SymbolKindEnvironmentFilterRejectsTooManyCsvEntries_Issue2906() + { + using var env = EnvironmentVariableScope.Capture(IndexCommandRunner.IncludeSymbolKindsEnvironmentVariable); + Environment.SetEnvironmentVariable( + IndexCommandRunner.IncludeSymbolKindsEnvironmentVariable, + string.Join(',', Enumerable.Repeat("function", IndexCommandRunner.MaxSymbolKindFilterCsvEntries + 1))); + + var options = IndexCommandRunner.ParseArgs(["."]); + + Assert.Contains(IndexCommandRunner.IncludeSymbolKindsEnvironmentVariable, options.SymbolKindFilter.ParseError); + Assert.Contains("accepts at most", options.SymbolKindFilter.ParseError); + Assert.Empty(options.SymbolKindFilter.Include); + } + [Fact] public void ParseArgs_SymbolKindCliFilters_ReplaceEnvironmentDefaults() { @@ -1261,6 +1298,30 @@ public void ParseArgs_ParallelismInlineFlag_ParsesPositiveValue() Assert.Equal(4, options.Parallelism); } + [Fact] + public void ParseArgs_ParallelismFlagClampsOversizedValue_Issue2904() + { + lock (TestConsoleLock.Gate) + { + var originalError = Console.Error; + using var stderr = new StringWriter(); + try + { + Console.SetError(stderr); + + var options = IndexCommandRunner.ParseArgs([".", "--parallelism", "999"]); + + Assert.Equal(IndexCommandRunner.MaxIndexParallelism, options.Parallelism); + Assert.Contains("--parallelism", stderr.ToString()); + Assert.Contains($"maximum {IndexCommandRunner.MaxIndexParallelism}", stderr.ToString()); + } + finally + { + Console.SetError(originalError); + } + } + } + [Fact] public void ParseArgs_IndexParallelismEnvironment_ProvidesDefault() { @@ -1279,6 +1340,32 @@ public void ParseArgs_IndexParallelismEnvironment_ProvidesDefault() } } + [Fact] + public void ParseArgs_IndexParallelismEnvironmentClampsOversizedValue_Issue2904() + { + lock (TestConsoleLock.Gate) + { + var originalError = Console.Error; + using var stderr = new StringWriter(); + using var env = EnvironmentVariableScope.Capture(IndexCommandRunner.IndexParallelismEnvironmentVariable); + try + { + Console.SetError(stderr); + Environment.SetEnvironmentVariable(IndexCommandRunner.IndexParallelismEnvironmentVariable, "999"); + + var options = IndexCommandRunner.ParseArgs(["."]); + + Assert.Equal(IndexCommandRunner.MaxIndexParallelism, options.Parallelism); + Assert.Contains(IndexCommandRunner.IndexParallelismEnvironmentVariable, stderr.ToString()); + Assert.Contains($"maximum {IndexCommandRunner.MaxIndexParallelism}", stderr.ToString()); + } + finally + { + Console.SetError(originalError); + } + } + } + [Theory] [InlineData("--duration-format", "auto", DurationOutputFormat.Auto)] [InlineData("--duration-format", "seconds", DurationOutputFormat.Seconds)] diff --git a/tests/CodeIndex.Tests/McpServerTests.cs b/tests/CodeIndex.Tests/McpServerTests.cs index e15f8ed74f..5f67c533cd 100644 --- a/tests/CodeIndex.Tests/McpServerTests.cs +++ b/tests/CodeIndex.Tests/McpServerTests.cs @@ -2882,6 +2882,55 @@ public void McpToolFilter_Parse_DenyListRemovesIndividualTools() Assert.False(filter.IsEnabled("backfill_fold")); } + [Fact] + public void McpToolFilter_Parse_OverlongAllowListFailsClosed_Issue2905() + { + lock (TestConsoleLock.Gate) + { + var originalError = Console.Error; + using var stderr = new StringWriter(); + try + { + Console.SetError(stderr); + var filter = McpToolFilter.Parse(new string('s', McpToolFilter.MaxToolFilterCsvLength + 1), null); + + foreach (var name in McpToolFilter.KnownToolNames) + Assert.False(filter.IsEnabled(name), $"{name} should be disabled when an invalid allowlist is supplied"); + Assert.Contains(McpToolFilter.AllowEnvVarName, stderr.ToString()); + Assert.Contains("was rejected", stderr.ToString()); + } + finally + { + Console.SetError(originalError); + } + } + } + + [Fact] + public void McpToolFilter_Parse_TooManyDenyEntriesAreRejected_Issue2905() + { + lock (TestConsoleLock.Gate) + { + var originalError = Console.Error; + using var stderr = new StringWriter(); + try + { + Console.SetError(stderr); + var tooMany = string.Join(',', Enumerable.Repeat("index", McpToolFilter.MaxToolFilterCsvEntries + 1)); + var filter = McpToolFilter.Parse(null, tooMany); + + Assert.True(filter.IsEnabled("index")); + Assert.Contains(McpToolFilter.DenyEnvVarName, stderr.ToString()); + Assert.Contains("accepts at most", stderr.ToString()); + Assert.Contains("was rejected", stderr.ToString()); + } + finally + { + Console.SetError(originalError); + } + } + } + [Fact] public void McpToolFilter_Parse_AllowWinsOverDeny() { diff --git a/tests/CodeIndex.Tests/QueryCommandRunnerTests.cs b/tests/CodeIndex.Tests/QueryCommandRunnerTests.cs index 88509cad4b..08c3b9cfc2 100644 --- a/tests/CodeIndex.Tests/QueryCommandRunnerTests.cs +++ b/tests/CodeIndex.Tests/QueryCommandRunnerTests.cs @@ -77,6 +77,40 @@ public void ParseArgs_ParsesFiltersFlagsAndAcceptsMaxSnippetLines() Assert.True(options.NoVisibilityRank); } + [Theory] + [InlineData("--visibility")] + [InlineData("--exclude-visibility")] + public void ParseArgs_VisibilityFiltersRejectOverlongCsv_Issue2912(string optionName) + { + var tooLong = new string('p', QueryCommandRunner.MaxVisibilityFilterCsvLength + 1); + + var options = QueryCommandRunner.ParseArgs( + ["RunSearch", optionName, tooLong], + jsonDefault: false, + allowNamedQuery: true); + + Assert.Contains($"{optionName} value is too long", options.ParseError); + Assert.Empty(options.VisibilityFilters); + Assert.Empty(options.ExcludeVisibilityFilters); + } + + [Theory] + [InlineData("--visibility")] + [InlineData("--exclude-visibility")] + public void ParseArgs_VisibilityFiltersRejectTooManyCsvEntries_Issue2912(string optionName) + { + var tooMany = string.Join(',', Enumerable.Repeat("public", QueryCommandRunner.MaxVisibilityFilterCsvEntries + 1)); + + var options = QueryCommandRunner.ParseArgs( + ["RunSearch", optionName, tooMany], + jsonDefault: false, + allowNamedQuery: true); + + Assert.Contains($"{optionName} accepts at most", options.ParseError); + Assert.Empty(options.VisibilityFilters); + Assert.Empty(options.ExcludeVisibilityFilters); + } + [Fact] public void ParseArgs_AllowsZeroMaxLineWidth() { @@ -364,6 +398,36 @@ public void ParseArgs_StatusStaleAfterStoresDuration() Assert.Equal(TimeSpan.FromHours(2), options.StaleAfter); } + [Fact] + public void ParseArgs_StatusCheckScopesRejectsOverlongCsv_Issue2913() + { + var tooLong = new string('w', QueryCommandRunner.MaxStatusCheckScopesCsvLength + 1); + + var options = QueryCommandRunner.ParseArgs( + [$"--check={tooLong}"], + jsonDefault: false, + allowStatusCheck: true); + + Assert.True(options.CheckWorkspace); + Assert.Contains("--check value is too long", options.ParseError); + Assert.Null(options.StatusCheckScopes); + } + + [Fact] + public void ParseArgs_StatusCheckScopesRejectsTooManyCsvEntries_Issue2913() + { + var tooMany = string.Join(',', Enumerable.Repeat("workspace", QueryCommandRunner.MaxStatusCheckScopesCsvEntries + 1)); + + var options = QueryCommandRunner.ParseArgs( + [$"--check={tooMany}"], + jsonDefault: false, + allowStatusCheck: true); + + Assert.True(options.CheckWorkspace); + Assert.Contains("--check accepts at most", options.ParseError); + Assert.Null(options.StatusCheckScopes); + } + [Fact] public void RunStatusConfig_PrintsEffectiveConfigWithoutOpeningDb() { @@ -3038,6 +3102,41 @@ public void RunSearch_InvalidPathGlobReturnsUsageErrorBeforeQuery_Issue2073(stri } } + [Theory] + [InlineData("--path")] + [InlineData("--exclude-path")] + public void ParseArgs_PathFiltersRejectTooManyValues_Issue2911(string optionName) + { + var args = new List { "RunSearch" }; + for (var i = 0; i <= QueryCommandRunner.MaxQueryPathFilterCount; i++) + { + args.Add(optionName); + args.Add("src/**"); + } + + var options = QueryCommandRunner.ParseArgs( + args.ToArray(), + jsonDefault: false, + allowNamedQuery: true); + + Assert.Contains($"{optionName} accepts at most", options.ParseError); + } + + [Theory] + [InlineData("--path")] + [InlineData("--exclude-path")] + public void ParseArgs_PathFiltersRejectOverlongPattern_Issue2911(string optionName) + { + var tooLong = new string('a', QueryCommandRunner.MaxQueryPathFilterLength + 1); + + var options = QueryCommandRunner.ParseArgs( + ["RunSearch", optionName, tooLong], + jsonDefault: false, + allowNamedQuery: true); + + Assert.Contains($"{optionName} value is too long", options.ParseError); + } + // Issue #1507: missing-value errors for CLI flags must append a per-flag `Hint:` line that // shows the expected value type or range (e.g. positive integer, glob pattern, language id), // so users do not need to consult `--help` for trivial mistakes. The hint is sourced from @@ -12706,6 +12805,36 @@ public void RunMap_ParseSectionsAndDepth_StoresSelectors() Assert.Null(options.ParseError); } + [Fact] + public void ParseArgs_MapSectionsRejectsOverlongCsv_Issue2914() + { + var tooLong = new string('t', QueryCommandRunner.MaxMapSectionsCsvLength + 1); + + var options = QueryCommandRunner.ParseArgs( + ["--sections", tooLong], + jsonDefault: false, + validateDefaultSnippetLines: false, + validateDefaultMaxLineWidth: false); + + Assert.Contains("--sections value is too long", options.ParseError); + Assert.Empty(options.MapSections!); + } + + [Fact] + public void ParseArgs_MapSectionsRejectsTooManyCsvEntries_Issue2914() + { + var tooMany = string.Join(',', Enumerable.Repeat("tree", QueryCommandRunner.MaxMapSectionsCsvEntries + 1)); + + var options = QueryCommandRunner.ParseArgs( + ["--sections", tooMany], + jsonDefault: false, + validateDefaultSnippetLines: false, + validateDefaultMaxLineWidth: false); + + Assert.Contains("--sections accepts at most", options.ParseError); + Assert.Empty(options.MapSections!); + } + [Fact] public void ParseArgs_GraphFormatOutsideDeps_ReturnsParseError() {