Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -122,12 +122,26 @@ HRESULT ISequentialStream.Interface.Read(void* pv, uint cb, uint* pcbRead)
ActualizeVirtualPosition();

Span<byte> buffer = new(pv, checked((int)cb));
int read = _dataStream.Read(buffer);

Comment thread
ricardobossan marked this conversation as resolved.
// Stream.Read can legally return fewer bytes than requested (e.g. chunked/network streams).
// Some callers (GDI+) treat a short read as failure, so fill the buffer until EOF.
int read = 0;
while (read < buffer.Length)
Comment thread
ricardobossan marked this conversation as resolved.
{
int bytesRead = _dataStream.Read(buffer[read..]);
if (bytesRead == 0)
{
break;
}

read += bytesRead;
}

if (pcbRead is not null)
*pcbRead = (uint)read;
Comment thread
ricardobossan marked this conversation as resolved.

return HRESULT.S_OK;
// Per IStream::Read, a short read caused by reaching EOF is signaled with S_FALSE.
return read < buffer.Length ? HRESULT.S_FALSE : HRESULT.S_OK;
}

HRESULT IStream.Interface.Read(void* pv, uint cb, uint* pcbRead)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using Windows.Win32.Foundation;

namespace Windows.Win32.System.Com.Tests;

public class ComManagedStreamTests
Expand All @@ -22,6 +24,56 @@ public void Ctor_SeekableStream_UsesOriginalStream()
comManagedStream.GetDataStream().Should().BeSameAs(seekableStream);
}

[Fact]
public unsafe void Read_StreamReturnsShortReads_FillsBuffer()
{
// Streams are allowed to return fewer bytes than requested. Simulate a chunked/network stream
// and verify the wrapper keeps reading until the requested buffer is filled. See issue #14064.
byte[] sourceBytes = new byte[1024];
for (int index = 0; index < sourceBytes.Length; index++)
{
sourceBytes[index] = (byte)index;
}

using ChunkingStream shortReadStream = new(sourceBytes, chunkSize: 100);
ComManagedStream comManagedStream = new(shortReadStream);

byte[] destinationBuffer = new byte[sourceBytes.Length];
uint bytesReadCount;
fixed (byte* destinationPointer = destinationBuffer)
{
((IStream.Interface)comManagedStream).Read(destinationPointer, (uint)destinationBuffer.Length, &bytesReadCount).Should().Be(HRESULT.S_OK);
}

bytesReadCount.Should().Be((uint)sourceBytes.Length);
destinationBuffer.Should().Equal(sourceBytes);
}

[Fact]
public unsafe void Read_RequestPastEndOfStream_ReturnsOnlyAvailableBytes()
{
// When more bytes are requested than remain, only the available bytes are returned (no throw on EOF),
// and a short read caused by EOF is reported with S_FALSE. See issue #14064.
byte[] sourceBytes = new byte[50];
for (int index = 0; index < sourceBytes.Length; index++)
{
sourceBytes[index] = (byte)index;
}

using ChunkingStream shortReadStream = new(sourceBytes, chunkSize: 10);
ComManagedStream comManagedStream = new(shortReadStream);

byte[] destinationBuffer = new byte[sourceBytes.Length * 2];
uint bytesReadCount;
fixed (byte* destinationPointer = destinationBuffer)
{
((IStream.Interface)comManagedStream).Read(destinationPointer, (uint)destinationBuffer.Length, &bytesReadCount).Should().Be(HRESULT.S_FALSE);
}

bytesReadCount.Should().Be((uint)sourceBytes.Length);
Comment thread
ricardobossan marked this conversation as resolved.
destinationBuffer.Take(sourceBytes.Length).Should().Equal(sourceBytes);
}

private class TestStream : MemoryStream
{
private readonly bool _canSeek;
Expand All @@ -33,4 +85,60 @@ public TestStream(bool canSeek, int numBytes) : base(new byte[numBytes])
_canSeek = canSeek;
}
}

// Seekable stream that never returns more than a fixed chunk per Read, simulating chunked/network streams.
// Derives from Stream (not MemoryStream) so the span-based Read routes through this Read override on all targets.
private sealed class ChunkingStream : Stream
{
private readonly byte[] _sourceData;
private readonly int _maxBytesPerRead;
private int _position;

public ChunkingStream(byte[] sourceData, int chunkSize)
{
_sourceData = sourceData;
_maxBytesPerRead = chunkSize;
}

public override bool CanRead => true;
public override bool CanSeek => true;
public override bool CanWrite => false;
public override long Length => _sourceData.Length;

public override long Position
{
get => _position;
set => _position = (int)value;
}

public override int Read(byte[] buffer, int offset, int count)
{
int bytesToRead = Math.Min(Math.Min(count, _maxBytesPerRead), _sourceData.Length - _position);
if (bytesToRead <= 0)
{
return 0;
}

Array.Copy(_sourceData, _position, buffer, offset, bytesToRead);
_position += bytesToRead;
return bytesToRead;
}

public override long Seek(long offset, SeekOrigin origin)
{
_position = origin switch
{
SeekOrigin.Begin => (int)offset,
SeekOrigin.Current => _position + (int)offset,
SeekOrigin.End => _sourceData.Length + (int)offset,
_ => _position,
};

return _position;
}

public override void Flush() { }
public override void SetLength(long value) => throw new NotSupportedException();
public override void Write(byte[] buffer, int offset, int count) => throw new NotSupportedException();
}
}