Visitar URL original
fix(storage): Open a clone in CreateFromStream by mikernet · Pull Request #25867 · unoplatform/uno · GitHub
Skip to content

fix(storage): Open a clone in CreateFromStream - #25867

Open
mikernet wants to merge 3 commits into
unoplatform:masterfrom
mikernet:dev/mikernet/stream-reference-open
Open

mikernet wants to merge 3 commits into
unoplatform:masterfrom
mikernet:dev/mikernet/stream-reference-open

Conversation

@mikernet

@mikernet mikernet commented Nov 1, 2026

Copy link
Copy Markdown
Contributor

GitHub Issue: closes #25866

#25503 makes a similar CreateFromStream change inside the Wayland host work: it seeks each clone to 0, doesn't fall back to the source's content type, and locks the clone copies of InMemoryRandomAccessStream (unneeded after this PR) and RandomAccessStreamOverStream. This PR is the fix on its own, with runtime tests. Both change RandomAccessStreamReference.cs, RandomAccessStreamWithContentType.cs and InMemoryRandomAccessStream.cs, so the second to merge gets a small conflict; I'll rebase this one if #25503 lands first.

PR Type:

🐞 Bugfix

What changed? 🚀

RandomAccessStreamReference.CreateFromStream(stream) returned the caller's stream from every OpenReadAsync(), so all opens shared one position and one lifetime, and disposing one open disposed the source for every later open. A bitmap pasted from the Windows clipboard, which Uno creates with CreateFromStream, could be opened only once.

As on WinUI, each open now clones the source, so it starts at 0 with its own position and lifetime (CloneStream documents both):

  • InMemoryRandomAccessStream.CloneStream copies its buffer, so the clone starts at 0 and the source doesn't move; it used to start the clone at the source's position, moving the source while copying.
  • RandomAccessStreamWithContentType.CloneStream keeps the content type, as WinUI's clones do.
  • CreateFromStream opens stream.CloneStream(), and gives the clone the source's content type when it has none. If the source can't be cloned, the open fails, as on WinUI. Every Uno stream's clone starts at 0, so the open doesn't seek: some Android document-provider streams can't.
  • Win32ClipboardExtension.ResolveBitmap disposes the stream it opens, now a clone it owns, instead of restoring its position.

Threading: concurrent opens of one reference no longer race for an in-memory source. The AsRandomAccessStream() wrapper still moves the source while it copies a stream other than a MemoryStream, so those can race (#25503's lock there isn't part of this PR); the Windows clipboard and drag-and-drop references wrap a MemoryStream, and Clipboard.SetContent reads the bitmap on the caller's thread. Writes to the source during an open can still give a torn copy.

Behavior changes:

  • An app's stream whose CloneStream() throws now fails the open.
  • Each open of an in-memory source copies it. A wrapped FileStream or other .NET stream is read fully into memory on each open, and so is a WebAssembly download-picker file, whose content lives outside the WebAssembly heap. Sources over 2 GB fail (RandomAccessStreamOverStream.CloneStream).
  • A file-backed source opens a new file handle on each open, which the caller disposes.
  • For in-memory and wrapped .NET sources, writes through an opened stream, and writes to the source after an open, are no longer shared with that open; WinUI's clones share the data. File-backed opens still see each other's writes through the file.
  • BitmapSource.SetSource/SetSourceAsync (BitmapImage, WriteableBitmap) clone the stream, so an InMemoryRandomAccessStream that isn't at 0 now decodes from 0, as other Uno streams already did; SvgImageSource already decoded from 0.

Known issues, not changed here:

  • WinUI sniffs the content type from the content (image/png for PNG bytes, an empty string for unknown data); Uno reports the source's content type, or application/octet-stream.
  • WinUI seeks each opened clone to 0; Uno doesn't, since its own clones start at 0 and some Android document-provider streams can't seek. An app stream whose CloneStream() doesn't start at 0 opens wherever its clone starts.
  • WinUI's BitmapSource.SetSource and SvgImageSource.SetSourceAsync read the stream itself from its current position to its end, leaving it at the end; Uno decodes a copy from 0 and leaves the stream where it was.
  • An open after the source is disposed still succeeds when the clone doesn't need the source: the MemoryStream.AsRandomAccessStream() wrapper (MemoryStream.ToArray() works on a closed stream), file streams on every platform (their clones reopen the file or rent the base stream), and StreamedRandomAccessStream, which CreateFromUri downloads use. It fails for an InMemoryRandomAccessStream, as on WinUI.
  • File clones reopen the path with FileMode.OpenOrCreate (FileRandomAccessStream.local.cs, FileRandomAccessStream.UIKit.cs), so a moved or deleted file opens as a new, empty file.

Tests: on Skia Desktop (Windows), on master at 1f2fc30:

Test Before After
Given_InMemoryRandomAccessStream.When_CloneStream_Then_Clone_Starts_At_Zero_And_Source_Does_Not_Move fails: the clone starts at 4 passes
Given_StorageFile.When_CloneStream_Then_ContentType_Is_Kept fails: the clone has no content type passes
When_FromStream_Opened_Twice_Then_Opens_Are_Independent fails: the first open starts at 6 passes
When_FromStream_Opened_Stream_Disposed_Then_Source_And_Other_Opens_Stay_Readable, both data rows fail: ObjectDisposedException pass
When_FromStream_Source_Disposed_Then_Earlier_Opens_Stay_Readable_And_Later_Opens_Fail fails: ObjectDisposedException passes
When_FromStream_With_ContentType_Then_Opens_And_Clones_Keep_It fails: a clone of the open has no content type passes
When_FromStream_Source_Clones_Have_No_ContentType_Then_Opens_Keep_The_Source_ContentType (not on WinUI, which sniffs) passes, since the open was the source itself; fails with the fix minus the fallback passes
When_FromStream_Over_MemoryStream_Then_Opens_Are_Independent_And_Survive_Dispose (not on WinUI, which can't clone that wrapper) fails: the first open starts at 4 passes
When_FromStream_Source_Cannot_Be_Cloned_Then_Open_Fails fails: the open succeeds passes
Given_Clipboard.When_Clipboard_Bitmap_Reopened_After_Dispose_Then_Reads_All_Of_It (Win32 clipboard paste) fails: the second open is the disposed stream passes

The same assertions, except the clipboard test's, pass in a console app against WinUI's Windows.Storage.Streams (Windows 11 26200, CsWinRT; a temporary .png file stands in for the ms-appx: asset), except the MemoryStream test's, which is why it's excluded on WinUI. Also with the fix:

  • Given_Clipboard, Given_Win32DragDrop and the 66 Given_RichEditBox clipboard tests, in a window station with its own clipboard: 73 pass, 5 skipped by platform (before: the new test fails);
  • the storage and imaging tests: 453 pass, 1 inconclusive, 5 skipped, and Given_WriteableBitmap.When_WriteableBitmap_Assigned_With_Data_Present fails, as it does without this change;
  • WebAssembly (headless Chrome), Windows_Storage.Streams and Given_StorageFile, with all the new tests but the Win32 one: 188 pass, 1 skipped.

PR Checklist ✅

@mergify

mergify Bot commented Nov 10, 2026

Copy link
Copy Markdown
Contributor

Tick the box to add this pull request to the merge queue (same as @mergifyio queue). If it does not match the queue conditions yet, it enters the queue once it does.

  • Queue this pull request

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

RandomAccessStreamReference.CreateFromStream returns the same stream on every open

1 participant