From a57ed0cce0f9fd2f855eca6842ea0ba1539486eb Mon Sep 17 00:00:00 2001 From: Widthdom Date: Sun, 31 May 2026 12:59:42 +0900 Subject: [PATCH 1/3] Fix POSIX ignore backslash normalization (#1715) --- changelog.d/unreleased/1715.fixed.md | 17 +++++++++++++++++ src/CodeIndex/Indexer/Scanning/FileIndexer.cs | 4 ++-- tests/CodeIndex.Tests/FileIndexerTests.cs | 11 +++++++++++ 3 files changed, 30 insertions(+), 2 deletions(-) create mode 100644 changelog.d/unreleased/1715.fixed.md diff --git a/changelog.d/unreleased/1715.fixed.md b/changelog.d/unreleased/1715.fixed.md new file mode 100644 index 0000000000..4e16f5dcb9 --- /dev/null +++ b/changelog.d/unreleased/1715.fixed.md @@ -0,0 +1,17 @@ +--- +category: fixed +issues: + - 1715 +affected: + - src/CodeIndex/Indexer/Scanning/FileIndexer.cs + - src/CodeIndex/Cli/IndexCommandRunner.cs + - src/CodeIndex/Cli/ProgramRunner.cs +--- + +## English + +- **Ignore path normalization now respects POSIX backslashes (#1715)** — `.cdidxignore` and `.gitignore` comparisons no longer rewrite literal backslashes in POSIX filenames while Windows paths still normalize separators. + +## 日本語 + +- **ignore path 正規化が POSIX の backslash を尊重するようになりました (#1715)** — `.cdidxignore` / `.gitignore` の比較で POSIX ファイル名内のリテラル backslash を書き換えず、Windows path では引き続き区切り文字を正規化します。 diff --git a/src/CodeIndex/Indexer/Scanning/FileIndexer.cs b/src/CodeIndex/Indexer/Scanning/FileIndexer.cs index a3b737ee30..d37df9394d 100644 --- a/src/CodeIndex/Indexer/Scanning/FileIndexer.cs +++ b/src/CodeIndex/Indexer/Scanning/FileIndexer.cs @@ -2523,8 +2523,8 @@ private static IEnumerable ParseSubmodulePathsFromGitmodules(IEnumerable } } - private static string NormalizeIgnorePath(string path) - => path.Replace('\\', '/').TrimEnd('/'); + internal static string NormalizeIgnorePath(string path) + => NormalizePathSeparators(path).TrimEnd('/'); /// /// Normalize OS path separators to '/' for DB storage and lookup. diff --git a/tests/CodeIndex.Tests/FileIndexerTests.cs b/tests/CodeIndex.Tests/FileIndexerTests.cs index 867fe0b024..5d2da546a7 100644 --- a/tests/CodeIndex.Tests/FileIndexerTests.cs +++ b/tests/CodeIndex.Tests/FileIndexerTests.cs @@ -17,6 +17,17 @@ namespace CodeIndex.Tests; /// public class FileIndexerTests { + [Fact] + public void NormalizeIgnorePath_PosixPreservesLiteralBackslash() + { + var normalized = FileIndexer.NormalizeIgnorePath(@"weird\name.py/"); + + if (OperatingSystem.IsWindows()) + Assert.Equal("weird/name.py", normalized); + else + Assert.Equal(@"weird\name.py", normalized); + } + [Fact] public void ScanFilesDetailed_CancelledToken_ThrowsBeforeEnumeration() { From 88652f15e2447ceb6057a2f8f625a6f8f292a58c Mon Sep 17 00:00:00 2001 From: Widthdom Date: Sun, 31 May 2026 13:33:24 +0900 Subject: [PATCH 2/3] Reject backslash project-root traversal (#1717) --- changelog.d/unreleased/1717.fixed.md | 15 ++++++++ src/CodeIndex/Cli/IndexCommandRunner.cs | 12 +++++- .../IndexCommandRunnerTests.cs | 38 +++++++++++++++++++ 3 files changed, 63 insertions(+), 2 deletions(-) create mode 100644 changelog.d/unreleased/1717.fixed.md diff --git a/changelog.d/unreleased/1717.fixed.md b/changelog.d/unreleased/1717.fixed.md new file mode 100644 index 0000000000..161b654911 --- /dev/null +++ b/changelog.d/unreleased/1717.fixed.md @@ -0,0 +1,15 @@ +--- +category: fixed +issues: + - 1717 +affected: + - src/CodeIndex/Cli/IndexCommandRunner.cs +--- + +## English + +- **Project-root escape checks now reject backslash traversal (#1717)** — commit and file-scoped indexing paths treat `..\` traversal the same as `../` before accepting a relative target. + +## 日本語 + +- **project root 外への escape 判定が backslash traversal を拒否するようになりました (#1717)** — commit / file scoped indexing path は relative target を受け入れる前に `..\` traversal を `../` と同じように扱います。 diff --git a/src/CodeIndex/Cli/IndexCommandRunner.cs b/src/CodeIndex/Cli/IndexCommandRunner.cs index 395373bbd1..4436267598 100644 --- a/src/CodeIndex/Cli/IndexCommandRunner.cs +++ b/src/CodeIndex/Cli/IndexCommandRunner.cs @@ -365,8 +365,16 @@ private static bool AllowReuseWithCurrentHotspotFamilyTrust( && matchesCurrent; } - private static bool IsOutsideProjectRoot(string relativePath) => - relativePath == ".." || relativePath.StartsWith("../", StringComparison.Ordinal); + internal static bool IsOutsideProjectRoot(string relativePath) + { + if (Path.IsPathRooted(relativePath)) + return true; + + var normalized = OperatingSystem.IsWindows() + ? relativePath.Replace('\\', '/') + : relativePath; + return normalized == ".." || normalized.StartsWith("../", StringComparison.Ordinal); + } private static bool ContainsIgnoreFilePath(IEnumerable paths) => paths.Any(FileIndexer.IsIgnoreFilePath); diff --git a/tests/CodeIndex.Tests/IndexCommandRunnerTests.cs b/tests/CodeIndex.Tests/IndexCommandRunnerTests.cs index 087cf4df48..8cb45394cc 100644 --- a/tests/CodeIndex.Tests/IndexCommandRunnerTests.cs +++ b/tests/CodeIndex.Tests/IndexCommandRunnerTests.cs @@ -41,6 +41,44 @@ public class IndexCommandRunnerTests PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower, }; + [Theory] + [InlineData("..")] + [InlineData("../evil.txt")] + [InlineData(@"..\evil.txt")] + [InlineData(@"..\..\evil.txt")] + public void IsOutsideProjectRoot_ParentTraversalSeparators_ReturnsTrue(string relativePath) + { + if (relativePath.Contains('\\') && !OperatingSystem.IsWindows()) + return; + + Assert.True(IndexCommandRunner.IsOutsideProjectRoot(relativePath)); + } + + [Fact] + public void IsOutsideProjectRoot_PosixLiteralBackslashPath_ReturnsFalse() + { + if (OperatingSystem.IsWindows()) + return; + + Assert.False(IndexCommandRunner.IsOutsideProjectRoot(@"..\evil.txt")); + } + + [Fact] + public void IsOutsideProjectRoot_RootedPath_ReturnsTrue() + { + var rootedPath = OperatingSystem.IsWindows() + ? @"C:\Windows\evil.txt" + : "/etc/passwd"; + + Assert.True(IndexCommandRunner.IsOutsideProjectRoot(rootedPath)); + } + + [Fact] + public void IsOutsideProjectRoot_NormalRelativePath_ReturnsFalse() + { + Assert.False(IndexCommandRunner.IsOutsideProjectRoot("src/app.cs")); + } + [Fact] public void ParseArgs_HelpFlagSetsShowHelp() { From 2aa93b66fdc26ce2b2e639691343779419ba3bc8 Mon Sep 17 00:00:00 2001 From: Widthdom Date: Sun, 31 May 2026 13:33:55 +0900 Subject: [PATCH 3/3] Respect platform project path syntax (#1725) --- changelog.d/unreleased/1725.fixed.md | 15 +++++++++++ src/CodeIndex/Cli/ProgramRunner.cs | 26 ++++++++++++++++-- tests/CodeIndex.Tests/ProgramRunnerTests.cs | 30 +++++++++++++++++++++ 3 files changed, 69 insertions(+), 2 deletions(-) create mode 100644 changelog.d/unreleased/1725.fixed.md diff --git a/changelog.d/unreleased/1725.fixed.md b/changelog.d/unreleased/1725.fixed.md new file mode 100644 index 0000000000..f42750db59 --- /dev/null +++ b/changelog.d/unreleased/1725.fixed.md @@ -0,0 +1,15 @@ +--- +category: fixed +issues: + - 1725 +affected: + - src/CodeIndex/Cli/ProgramRunner.cs +--- + +## English + +- **CLI project-path detection now follows platform path syntax (#1725)** — Windows drive and UNC forms are recognized on Windows, while POSIX filenames containing literal backslashes are no longer treated as path syntax. + +## 日本語 + +- **CLI の project path 判定がプラットフォーム別の path 構文に沿うようになりました (#1725)** — Windows では drive / UNC 形式を認識し、POSIX ではリテラル backslash を含むファイル名を path 構文として扱わないようになりました。 diff --git a/src/CodeIndex/Cli/ProgramRunner.cs b/src/CodeIndex/Cli/ProgramRunner.cs index 5523efde4f..12f8f4c270 100644 --- a/src/CodeIndex/Cli/ProgramRunner.cs +++ b/src/CodeIndex/Cli/ProgramRunner.cs @@ -298,8 +298,30 @@ _ when IsProjectPathArg(commandName) } } - internal static bool IsProjectPathArg(string arg) => - !arg.StartsWith('-') && (Directory.Exists(arg) || arg.Contains('/') || arg.Contains('\\') || arg == "."); + internal static bool IsProjectPathArg(string arg) + { + if (arg.StartsWith('-')) + return false; + + if (arg == "." || Directory.Exists(arg) || Path.IsPathRooted(arg) || Path.IsPathFullyQualified(arg)) + return true; + + if (arg.Contains(Path.DirectorySeparatorChar)) + return true; + + if (Path.AltDirectorySeparatorChar != '\0' + && Path.AltDirectorySeparatorChar != Path.DirectorySeparatorChar + && arg.Contains(Path.AltDirectorySeparatorChar)) + return true; + + return OperatingSystem.IsWindows() + && (IsWindowsDrivePath(arg) || arg.StartsWith(@"\\", StringComparison.Ordinal)); + } + + private static bool IsWindowsDrivePath(string arg) => + arg.Length >= 2 + && arg[1] == ':' + && ((arg[0] >= 'A' && arg[0] <= 'Z') || (arg[0] >= 'a' && arg[0] <= 'z')); internal static void EnsureRedirectedStdoutUsesUtf8() { diff --git a/tests/CodeIndex.Tests/ProgramRunnerTests.cs b/tests/CodeIndex.Tests/ProgramRunnerTests.cs index ce87b1e7f2..7e4b4fb7e7 100644 --- a/tests/CodeIndex.Tests/ProgramRunnerTests.cs +++ b/tests/CodeIndex.Tests/ProgramRunnerTests.cs @@ -11,6 +11,36 @@ namespace CodeIndex.Tests; [Collection("SQLite pool sensitive")] public class ProgramRunnerTests { + [Theory] + [InlineData("foo.cs", false)] + [InlineData("./foo", true)] + [InlineData(".", true)] + public void IsProjectPathArg_CommonForms_ReturnsExpectedValue(string arg, bool expected) + { + Assert.Equal(expected, ProgramRunner.IsProjectPathArg(arg)); + } + + [Fact] + public void IsProjectPathArg_PosixLiteralBackslashFileName_IsNotPathSyntax() + { + if (OperatingSystem.IsWindows()) + return; + + Assert.False(ProgramRunner.IsProjectPathArg(@"weird\name.txt")); + } + + [Theory] + [InlineData(@"C:\foo")] + [InlineData("C:")] + [InlineData(@"\\server\share\foo")] + public void IsProjectPathArg_WindowsPathForms_ReturnTrueOnWindows(string arg) + { + if (!OperatingSystem.IsWindows()) + return; + + Assert.True(ProgramRunner.IsProjectPathArg(arg)); + } + [Theory] [InlineData("--json")] [InlineData("--json=array")]