Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
Prev Previous commit
Next Next commit
Make HostedFileDownloadStream explicitly read-only (#7394)
* Initial plan

* Make HostedFileDownloadStream read-only by default

Co-authored-by: stephentoub <2642209+stephentoub@users.noreply.github.com>

* Fix HostedFileDownloadStream write overload ordering

Co-authored-by: stephentoub <2642209+stephentoub@users.noreply.github.com>

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: stephentoub <2642209+stephentoub@users.noreply.github.com>
  • Loading branch information
2 people authored and jeffhandley committed Mar 17, 2026
commit eadf06d914306611452cd0e13ffdda553e93f45b
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,40 @@ protected HostedFileDownloadStream()
/// </remarks>
public virtual string? FileName => null;

/// <inheritdoc />
public override bool CanWrite => false;

/// <inheritdoc />
public override void SetLength(long value) => throw new NotSupportedException();

/// <inheritdoc />
public override IAsyncResult BeginWrite(byte[] buffer, int offset, int count, AsyncCallback? callback, object? state) =>
throw new NotSupportedException();

/// <inheritdoc />
public override void EndWrite(IAsyncResult asyncResult) => throw new NotSupportedException();

/// <inheritdoc />
public override void WriteByte(byte value) => throw new NotSupportedException();

/// <inheritdoc />
public override void Write(byte[] buffer, int offset, int count) => throw new NotSupportedException();

#if NET
/// <inheritdoc />
public override void Write(ReadOnlySpan<byte> buffer) => throw new NotSupportedException();
#endif

/// <inheritdoc />
public override Task WriteAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) =>
throw new NotSupportedException();

#if NET
/// <inheritdoc />
public override ValueTask WriteAsync(ReadOnlyMemory<byte> buffer, CancellationToken cancellationToken = default) =>
throw new NotSupportedException();
#endif

/// <summary>
/// Reads the entire stream content from its current position and returns it as a <see cref="DataContent"/>.
/// </summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -61,9 +61,6 @@ public override Task<DataContent> ToDataContentAsync(CancellationToken cancellat
/// <inheritdoc />
public override bool CanSeek => _innerStream.CanSeek;

/// <inheritdoc />
public override bool CanWrite => false;

/// <inheritdoc />
public override long Length => _innerStream.Length;

Expand Down Expand Up @@ -94,22 +91,10 @@ public override int Read(byte[] buffer, int offset, int count) =>
public override Task<int> ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) =>
_innerStream.ReadAsync(buffer, offset, count, cancellationToken);

/// <inheritdoc />
public override void Write(byte[] buffer, int offset, int count) =>
throw new NotSupportedException();

/// <inheritdoc />
public override Task WriteAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) =>
throw new NotSupportedException();

/// <inheritdoc />
public override long Seek(long offset, SeekOrigin origin) =>
_innerStream.Seek(offset, origin);

/// <inheritdoc />
public override void SetLength(long value) =>
_innerStream.SetLength(value);

/// <inheritdoc />
public override Task CopyToAsync(Stream destination, int bufferSize, CancellationToken cancellationToken) =>
_innerStream.CopyToAsync(destination, bufferSize, cancellationToken);
Expand All @@ -135,13 +120,6 @@ protected override void Dispose(bool disposing)
public override ValueTask<int> ReadAsync(Memory<byte> buffer, CancellationToken cancellationToken = default) =>
_innerStream.ReadAsync(buffer, cancellationToken);

/// <inheritdoc />
public override void Write(ReadOnlySpan<byte> buffer) => throw new NotSupportedException();

/// <inheritdoc />
public override ValueTask WriteAsync(ReadOnlyMemory<byte> buffer, CancellationToken cancellationToken = default) =>
throw new NotSupportedException();

/// <inheritdoc />
public override async ValueTask DisposeAsync()
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,31 @@ public async Task ToDataContentAsync_EmptyStream_ReturnsEmptyData()
Assert.Empty(content.Data.ToArray());
}

[Fact]
public void CanWrite_ReturnsFalse()
{
using var stream = new MinimalDownloadStream([1, 2, 3]);
Assert.False(stream.CanWrite);
}

[Fact]
public async Task WriteApis_ThrowNotSupportedException()
{
using var stream = new MinimalDownloadStream([1, 2, 3]);

Assert.Throws<NotSupportedException>(() => stream.SetLength(1));
Assert.Throws<NotSupportedException>(() => stream.Write([4], 0, 1));
Assert.Throws<NotSupportedException>(() => stream.WriteByte(4));
await Assert.ThrowsAsync<NotSupportedException>(() => stream.WriteAsync(new byte[] { 4 }, 0, 1));
Assert.Throws<NotSupportedException>(() => stream.BeginWrite([4], 0, 1, callback: null, state: null));
Assert.Throws<NotSupportedException>(() => stream.EndWrite(Task.CompletedTask));

#if NET
Assert.Throws<NotSupportedException>(() => stream.Write([4]));
await Assert.ThrowsAsync<NotSupportedException>(() => stream.WriteAsync(new byte[] { 4 }).AsTask());
#endif
}

/// <summary>
/// Minimal implementation that does not override MediaType or FileName, testing the default behavior.
/// </summary>
Expand All @@ -64,14 +89,11 @@ public MinimalDownloadStream(byte[] data)

public override bool CanRead => _inner.CanRead;
public override bool CanSeek => _inner.CanSeek;
public override bool CanWrite => false;
public override long Length => _inner.Length;
public override long Position { get => _inner.Position; set => _inner.Position = value; }
public override void Flush() => _inner.Flush();
public override int Read(byte[] buffer, int offset, int count) => _inner.Read(buffer, offset, count);
public override long Seek(long offset, SeekOrigin origin) => _inner.Seek(offset, origin);
public override void SetLength(long value) => _inner.SetLength(value);
public override void Write(byte[] buffer, int offset, int count) => throw new NotSupportedException();

protected override void Dispose(bool disposing)
{
Expand Down Expand Up @@ -102,14 +124,11 @@ public MetadataDownloadStream(byte[] data, string? mediaType, string? fileName)
public override string? FileName { get; }
public override bool CanRead => _inner.CanRead;
public override bool CanSeek => _inner.CanSeek;
public override bool CanWrite => false;
public override long Length => _inner.Length;
public override long Position { get => _inner.Position; set => _inner.Position = value; }
public override void Flush() => _inner.Flush();
public override int Read(byte[] buffer, int offset, int count) => _inner.Read(buffer, offset, count);
public override long Seek(long offset, SeekOrigin origin) => _inner.Seek(offset, origin);
public override void SetLength(long value) => _inner.SetLength(value);
public override void Write(byte[] buffer, int offset, int count) => throw new NotSupportedException();

protected override void Dispose(bool disposing)
{
Expand Down