From 5829a04918ec30e8da05ca3645e2bafe8327304b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Amaury=20Lev=C3=A9?= Date: Thu, 6 Aug 2026 15:55:15 +0200 Subject: [PATCH 1/2] Fix git ls-files partial read parsing Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .../SourceControl/GitFileHashProviderTest.cs | Bin 13599 -> 16289 bytes .../SourceControl/GitFileHashProvider.cs | 84 +++++++----------- 2 files changed, 34 insertions(+), 50 deletions(-) diff --git a/src/Common.Tests/SourceControl/GitFileHashProviderTest.cs b/src/Common.Tests/SourceControl/GitFileHashProviderTest.cs index 30798bce79a2dd6ce036b42c2fa948a22f768a08..f69ddbea6d031d30961376255a3de323e5fa1769 100644 GIT binary patch delta 1785 zcmbVM-A)rh6utptNI(S*#2}eAKzGwpc4>iDXbe;uF%SvXctMF7wo^J%b~m%LP|}b- zgP8{~zJZB1-Vk{KFMI$meE{FUIlJ5L($#2enshVs?K$7iIeq{3L-*tJgn04j&b&i+FXWfCyhiY<5d(;y!!z9)=L{RN_+sq7PT69E#*SMgYdEpfTAYAu{-w06 zG|byPsfu^wpT~X08C1b1udyK-Jd9YI%8Usj{qBRtigcv63t2 zRyHNj zbDtO%tM6cTOjxyvO-N+ZZ8T&ms!y~dx(k!Hn^J?!FDZTuE8XN;sKMb3QnoSo?psi4 zF^SS%)c&QC!l?`@=*1;fo+q>5QoMri7)*uiGYdlinRlS^CDLlV11&CPGHczi`+tsD z*emd1HC3ynR8^q4+@~jr?-PMJHlt^l6XX!wB6Vc6u;aux z4?RZV8U}HP=JskZITM!Ixanj}8?=&v@q)xciTWC09sX4Lv+KZ^{)zieKDQht_*mWOs;pELUKDJ~-)VL#0v= za}as+FnA%LFRq6sOQ0bFNv9*`!5w`3`>;6D0+6`QKga-rHf~Z3`^Q^Ozzd9Umzg&T zb>TeNY|laIq~F=OA!2|aI10m32Fv6tyD+S~)KokVGkzF7rxpgZsF7k`f3I=2Y}pr8 sD=@vMI}-`Z3##8WS_MQ5_?0e&1S9> ParseGitLsFiles( // file paths are relative and have / instead of \ but otherwise unmodified (-z is important here) // and delimited by a tab from other staging info (if there is any) // staging info which includes the hash are space delimited. See UT's for more examples - using var reader = new GitLsFileOutputReader(gitOutput); + var reader = new GitLsFileOutputReader(gitOutput); var fileHashes = new Dictionary(StringComparer.OrdinalIgnoreCase); var filesToRehash = new List(); StringBuilder? line; @@ -315,72 +314,57 @@ internal List ParseGitSubmoduleStatus(string output) return submodules; } - private sealed class GitLsFileOutputReader : IDisposable + private sealed class GitLsFileOutputReader { - readonly BlockingCollection _lines = new BlockingCollection(); + private const int BufferSize = 4096; + + private readonly TextReader _reader; + private readonly char[] _buffer = new char[BufferSize]; + private int _bufferPosition; + private int _bufferLength; + private bool _endOfStream; public GitLsFileOutputReader(TextReader reader) { - Task.Run(() => PopulateAsync(reader)); + _reader = reader; } - private void PopulateAsync(TextReader reader) + public StringBuilder? ReadLine() { - int overflowLength = 0; - var buffer = new char[4096]; // must be large enough to hold at least one line of output + if (_endOfStream) + { + return null; + } + + StringBuilder? line = null; while (true) { - int readCnt = reader.Read(buffer, overflowLength, buffer.Length - overflowLength); - if (readCnt == 0) // end of stream + if (_bufferPosition == _bufferLength) { - if (overflowLength > 0) + _bufferLength = _reader.Read(_buffer, 0, _buffer.Length); + _bufferPosition = 0; + if (_bufferLength == 0) { - _lines.Add(new StringBuilder(overflowLength).Append(buffer, 0, overflowLength)); + _endOfStream = true; + return line; } - _lines.CompleteAdding(); - return; } - readCnt += overflowLength; - int startIdx = 0, eolIdx; - while (startIdx < readCnt && (eolIdx = Array.IndexOf(buffer, '\0', startIdx)) != -1) + int eolIdx = Array.IndexOf(_buffer, '\0', _bufferPosition, _bufferLength - _bufferPosition); + if (eolIdx >= 0) { - int lineLength = eolIdx - startIdx; - if (overflowLength > 0) - { - overflowLength = 0; - startIdx = 0; - } - _lines.Add(new StringBuilder(lineLength).Append(buffer, startIdx, lineLength)); - startIdx = eolIdx + 1; - } - if (startIdx < readCnt) - { - if (overflowLength > 0) // we already have some overflow left, but the line could not fit the buffer - { - throw new InvalidDataException($"Internal: git ls-files output line length {readCnt - startIdx} exceeds {nameof(buffer)} size {buffer.Length}. Increase the latter."); - } - overflowLength = readCnt - startIdx; - Array.Copy(buffer, startIdx, buffer, 0, overflowLength); + int segmentLength = eolIdx - _bufferPosition; + line ??= new StringBuilder(segmentLength); + line.Append(_buffer, _bufferPosition, segmentLength); + _bufferPosition = eolIdx + 1; + return line; } - } - } - public StringBuilder? ReadLine() - { - while (!_lines.IsCompleted) - { - if (_lines.TryTake(out StringBuilder? result, -1)) - { - return result; - } + int remainingLength = _bufferLength - _bufferPosition; + line ??= new StringBuilder(remainingLength); + line.Append(_buffer, _bufferPosition, remainingLength); + _bufferPosition = _bufferLength; } - return null; - } - - public void Dispose() - { - _lines.Dispose(); } } } From 639e2562bf463762d4813663fd2fb77879fe12f2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Amaury=20Lev=C3=A9?= Date: Thu, 6 Aug 2026 18:32:54 +0200 Subject: [PATCH 2/2] Narrow git output parsing fix Preserve the existing producer-consumer reader while bounding NUL scans to valid data and fixing partial-record carry-over. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .../SourceControl/GitFileHashProviderTest.cs | Bin 16289 -> 16292 bytes .../SourceControl/GitFileHashProvider.cs | 76 ++++++++++-------- 2 files changed, 42 insertions(+), 34 deletions(-) diff --git a/src/Common.Tests/SourceControl/GitFileHashProviderTest.cs b/src/Common.Tests/SourceControl/GitFileHashProviderTest.cs index f69ddbea6d031d30961376255a3de323e5fa1769..08291475c9d8ea68ec2dba4e5164b27172f0286f 100644 GIT binary patch delta 186 zcmZ2jzodS{Ar&FVlAzSY6sOX(wA3P}{L;LX#L4rmL?>tI^G;H=0a5~7>6v-y0Y#~4 znH7`!RmCP3sR~baR#mb!F|agM&{ZfdNkk~n^GVH1FUbH3E9E2>rKjpEA&KafRFuH@ ylQUIiWQ$Uh^NUgxY*Ecn&{il&EXkO>PSuGGqGGb6k}#v@x$vXlbOWpsk>!S5h%KQB_1UzqF*Fv?L_8qC~+~K}E^Xz`)GJM4>t(u{fhv!62rj zx*)M6qc+AsX)>>xQ$0epUT|qraY<2TUb=>Xj)IASrJ1G VazTuqJWFW(WLIT_&4KE{5&$o-PL}`x diff --git a/src/Common/SourceControl/GitFileHashProvider.cs b/src/Common/SourceControl/GitFileHashProvider.cs index 790cbf6..9172a26 100644 --- a/src/Common/SourceControl/GitFileHashProvider.cs +++ b/src/Common/SourceControl/GitFileHashProvider.cs @@ -2,6 +2,7 @@ // Licensed under the MIT license. See LICENSE file in the project root for full license information. using System; +using System.Collections.Concurrent; using System.Collections.Generic; using System.Diagnostics; using System.IO; @@ -124,7 +125,7 @@ internal async Task> ParseGitLsFiles( // file paths are relative and have / instead of \ but otherwise unmodified (-z is important here) // and delimited by a tab from other staging info (if there is any) // staging info which includes the hash are space delimited. See UT's for more examples - var reader = new GitLsFileOutputReader(gitOutput); + using var reader = new GitLsFileOutputReader(gitOutput); var fileHashes = new Dictionary(StringComparer.OrdinalIgnoreCase); var filesToRehash = new List(); StringBuilder? line; @@ -314,57 +315,64 @@ internal List ParseGitSubmoduleStatus(string output) return submodules; } - private sealed class GitLsFileOutputReader + private sealed class GitLsFileOutputReader : IDisposable { - private const int BufferSize = 4096; - - private readonly TextReader _reader; - private readonly char[] _buffer = new char[BufferSize]; - private int _bufferPosition; - private int _bufferLength; - private bool _endOfStream; + readonly BlockingCollection _lines = new BlockingCollection(); public GitLsFileOutputReader(TextReader reader) { - _reader = reader; + Task.Run(() => PopulateAsync(reader)); } - public StringBuilder? ReadLine() + private void PopulateAsync(TextReader reader) { - if (_endOfStream) - { - return null; - } - - StringBuilder? line = null; + int overflowLength = 0; + var buffer = new char[4096]; // must be large enough to hold at least one line of output while (true) { - if (_bufferPosition == _bufferLength) + int readCnt = reader.Read(buffer, overflowLength, buffer.Length - overflowLength); + if (readCnt == 0) // end of stream { - _bufferLength = _reader.Read(_buffer, 0, _buffer.Length); - _bufferPosition = 0; - if (_bufferLength == 0) + if (overflowLength > 0) { - _endOfStream = true; - return line; + _lines.Add(new StringBuilder(overflowLength).Append(buffer, 0, overflowLength)); } + _lines.CompleteAdding(); + return; } - int eolIdx = Array.IndexOf(_buffer, '\0', _bufferPosition, _bufferLength - _bufferPosition); - if (eolIdx >= 0) + readCnt += overflowLength; + int startIdx = 0, eolIdx; + while (startIdx < readCnt && (eolIdx = Array.IndexOf(buffer, '\0', startIdx, readCnt - startIdx)) != -1) { - int segmentLength = eolIdx - _bufferPosition; - line ??= new StringBuilder(segmentLength); - line.Append(_buffer, _bufferPosition, segmentLength); - _bufferPosition = eolIdx + 1; - return line; + int lineLength = eolIdx - startIdx; + _lines.Add(new StringBuilder(lineLength).Append(buffer, startIdx, lineLength)); + startIdx = eolIdx + 1; } + overflowLength = readCnt - startIdx; + if (overflowLength == buffer.Length) + { + throw new InvalidDataException($"Internal: git ls-files output line length {overflowLength} exceeds {nameof(buffer)} size {buffer.Length}. Increase the latter."); + } + Array.Copy(buffer, startIdx, buffer, 0, overflowLength); + } + } - int remainingLength = _bufferLength - _bufferPosition; - line ??= new StringBuilder(remainingLength); - line.Append(_buffer, _bufferPosition, remainingLength); - _bufferPosition = _bufferLength; + public StringBuilder? ReadLine() + { + while (!_lines.IsCompleted) + { + if (_lines.TryTake(out StringBuilder? result, -1)) + { + return result; + } } + return null; + } + + public void Dispose() + { + _lines.Dispose(); } } }