Skip to content

Commit

Permalink
Introduce FileStreamStrategy as a first step of FileStream rewrite (#…
Browse files Browse the repository at this point in the history
…47128)

Co-authored-by: Stephen Toub <stoub@microsoft.com>
Co-authored-by: David Cantú <dacantu@microsoft.com>
  • Loading branch information
3 people authored Feb 24, 2021
1 parent 998126c commit 87a7159
Show file tree
Hide file tree
Showing 15 changed files with 1,133 additions and 760 deletions.
12 changes: 12 additions & 0 deletions src/libraries/System.IO.FileSystem/tests/FileStream/Flush.cs
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,18 @@ public void FlushCallsFlush_flushToDisk_False()
}
}

[Fact]
public void SafeFileHandleCallsFlush_flushToDisk_False()
{
using (StoreFlushArgFileStream fs = new StoreFlushArgFileStream(GetTestFilePath(), FileMode.Create))
{
GC.KeepAlive(fs.SafeFileHandle); // this should call Flush, which should call StoreFlushArgFileStream.Flush(false)

Assert.True(fs.LastFlushArg.HasValue);
Assert.False(fs.LastFlushArg.Value);
}
}

[Theory]
[InlineData(null)]
[InlineData(false)]
Expand Down
10 changes: 10 additions & 0 deletions src/libraries/System.IO.FileSystem/tests/FileStream/Position.cs
Original file line number Diff line number Diff line change
Expand Up @@ -28,5 +28,15 @@ public void SetPositionAppendModify()
Assert.Equal(length + 1, fs.Position);
}
}

[Fact]
public void GetPositionThrowsForUnseekableFileStream()
{
string fileName = GetTestFilePath();
using (FileStream fs = new UnseekableFileStream(fileName, FileMode.Create))
{
Assert.Throws<NotSupportedException>(() => _ = fs.Position);
}
}
}
}
20 changes: 20 additions & 0 deletions src/libraries/System.IO.FileSystem/tests/FileStream/SetLength.cs
Original file line number Diff line number Diff line change
Expand Up @@ -25,5 +25,25 @@ public void SetLengthAppendModifyThrows()
Assert.Equal(length, fs.Length);
}
}

[Fact]
public void SetLengthThrowsForUnseekableFileStream()
{
string fileName = GetTestFilePath();
using (FileStream fs = new UnseekableFileStream(fileName, FileMode.Create))
{
Assert.Throws<NotSupportedException>(() => fs.SetLength(1));
}
}

