// Licensed to the .NET Foundation under one or more agreements.// The .NET Foundation licenses this file to you under the MIT license.usingSystem;usingSystem.IO;usingSystem.Threading;usingSystem.Threading.Tasks;internalclassReadReturnsOneByteAtATimeStream:Stream{privatereadonlyStream_underlyingStream;publicReadReturnsOneByteAtATimeStream():this(newMemoryStream()){}publicReadReturnsOneByteAtATimeStream(StreamunderlyingStream)=>_underlyingStream=underlyingStream;publicoverrideboolCanRead=>_underlyingStream.CanRead;publicoverrideboolCanSeek=>_underlyingStream.CanSeek;publicoverrideboolCanWrite=>_underlyingStream.CanWrite;publicoverridelongLength=>_underlyingStream.Length;publicoverridelongPosition{get=>_underlyingStream.Position;set=>_underlyingStream.Position=value;}publicoverridevoidCopyTo(Streamdestination,intbufferSize)=>_underlyingStream.CopyTo(destination,bufferSize);publicoverrideTaskCopyToAsync(Streamdestination,intbufferSize,CancellationTokencancellationToken)=>_underlyingStream.CopyToAsync(destination,bufferSize,cancellationToken);publicnewvoidDispose()=>_underlyingStream.Dispose();publicoverrideValueTaskDisposeAsync()=>_underlyingStream.DisposeAsync();publicoverridevoidFlush()=>_underlyingStream.Flush();publicoverrideTaskFlushAsync(CancellationTokencancellationToken)=>_underlyingStream.FlushAsync(cancellationToken);/// <summary>/// Reads one byte at a time./// </summary>publicoverrideintRead(byte[]buffer,intoffset,intcount)=>_underlyingStream.ReadAtLeast(buffer.AsSpan(0,1),1,throwOnEndOfStream:false);/// <summary>/// Reads one byte at a time./// </summary>publicoverrideintRead(Span<byte>buffer)=>_underlyingStream.ReadAtLeast(buffer.Slice(0,1),1,throwOnEndOfStream:false);/// <summary>/// Reads one byte at a time./// </summary>publicoverrideasyncTask<int>ReadAsync(byte[]buffer,intoffset,intcount,CancellationTokencancellationToken)=>await_underlyingStream.ReadAtLeastAsync(buffer.AsMemory(0,1),1,throwOnEndOfStream:false,cancellationToken);/// <summary>/// Reads one byte at a time./// </summary>publicoverrideasyncValueTask<int>ReadAsync(Memory<byte>buffer,CancellationTokencancellationToken=default)=>await_underlyingStream.ReadAtLeastAsync(buffer.Slice(0,1),1,throwOnEndOfStream:false,cancellationToken);publicnewintReadAtLeast(Span<byte>buffer,intminimumBytes,boolthrowOnEndOfStream=true)=>_underlyingStream.ReadAtLeast(buffer,minimumBytes,throwOnEndOfStream);publicnewValueTask<int>ReadAtLeastAsync(Memory<byte>buffer,intminimumBytes,boolthrowOnEndOfStream=true,CancellationTokencancellationToken=default)=>_underlyingStream.ReadAtLeastAsync(buffer,minimumBytes,throwOnEndOfStream,cancellationToken);publicoverrideintReadByte()=>_underlyingStream.ReadByte();publicnewvoidReadExactly(Span<byte>buffer)=>_underlyingStream.ReadExactly(buffer);publicnewvoidReadExactly(byte[]buffer,intoffset,intcount)=>_underlyingStream.ReadExactly(buffer,offset,count);publicnewValueTaskReadExactlyAsync(byte[]buffer,intoffset,intcount,CancellationTokencancellationToken=default)=>_underlyingStream.ReadExactlyAsync(buffer,offset,count,cancellationToken);publicnewValueTaskReadExactlyAsync(Memory<byte>buffer,CancellationTokencancellationToken=default)=>_underlyingStream.ReadExactlyAsync(buffer,cancellationToken);publicoverridelongSeek(longoffset,SeekOriginorigin)=>_underlyingStream.Seek(offset,origin);publicoverridevoidSetLength(longvalue)=>_underlyingStream.SetLength(value);publicoverridevoidWrite(byte[]buffer,intoffset,intcount)=>_underlyingStream.Write(buffer,offset,count);publicoverridevoidWrite(ReadOnlySpan<byte>buffer)=>_underlyingStream.Write(buffer);publicoverrideTaskWriteAsync(byte[]buffer,intoffset,intcount,CancellationTokencancellationToken)=>_underlyingStream.WriteAsync(buffer,offset,count,cancellationToken);publicoverrideValueTaskWriteAsync(ReadOnlyMemory<byte>buffer,CancellationTokencancellationToken=default)=>_underlyingStream.WriteAsync(buffer,cancellationToken);publicoverridevoidWriteByte(bytevalue)=>_underlyingStream.WriteByte(value);}
There are various places in System.IO.Compression.ZipArchive* that are currently calling
stream.Readand assuming the number of bytes returned will be the requested one. This method does not work like that, it can return less than the requested number of bytes.This is a problem when using a stream whose Read method constantly returns less than the requested number of bytes. I tested the extreme scenario of creating my own custom Stream implementation, overriding the Read methods and forcing them to always return one byte at a time. This causes valid archives to throw unexpected exceptions when calling archive.Entries, as EnsureCentralDirectoryRead will be unable to finish reading all the entry bytes because many places assume that the number of bytes returned is insufficient and exit early.
The fix is to replace these Read calls with ReadExactly or ReadAtLeast, depending on each case (the former always throws if not enough bytes, the latter can optionally not throw on end of stream).
@edwardneal would you be interested in looking into this?
Here's my custom stream implementation:
Expand
And here is a test with a valid zip that throws:
Expand