fix: bound the numstat split so a tab in a path does not throw - #105
Conversation
The numstat record carries its path inside the same NUL-delimited token as the counts, so splitting on every tab turns a legally tab-named path into a fourth field and throws GitParseException — taking out the entire listing rather than one entry. Bound the split at three fields and relax the length check to `< 3`, matching what GitLogParser and GitTagParser already do. Both existing shapes still read the same: a rename's "0\t0\t" yields an empty third field and consumes its two following path tokens, and a malformed record with too few fields still throws. Fixes #102 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Vh7BAYZXphEn2dcrQddn1X
The two new parser tests read a tab-named path end to end, which Windows CI refused: RelativeFilePath rejects a control character there, and the raw section converts its path before any numstat record is read, so the entry is turned away before the bounded split is reached. That refusal is deliberate — GitParseValues documents it as reporting the path rather than dropping the entry — so branch on OperatingSystem.IsWindows() and assert what each platform can observe: the full read on POSIX, and on Windows that the failure is the representability one rather than a malformed numstat record. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Vh7BAYZXphEn2dcrQddn1X
|
| Check | Result |
|---|---|
| Test on ubuntu-latest | ✅ |
| Test on windows-latest | ✅ |
| Test on macos-latest | ✅ |
| CodeQL / Analyze (csharp, actions) | ✅ |
| github-advanced-security | ✅ |
There is no fix to port: the scanner is behaving correctly and the remote is down. Nothing in this PR can make that request succeed, and working around it would mean disabling the analysis step, which is not this PR's to change. The job should pass on the next run once SonarCloud is reachable again.
I'll keep watching the PR and re-check until it's green.
Generated by Claude Code
Fixes #102
The bug
ReadNumstatSectionsplit each record on every tab:Unlike the raw section, where a path is its own NUL-delimited token, the numstat record carries its path inside the same token as the counts —
1\t0\ttab\tname.txt. A tab is a legal byte in a git path, so such a file yields four fields and throws, and because the throw takes out the whole listing rather than one entry,Diff().WithLineCounts()becomes unusable on that repository while plainDiff()reads it fine.The fix
Bound the split at three fields and relax the length check to
< 3, matching the patternGitLogParserandGitTagParseralready use for exactly this reason:Both existing shapes read unchanged: the rename form
"1\t0\t"still yields an empty third field and still consumes its two following path tokens, and a record with too few fields (e.g. the space-delimited"0 1 d.txt"covered byThrowsForAMalformedNumstatRecord) still throws.Tests
Two tests added to
GitDiffParserTests:ReadsANumstatPathContainingATabWithoutMistakingItForAnExtraField— the shape from the issue, captured from git 2.43 for a file namedtab<TAB>name.txt. Verified to fail without the change (GitParseException: Malformed numstat diff record: '1\t0\ttab\tname.txt') and pass with it.ReadsARenameWhoseNumstatPathsContainTabs— guards the rename form against the bound, since its empty path field and two trailing tokens are the shape most at risk from a change here.Full suite: 578 passed, 0 failed.
dotnet build -c Release: 0 warnings, 0 errors.🤖 Generated with Claude Code
https://claude.ai/code/session_01Vh7BAYZXphEn2dcrQddn1X
Generated by Claude Code