Fixes System.Drawing.Image.FromStream(...) throws "Parameter is not valid." when Stream.Read does not return full file length#14705
Open
ricardobossan wants to merge 2 commits into
Conversation
- Fix `ComManagedStream.Read` (the COM `IStream` wrapper used when a managed `Stream` is handed to GDI+) to keep reading until the requested buffer is filled or the stream reaches EOF, instead of issuing a single `Stream.Read`. - This repairs `Image.FromStream` / `Bitmap.FromStream` / `new Bitmap(stream)` / `Metafile(stream)` for any stream that legally returns fewer bytes than requested (chunked / network / zip-wrapped), which GDI+ was rejecting with `Parameter is not valid.` - Add regression tests for the short-read case and the read-past-EOF case. - Loading images through `System.Drawing` from a stream that returns partial reads now succeeds instead of throwing `ArgumentException: Parameter is not valid.` Reported in dotnet#14064 (and earlier in dotnet#8824). - No change for streams that already returned full reads (`FileStream`, `MemoryStream`, ...); those keep working exactly as before. - No. The single-read behavior was ported verbatim from the legacy `GPStream` (git blame) and predates that; dotnet#8824 shows the same failure on .NET Core 3.1 / System.Drawing.Common 4.7.0. - Low. The change is confined to one method and only adds a fill loop: it returns identical results for full-read streams and the correct total for short-read streams. The COM `IStream::Read` contract explicitly allows satisfying a request across multiple source reads. - New unit tests in `ComManagedStreamTests` call `IStream::Read` directly against a stream that caps every read: one asserts the buffer is filled across chunks, the other asserts a request past EOF returns only the available bytes without throwing. - Manual end-to-end check using the attached `Program.cs` (paste it into `src/test/integration/ScratchProject/Program.cs` and run): it decodes an in-memory PNG through a 32-byte-per-read stream via `Image.FromStream` and shows the loaded image. Throws `Parameter is not valid.` before the fix, loads after. - Full `System.Private.Windows.Core.Tests` suite. - 11.0.100-preview.5.26302.115
Contributor
There was a problem hiding this comment.
Pull request overview
This PR fixes System.Drawing stream decoding failures by improving the COM IStream wrapper (ComManagedStream) so it can satisfy a single read request across multiple underlying Stream.Read calls (handling “short reads” correctly), and adds regression tests to cover the scenario.
Changes:
- Update
ComManagedStream’sISequentialStream::Readimplementation to keep reading until the requested buffer is filled or EOF is reached. - Add unit tests that exercise short-read streams and reads that request more bytes than remain.
Reviewed changes
Copilot reviewed 2 out of 2 changed files in this pull request and generated 2 comments.
| File | Description |
|---|---|
| src/System.Private.Windows.Core/src/Windows/Win32/System/Com/ComManagedStream.cs | Implements a fill-loop in Read to handle short reads from managed streams passed to native consumers (e.g., GDI+). |
| src/System.Private.Windows.Core/tests/System.Private.Windows.Core.Tests/Windows/Win32/System/Com/ComManagedStreamTests.cs | Adds regression coverage for chunked/short-read behavior and read-past-EOF behavior via direct IStream::Read calls. |
Comments suppressed due to low confidence (1)
src/System.Private.Windows.Core/src/Windows/Win32/System/Com/ComManagedStream.cs:143
ISequentialStream::Readshould returnS_FALSEwhen fewer thancbbytes are read due to end-of-stream. ReturningS_OKeven on short reads diverges from the COM contract and makes it harder for callers to distinguish a complete read from EOF.
if (pcbRead is not null)
*pcbRead = (uint)read;
return HRESULT.S_OK;
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Per IStream::Read, S_OK means a full read and a short read signals EOF. Update the past-EOF test to expect S_FALSE and verify the reassembled bytes.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #14064
Proposed changes
ComManagedStream.Read(the COMIStreamwrapper used when a managedStreamis handed to GDI+) to keep reading until the requested buffer is filled or the stream reaches EOF, instead of issuing a singleStream.Read.Image.FromStream/Bitmap.FromStream/new Bitmap(stream)/Metafile(stream)for any stream that legally returns fewer bytes than requested (chunked / network / zip-wrapped), which GDI+ was rejecting withParameter is not valid.System.Drawingfrom a stream that returns partial reads now succeeds instead of throwingArgumentException: Parameter is not valid.(Also reported in System.Drawing.Bitmap.FromStream fails when called with ZipWrappingStream #8824).FileStream,MemoryStream, ...); those keep working exactly as before.Regression?
GPStream(git blame) and predates that; System.Drawing.Bitmap.FromStream fails when called with ZipWrappingStream #8824 shows the same failure on .NET Core 3.1 / System.Drawing.Common 4.7.0.Risk
IStream::Readcontract explicitly allows satisfying a request across multiple source reads.Screenshots
Before
After
Test methodology
ComManagedStreamTestscallIStream::Readdirectly against a stream that caps every read: one asserts the buffer is filled across chunks, the other asserts a request past EOF returns only the available bytes without throwing.src/test/integration/ScratchProject/Program.csand run): it decodes an in-memory PNG through a 32-byte-per-read stream viaImage.FromStreamand shows the loaded image. ThrowsParameter is not valid.before the fix, loads after.System.Private.Windows.Core.Testssuite.Test environment(s)
Microsoft Reviewers: Open in CodeFlow