// Copyright (C) 2025, The Duplicati Team // https://duplicati.com, hello@duplicati.com // // Permission is hereby granted, free of charge, to any person obtaining a // copy of this software and associated documentation files (the "Software"), // to deal in the Software without restriction, including without limitation // the rights to use, copy, modify, merge, publish, distribute, sublicense, // and/or sell copies of the Software, and to permit persons to whom the // Software is furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in // all copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS // OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING // FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER // DEALINGS IN THE SOFTWARE. #nullable enable using System; using System.IO; using System.Threading; using System.Threading.Channels; using System.Threading.Tasks; namespace Duplicati.Library.Utility; /// /// A stream that maps data with an internal buffer. /// This allows a reader and a writer to share a stream without blocking. /// public class MapStream : Stream { /// /// The channel used to communicate between the reader and writer. /// private readonly Channel channel = Channel.CreateBounded(0); /// /// The buffer used to store data that has been written but not consumed by the reader. /// private Memory readBuffer = Array.Empty(); /// /// The task that is using the map stream. /// public Task? CopyTask { get; set; } /// public override bool CanRead => true; /// public override bool CanSeek => false; /// public override bool CanWrite => true; /// public override long Length => throw new NotImplementedException(); /// public override long Position { get => throw new NotImplementedException(); set => throw new NotImplementedException(); } /// public override async Task ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) { int read = 0; // If we have left-over data from the previous read, return it if (readBuffer.Length > 0) { // Return as much as possible var requested = Math.Min(readBuffer.Length, count); readBuffer.Span.Slice(0, requested).CopyTo(buffer.AsSpan(offset, count)); // Adjust the remaining buffer if (readBuffer.Length > requested) readBuffer = readBuffer.Slice(requested); else readBuffer = Array.Empty(); // Adjust the counters offset += requested; count -= requested; read += requested; // Return if we have read all that was requested if (count == 0) return read; } // Handle external task var ct = CopyTask; // If the task is completed, clear it if (ct != null && ct.IsCompletedSuccessfully) CopyTask = ct = null; // If the task failed, stop reading else if (ct != null && (ct.IsCanceled || ct.IsFaulted)) { // If we have any data in the buffer, return it if (read > 0) return read; // Otherwise, throw the exception await ct; } if (!await channel.Reader.WaitToReadAsync(cancellationToken)) { // If we have any data in the buffer, return it if (read > 0) return read; // If the copy crashed, throw the exception ct = CopyTask; if (ct != null && (ct.IsCanceled || ct.IsFaulted)) await ct; // Otherwise, return 0 return read; } // Read the data from the channel var res = await channel.Reader.ReadAsync(cancellationToken); // Consume as much as was requested var consumed = Math.Min(res.Length, count); // Keep the rest in the buffer if (res.Length > count) readBuffer = res.AsMemory(count); res.AsSpan(0, consumed).CopyTo(buffer.AsSpan(offset, count)); read += consumed; return read; } /// public override async Task WriteAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) { var ct = CopyTask; // Throw if the task failed if (ct != null && (ct.IsCanceled || ct.IsFaulted)) await ct; await channel.Writer.WriteAsync(buffer.AsSpan(offset, count).ToArray(), cancellationToken); } public override void Close() { base.Close(); channel.Writer.Complete(); } protected override void Dispose(bool disposing) { channel.Writer.Complete(); base.Dispose(disposing); } public override void Flush() { } public override int Read(byte[] buffer, int offset, int count) => ReadAsync(buffer, offset, count, CancellationToken.None).Await(); public override long Seek(long offset, SeekOrigin origin) => throw new NotImplementedException(); public override void SetLength(long value) => throw new NotImplementedException(); public override void Write(byte[] buffer, int offset, int count) => WriteAsync(buffer, offset, count, CancellationToken.None).Await(); }