[Fact]
public void GetLengthThrowsForUnseekableFileStream()
{
string fileName = GetTestFilePath();
using (FileStream fs = new UnseekableFileStream(fileName, FileMode.Create))
{
Assert.Throws<NotSupportedException>(() => _ = fs.Length);
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -393,6 +393,7 @@
<Compile Include="$(MSBuildThisFileDirectory)System\IO\BinaryReader.cs" />
<Compile Include="$(MSBuildThisFileDirectory)System\IO\BinaryWriter.cs" />
<Compile Include="$(MSBuildThisFileDirectory)System\IO\BufferedStream.cs" />
<Compile Include="$(MSBuildThisFileDirectory)System\IO\DerivedFileStreamStrategy.cs" />
<Compile Include="$(MSBuildThisFileDirectory)System\IO\DirectoryNotFoundException.cs" />
<Compile Include="$(MSBuildThisFileDirectory)System\IO\EncodingCache.cs" />
<Compile Include="$(MSBuildThisFileDirectory)System\IO\EndOfStreamException.cs" />
Expand All @@ -405,10 +406,12 @@
<Compile Include="$(MSBuildThisFileDirectory)System\IO\FileOptions.cs" />
<Compile Include="$(MSBuildThisFileDirectory)System\IO\FileShare.cs" />
<Compile Include="$(MSBuildThisFileDirectory)System\IO\FileStream.cs" />
<Compile Include="$(MSBuildThisFileDirectory)System\IO\FileStreamStrategy.cs" />
<Compile Include="$(MSBuildThisFileDirectory)System\IO\HandleInheritability.cs" />
<Compile Include="$(MSBuildThisFileDirectory)System\IO\InvalidDataException.cs" />
<Compile Include="$(MSBuildThisFileDirectory)System\IO\IOException.cs" />
<Compile Include="$(MSBuildThisFileDirectory)System\IO\MemoryStream.cs" />
<Compile Include="$(MSBuildThisFileDirectory)System\IO\LegacyFileStreamStrategy.cs" />
<Compile Include="$(MSBuildThisFileDirectory)System\IO\Path.cs" />
<Compile Include="$(MSBuildThisFileDirectory)System\IO\PathInternal.cs" />
<Compile Include="$(MSBuildThisFileDirectory)System\IO\PathTooLongException.cs" />
Expand Down Expand Up @@ -1631,14 +1634,14 @@
<Compile Include="$(MSBuildThisFileDirectory)System\Globalization\GlobalizationMode.Windows.cs" />
<Compile Include="$(MSBuildThisFileDirectory)System\Globalization\HijriCalendar.Win32.cs" />
<Compile Include="$(MSBuildThisFileDirectory)System\Guid.Windows.cs" />
<Compile Include="$(MSBuildThisFileDirectory)System\IO\DisableMediaInsertionPrompt.cs" />
<Compile Include="$(MSBuildThisFileDirectory)System\IO\DriveInfoInternal.Windows.cs" />
<Compile Include="$(MSBuildThisFileDirectory)System\IO\FileStream.Windows.cs" />
<Compile Include="$(MSBuildThisFileDirectory)System\IO\FileStream.Win32.cs" />
<Compile Include="$(MSBuildThisFileDirectory)System\IO\FileStreamHelpers.Windows.cs" />
<Compile Include="$(MSBuildThisFileDirectory)System\IO\FileStreamCompletionSource.Win32.cs" />
<Compile Include="$(MSBuildThisFileDirectory)System\IO\Path.Windows.cs" />
<Compile Include="$(MSBuildThisFileDirectory)System\IO\PathHelper.Windows.cs" />
<Compile Include="$(MSBuildThisFileDirectory)System\IO\PathInternal.Windows.cs" />
<Compile Include="$(MSBuildThisFileDirectory)System\IO\DisableMediaInsertionPrompt.cs" />
<Compile Include="$(MSBuildThisFileDirectory)System\IO\LegacyFileStreamStrategy.Windows.cs" />
<Compile Include="$(MSBuildThisFileDirectory)System\PasteArguments.Windows.cs" />
<Compile Include="$(MSBuildThisFileDirectory)System\Runtime\Loader\LibraryNameVariation.Windows.cs" />
<Compile Include="$(MSBuildThisFileDirectory)System\Runtime\MemoryFailPoint.Windows.cs" />
Expand Down Expand Up @@ -1837,12 +1840,13 @@
<Compile Include="$(MSBuildThisFileDirectory)System\Globalization\GlobalizationMode.Unix.cs" />
<Compile Include="$(MSBuildThisFileDirectory)System\Globalization\HijriCalendar.Unix.cs" />
<Compile Include="$(MSBuildThisFileDirectory)System\Guid.Unix.cs" />
<Compile Include="$(MSBuildThisFileDirectory)System\IO\FileStream.Lock.OSX.cs" Condition="'$(IsOSXLike)' == 'true'" />
<Compile Include="$(MSBuildThisFileDirectory)System\IO\FileStream.Lock.Unix.cs" Condition="'$(IsOSXLike)' != 'true'" />
<Compile Include="$(MSBuildThisFileDirectory)System\IO\FileStream.Unix.cs" />
<Compile Include="$(MSBuildThisFileDirectory)System\IO\FileStreamHelpers.Unix.cs" />
<Compile Include="$(MSBuildThisFileDirectory)System\IO\Path.Unix.cs" />
<Compile Include="$(MSBuildThisFileDirectory)System\IO\PathInternal.Unix.cs" />
<Compile Include="$(MSBuildThisFileDirectory)System\IO\PersistedFiles.Names.Unix.cs" />
<Compile Include="$(MSBuildThisFileDirectory)System\IO\LegacyFileStreamStrategy.Unix.cs" />
<Compile Include="$(MSBuildThisFileDirectory)System\IO\LegacyFileStreamStrategy.Lock.OSX.cs" Condition="'$(IsOSXLike)' == 'true'" />
<Compile Include="$(MSBuildThisFileDirectory)System\IO\LegacyFileStreamStrategy.Lock.Unix.cs" Condition="'$(IsOSXLike)' != 'true'" />
<Compile Include="$(MSBuildThisFileDirectory)System\PasteArguments.Unix.cs" />
<Compile Include="$(MSBuildThisFileDirectory)System\Runtime\Loader\LibraryNameVariation.Unix.cs" />
<Compile Include="$(MSBuildThisFileDirectory)System\Runtime\MemoryFailPoint.Unix.cs" />
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System.Threading;
using System.Threading.Tasks;
using Microsoft.Win32.SafeHandles;

namespace System.IO
{
// this type exists so we can avoid GetType() != typeof(FileStream) checks in FileStream
// when FileStream was supposed to call base.Method() for such cases, we just call _fileStream.BaseMethod()
// for everything else we fall back to the actual strategy (like FileStream does)
//
// it's crucial to NOT use the "base" keyoword here! everything must be using _fileStream or _strategy
internal sealed class DerivedFileStreamStrategy : FileStreamStrategy
{
private readonly FileStreamStrategy _strategy;

internal DerivedFileStreamStrategy(FileStream fileStream, FileStreamStrategy strategy) : base(fileStream) => _strategy = strategy;

public override bool CanRead => _strategy.CanRead;

public override bool CanWrite => _strategy.CanWrite;

public override bool CanSeek => _strategy.CanSeek;

public override long Length => _strategy.Length;

public override long Position
{
get => _strategy.Position;
set => _strategy.Position = value;
}

internal override bool IsAsync => _strategy.IsAsync;

internal override string Name => _strategy.Name;

internal override SafeFileHandle SafeFileHandle => _strategy.SafeFileHandle;

internal override bool IsClosed => _strategy.IsClosed;

internal override void Lock(long position, long length) => _strategy.Lock(position, length);

internal override void Unlock(long position, long length) => _strategy.Unlock(position, length);

public override long Seek(long offset, SeekOrigin origin) => _strategy.Seek(offset, origin);

public override void SetLength(long value) => _strategy.SetLength(value);

public override int ReadByte() => _strategy.ReadByte();

public override IAsyncResult BeginRead(byte[] buffer, int offset, int count, AsyncCallback? callback, object? state)
=> _strategy.IsAsync
? _strategy.BeginRead(buffer, offset, count, callback, state)
: _fileStream.BaseBeginRead(buffer, offset, count, callback, state);

public override int EndRead(IAsyncResult asyncResult)
=> _strategy.IsAsync ? _strategy.EndRead(asyncResult) : _fileStream.BaseEndRead(asyncResult);

public override int Read(byte[] buffer, int offset, int count) => _strategy.Read(buffer, offset, count);

// If this is a derived type, it may have overridden Read(byte[], int, int) prior to this Read(Span<byte>)
// overload being introduced. In that case, this Read(Span<byte>) overload should use the behavior
// of Read(byte[],int,int) overload.
public override int Read(Span<byte> buffer)
=> _fileStream.BaseRead(buffer);

// If we have been inherited into a subclass, the Strategy implementation could be incorrect
// since it does not call through to Read() which a subclass might have overridden.
// To be safe we will only use this implementation in cases where we know it is safe to do so,
// and delegate to FileStream base class (which will call into Read/ReadAsync) when we are not sure.
public override Task<int> ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken)
=> _fileStream.BaseReadAsync(buffer, offset, count, cancellationToken);

// If this isn't a concrete FileStream, a derived type may have overridden ReadAsync(byte[],...),
// which was introduced first, so delegate to the base which will delegate to that.
public override ValueTask<int> ReadAsync(Memory<byte> buffer, CancellationToken cancellationToken = default)
=> _fileStream.BaseReadAsync(buffer, cancellationToken);

public override IAsyncResult BeginWrite(byte[] buffer, int offset, int count, AsyncCallback? callback, object? state)
=> _strategy.IsAsync
? _strategy.BeginWrite(buffer, offset, count, callback, state)
: _fileStream.BaseBeginWrite(buffer, offset, count, callback, state);

public override void EndWrite(IAsyncResult asyncResult)
{
if (_strategy.IsAsync)
{
_strategy.EndWrite(asyncResult);
}
else
{
_fileStream.BaseEndWrite(asyncResult);
}
}

public override void WriteByte(byte value) => _strategy.WriteByte(value);

public override void Write(byte[] buffer, int offset, int count) => _strategy.Write(buffer, offset, count);

// If this is a derived type, it may have overridden Write(byte[], int, int) prior to this Write(ReadOnlySpan<byte>)
// overload being introduced. In that case, this Write(ReadOnlySpan<byte>) overload should use the behavior
// of Write(byte[],int,int) overload.
public override void Write(ReadOnlySpan<byte> buffer)
=> _fileStream.BaseWrite(buffer);

// If we have been inherited into a subclass, the Strategy implementation could be incorrect
// since it does not call through to Write() or WriteAsync() which a subclass might have overridden.
// To be safe we will only use this implementation in cases where we know it is safe to do so,
// and delegate to our base class (which will call into Write/WriteAsync) when we are not sure.
public override Task WriteAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken)
=> _fileStream.BaseWriteAsync(buffer, offset, count, cancellationToken);

// If this isn't a concrete FileStream, a derived type may have overridden WriteAsync(byte[],...),
// which was introduced first, so delegate to the base which will delegate to that.
public override ValueTask WriteAsync(ReadOnlyMemory<byte> buffer, CancellationToken cancellationToken = default)
=> _fileStream.BaseWriteAsync(buffer, cancellationToken);

public override void Flush() => throw new InvalidOperationException("FileStream should never call this method.");

internal override void Flush(bool flushToDisk) => _strategy.Flush(flushToDisk);

// If we have been inherited into a subclass, the following implementation could be incorrect
// since it does not call through to Flush() which a subclass might have overridden. To be safe
// we will only use this implementation in cases where we know it is safe to do so,
// and delegate to our base class (which will call into Flush) when we are not sure.
public override Task FlushAsync(CancellationToken cancellationToken)
=> _fileStream.BaseFlushAsync(cancellationToken);

// We also need to take this path if this is a derived
// instance from FileStream, as a derived type could have overridden ReadAsync, in which
// case our custom CopyToAsync implementation isn't necessarily correct.
public override Task CopyToAsync(Stream destination, int bufferSize, CancellationToken cancellationToken)
=> _fileStream.BaseCopyToAsync(destination, bufferSize, cancellationToken);

public override ValueTask DisposeAsync() => _fileStream.BaseDisposeAsync();

internal override void DisposeInternal(bool disposing) => _strategy.DisposeInternal(disposing);
}
}
Loading

0 comments on commit 87a7159

Please sign in to comment.