@@ -172,7 +172,7 @@ private static bool ReadLooseObjectHeader(Stream input, out long size)
172172 return true ;
173173 }
174174
175- private LooseBlobState GetLooseBlobStateAtPath ( string blobPath , Action < Stream , long > writeAction , out long size )
175+ private LooseBlobState GetLooseBlobStateAtPath ( string blobPath , Action < Stream , long > writeAction , out long size )
176176 {
177177 bool corruptLooseObject = false ;
178178 try
@@ -191,7 +191,21 @@ private LooseBlobState GetLooseBlobStateAtPath(string blobPath, Action<Stream, l
191191 return LooseBlobState . Corrupt ;
192192 }
193193
194- writeAction ? . Invoke ( deflate , size ) ;
194+ if ( writeAction != null )
195+ {
196+ CountingStream counting = new CountingStream ( deflate ) ;
197+ writeAction ( counting , size ) ;
198+
199+ // .NET 10 DeflateStream silently returns partial data on truncated
200+ // zlib input instead of throwing InvalidDataException. Detect this
201+ // by comparing the bytes actually read to the size in the header.
202+ if ( counting . BytesRead < size )
203+ {
204+ corruptLooseObject = true ;
205+ return LooseBlobState . Corrupt ;
206+ }
207+ }
208+
195209 return LooseBlobState . Exists ;
196210 }
197211 }
@@ -278,5 +292,56 @@ private LooseBlobState GetLooseBlobState(string blobSha, Action<Stream, long> wr
278292
279293 return state ;
280294 }
295+
296+ /// <summary>
297+ /// A read-only stream wrapper that counts the total bytes read.
298+ /// Used to detect truncated loose objects where DeflateStream returns
299+ /// fewer bytes than the header declares (see GetLooseBlobStateAtPath).
300+ /// </summary>
301+ private sealed class CountingStream : Stream
302+ {
303+ private readonly Stream inner ;
304+ private long bytesRead ;
305+
306+ public CountingStream ( Stream inner )
307+ {
308+ this . inner = inner ;
309+ }
310+
311+ public long BytesRead => this . bytesRead ;
312+
313+ public override bool CanRead => true ;
314+ public override bool CanSeek => false ;
315+ public override bool CanWrite => false ;
316+ public override long Length => this . inner . Length ;
317+ public override long Position
318+ {
319+ get => this . inner . Position ;
320+ set => throw new NotSupportedException ( ) ;
321+ }
322+
323+ public override int Read ( byte [ ] buffer , int offset , int count )
324+ {
325+ int read = this . inner . Read ( buffer , offset , count ) ;
326+ this . bytesRead += read ;
327+ return read ;
328+ }
329+
330+ public override int ReadByte ( )
331+ {
332+ int b = this . inner . ReadByte ( ) ;
333+ if ( b >= 0 )
334+ {
335+ this . bytesRead ++ ;
336+ }
337+
338+ return b ;
339+ }
340+
341+ public override void Flush ( ) => this . inner . Flush ( ) ;
342+ public override long Seek ( long offset , SeekOrigin origin ) => throw new NotSupportedException ( ) ;
343+ public override void SetLength ( long value ) => throw new NotSupportedException ( ) ;
344+ public override void Write ( byte [ ] buffer , int offset , int count ) => throw new NotSupportedException ( ) ;
345+ }
281346 }
282347}
0 commit comments