// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Win32.SafeHandles;
using System.Diagnostics;
namespace System.IO
{
public partial class FileStream : Stream
{
private const FileShare DefaultShare = FileShare.Read;
private const bool DefaultIsAsync = false;
internal const int DefaultBufferSize = 4096;
private byte[] _buffer;
private int _bufferLength;
private readonly SafeFileHandle _fileHandle;
/// Whether the file is opened for reading, writing, or both.
private readonly FileAccess _access;
/// The path to the opened file.
private readonly string _path;
/// The next available byte to be read from the _buffer.
private int _readPos;
/// The number of valid bytes in _buffer.
private int _readLength;
/// The next location in which a write should occur to the buffer.
private int _writePos;
///
/// Whether asynchronous read/write/flush operations should be performed using async I/O.
/// On Windows FileOptions.Asynchronous controls how the file handle is configured,
/// and then as a result how operations are issued against that file handle. On Unix,
/// there isn't any distinction around how file descriptors are created for async vs
/// sync, but we still differentiate how the operations are issued in order to provide
/// similar behavioral semantics and performance characteristics as on Windows. On
/// Windows, if non-async, async read/write requests just delegate to the base stream,
/// and no attempt is made to synchronize between sync and async operations on the stream;
/// if async, then async read/write requests are implemented specially, and sync read/write
/// requests are coordinated with async ones by implementing the sync ones over the async
/// ones. On Unix, we do something similar. If non-async, async read/write requests just
/// delegate to the base stream, and no attempt is made to synchronize. If async, we use
/// a semaphore to coordinate both sync and async operations.
///
private readonly bool _useAsyncIO;
///
/// Currently cached position in the stream. This should always mirror the underlying file's actual position,
/// and should only ever be out of sync if another stream with access to this same file manipulates it, at which
/// point we attempt to error out.
///
private long _filePosition;
/// Whether the file stream's handle has been exposed.
private bool _exposedHandle;
[Obsolete("This constructor has been deprecated. Please use new FileStream(SafeFileHandle handle, FileAccess access) instead. http://go.microsoft.com/fwlink/?linkid=14202")]
public FileStream(IntPtr handle, FileAccess access)
: this(handle, access, true, DefaultBufferSize, false)
{
}
[Obsolete("This constructor has been deprecated. Please use new FileStream(SafeFileHandle handle, FileAccess access) instead, and optionally make a new SafeFileHandle with ownsHandle=false if needed. http://go.microsoft.com/fwlink/?linkid=14202")]
public FileStream(IntPtr handle, FileAccess access, bool ownsHandle)
: this(handle, access, ownsHandle, DefaultBufferSize, false)
{
}
[Obsolete("This constructor has been deprecated. Please use new FileStream(SafeFileHandle handle, FileAccess access, int bufferSize) instead, and optionally make a new SafeFileHandle with ownsHandle=false if needed. http://go.microsoft.com/fwlink/?linkid=14202")]
public FileStream(IntPtr handle, FileAccess access, bool ownsHandle, int bufferSize)
: this(handle, access, ownsHandle, bufferSize, false)
{
}
[Obsolete("This constructor has been deprecated. Please use new FileStream(SafeFileHandle handle, FileAccess access, int bufferSize, bool isAsync) instead, and optionally make a new SafeFileHandle with ownsHandle=false if needed. http://go.microsoft.com/fwlink/?linkid=14202")]
public FileStream(IntPtr handle, FileAccess access, bool ownsHandle, int bufferSize, bool isAsync)
: this(new SafeFileHandle(handle, ownsHandle), access, bufferSize, isAsync)
{
}
public FileStream(SafeFileHandle handle, FileAccess access)
: this(handle, access, DefaultBufferSize)
{
}
public FileStream(SafeFileHandle handle, FileAccess access, int bufferSize)
: this(handle, access, bufferSize, GetDefaultIsAsync(handle))
{
}
public FileStream(SafeFileHandle handle, FileAccess access, int bufferSize, bool isAsync)
{
if (handle.IsInvalid)
throw new ArgumentException(SR.Arg_InvalidHandle, nameof(handle));
if (access < FileAccess.Read || access > FileAccess.ReadWrite)
throw new ArgumentOutOfRangeException(nameof(access), SR.ArgumentOutOfRange_Enum);
if (bufferSize <= 0)
throw new ArgumentOutOfRangeException(nameof(bufferSize), SR.ArgumentOutOfRange_NeedPosNum);
if (handle.IsClosed)
throw new ObjectDisposedException(SR.ObjectDisposed_FileClosed);
if (handle.IsAsync.HasValue && isAsync != handle.IsAsync.Value)
throw new ArgumentException(SR.Arg_HandleNotAsync, nameof(handle));
_access = access;
_useAsyncIO = isAsync;
_exposedHandle = true;
_bufferLength = bufferSize;
_fileHandle = handle;
InitFromHandle(handle);
}
public FileStream(string path, FileMode mode) :
this(path, mode, (mode == FileMode.Append ? FileAccess.Write : FileAccess.ReadWrite), DefaultShare, DefaultBufferSize, DefaultIsAsync)
{ }
public FileStream(string path, FileMode mode, FileAccess access) :
this(path, mode, access, DefaultShare, DefaultBufferSize, DefaultIsAsync)
{ }
public FileStream(string path, FileMode mode, FileAccess access, FileShare share) :
this(path, mode, access, share, DefaultBufferSize, DefaultIsAsync)
{ }
public FileStream(string path, FileMode mode, FileAccess access, FileShare share, int bufferSize) :
this(path, mode, access, share, bufferSize, DefaultIsAsync)
{ }
public FileStream(string path, FileMode mode, FileAccess access, FileShare share, int bufferSize, bool useAsync) :
this(path, mode, access, share, bufferSize, useAsync ? FileOptions.Asynchronous : FileOptions.None)
{ }
public FileStream(string path, FileMode mode, FileAccess access, FileShare share, int bufferSize, FileOptions options)
{
if (path == null)
throw new ArgumentNullException(nameof(path), SR.ArgumentNull_Path);
if (path.Length == 0)
throw new ArgumentException(SR.Argument_EmptyPath, nameof(path));
// don't include inheritable in our bounds check for share
FileShare tempshare = share & ~FileShare.Inheritable;
string badArg = null;
if (mode < FileMode.CreateNew || mode > FileMode.Append)
badArg = nameof(mode);
else if (access < FileAccess.Read || access > FileAccess.ReadWrite)
badArg = nameof(access);
else if (tempshare < FileShare.None || tempshare > (FileShare.ReadWrite | FileShare.Delete))
badArg = nameof(share);
if (badArg != null)
throw new ArgumentOutOfRangeException(badArg, SR.ArgumentOutOfRange_Enum);
// NOTE: any change to FileOptions enum needs to be matched here in the error validation
if (options != FileOptions.None && (options & ~(FileOptions.WriteThrough | FileOptions.Asynchronous | FileOptions.RandomAccess | FileOptions.DeleteOnClose | FileOptions.SequentialScan | FileOptions.Encrypted | (FileOptions)0x20000000 /* NoBuffering */)) != 0)
throw new ArgumentOutOfRangeException(nameof(options), SR.ArgumentOutOfRange_Enum);
if (bufferSize <= 0)
throw new ArgumentOutOfRangeException(nameof(bufferSize), SR.ArgumentOutOfRange_NeedPosNum);
// Write access validation
if ((access & FileAccess.Write) == 0)
{
if (mode == FileMode.Truncate || mode == FileMode.CreateNew || mode == FileMode.Create || mode == FileMode.Append)
{
// No write access, mode and access disagree but flag access since mode comes first
throw new ArgumentException(SR.Format(SR.Argument_InvalidFileModeAndAccessCombo, mode, access), nameof(access));
}
}
if ((access & FileAccess.Read) != 0 && mode == FileMode.Append)
throw new ArgumentException(SR.Argument_InvalidAppendMode, nameof(access));
string fullPath = Path.GetFullPath(path);
_path = fullPath;
_access = access;
_bufferLength = bufferSize;
if ((options & FileOptions.Asynchronous) != 0)
_useAsyncIO = true;
_fileHandle = OpenHandle(mode, share, options);
try
{
Init(mode, share);
}
catch
{
// If anything goes wrong while setting up the stream, make sure we deterministically dispose
// of the opened handle.
_fileHandle.Dispose();
_fileHandle = null;
throw;
}
}
private static bool GetDefaultIsAsync(SafeFileHandle handle)
{
// This will eventually get more complicated as we can actually check the underlying handle type on Windows
return handle.IsAsync.HasValue ? handle.IsAsync.Value : false;
}
[Obsolete("This property has been deprecated. Please use FileStream's SafeFileHandle property instead. http://go.microsoft.com/fwlink/?linkid=14202")]
public virtual IntPtr Handle { get { return SafeFileHandle.DangerousGetHandle(); } }
public virtual void Lock(long position, long length)
{
if (position < 0 || length < 0)
{
throw new ArgumentOutOfRangeException(position < 0 ? nameof(position) : nameof(length), SR.ArgumentOutOfRange_NeedNonNegNum);
}
if (_fileHandle.IsClosed)
{
throw Error.GetFileNotOpen();
}
LockInternal(position, length);
}
public virtual void Unlock(long position, long length)
{
if (position < 0 || length < 0)
{
throw new ArgumentOutOfRangeException(position < 0 ? nameof(position) : nameof(length), SR.ArgumentOutOfRange_NeedNonNegNum);
}
if (_fileHandle.IsClosed)
{
throw Error.GetFileNotOpen();
}
UnlockInternal(position, length);
}
public override Task FlushAsync(CancellationToken cancellationToken)
{
// 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.
if (GetType() != typeof(FileStream))
return base.FlushAsync(cancellationToken);
return FlushAsyncInternal(cancellationToken);
}
public override Task ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken)
{
if (buffer == null)
throw new ArgumentNullException(nameof(buffer), SR.ArgumentNull_Buffer);
if (offset < 0)
throw new ArgumentOutOfRangeException(nameof(offset), SR.ArgumentOutOfRange_NeedNonNegNum);
if (count < 0)
throw new ArgumentOutOfRangeException(nameof(count), SR.ArgumentOutOfRange_NeedNonNegNum);
if (buffer.Length - offset < count)
throw new ArgumentException(SR.Argument_InvalidOffLen /*, no good single parameter name to pass*/);
// If we have been inherited into a subclass, the following implementation could be incorrect
// since it does not call through to Read() or ReadAsync() 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 Read/ReadAsync) when we are not sure.
if (GetType() != typeof(FileStream))
return base.ReadAsync(buffer, offset, count, cancellationToken);
if (cancellationToken.IsCancellationRequested)
return Task.FromCanceled(cancellationToken);
if (IsClosed)
throw Error.GetFileNotOpen();
return ReadAsyncInternal(buffer, offset, count, cancellationToken);
}
public override Task WriteAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken)
{
if (buffer == null)
throw new ArgumentNullException(nameof(buffer), SR.ArgumentNull_Buffer);
if (offset < 0)
throw new ArgumentOutOfRangeException(nameof(offset), SR.ArgumentOutOfRange_NeedNonNegNum);
if (count < 0)
throw new ArgumentOutOfRangeException(nameof(count), SR.ArgumentOutOfRange_NeedNonNegNum);
if (buffer.Length - offset < count)
throw new ArgumentException(SR.Argument_InvalidOffLen /*, no good single parameter name to pass*/);
// If we have been inherited into a subclass, the following 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.
if (GetType() != typeof(FileStream))
return base.WriteAsync(buffer, offset, count, cancellationToken);
if (cancellationToken.IsCancellationRequested)
return Task.FromCanceled(cancellationToken);
if (IsClosed)
throw Error.GetFileNotOpen();
return WriteAsyncInternal(buffer, offset, count, cancellationToken);
}
///
/// Clears buffers for this stream and causes any buffered data to be written to the file.
///
public override void Flush()
{
// Make sure that we call through the public virtual API
Flush(flushToDisk: false);
}
///
/// Clears buffers for this stream, and if is true,
/// causes any buffered data to be written to the file.
///
public virtual void Flush(bool flushToDisk)
{
if (IsClosed) throw Error.GetFileNotOpen();
FlushInternalBuffer();
if (flushToDisk && CanWrite)
{
FlushOSBuffer();
}
}
/// Gets a value indicating whether the current stream supports reading.
public override bool CanRead
{
get { return !_fileHandle.IsClosed && (_access & FileAccess.Read) != 0; }
}
/// Gets a value indicating whether the current stream supports writing.
public override bool CanWrite
{
get { return !_fileHandle.IsClosed && (_access & FileAccess.Write) != 0; }
}
/// Validates arguments to Read and Write and throws resulting exceptions.
/// The buffer to read from or write to.
/// The zero-based offset into the array.
/// The maximum number of bytes to read or write.
private void ValidateReadWriteArgs(byte[] array, int offset, int count)
{
if (array == null)
throw new ArgumentNullException(nameof(array), SR.ArgumentNull_Buffer);
if (offset < 0)
throw new ArgumentOutOfRangeException(nameof(offset), SR.ArgumentOutOfRange_NeedNonNegNum);
if (count < 0)
throw new ArgumentOutOfRangeException(nameof(count), SR.ArgumentOutOfRange_NeedNonNegNum);
if (array.Length - offset < count)
throw new ArgumentException(SR.Argument_InvalidOffLen /*, no good single parameter name to pass*/);
if (_fileHandle.IsClosed)
throw Error.GetFileNotOpen();
}
/// Sets the length of this stream to the given value.
/// The new length of the stream.
public override void SetLength(long value)
{
if (value < 0)
throw new ArgumentOutOfRangeException(nameof(value), SR.ArgumentOutOfRange_NeedNonNegNum);
if (_fileHandle.IsClosed)
throw Error.GetFileNotOpen();
if (!CanSeek)
throw Error.GetSeekNotSupported();
if (!CanWrite)
throw Error.GetWriteNotSupported();
SetLengthInternal(value);
}
public virtual SafeFileHandle SafeFileHandle
{
get
{
Flush();
_exposedHandle = true;
return _fileHandle;
}
}
/// Gets the path that was passed to the constructor.
public virtual string Name { get { return _path ?? SR.IO_UnknownFileName; } }
/// Gets a value indicating whether the stream was opened for I/O to be performed synchronously or asynchronously.
public virtual bool IsAsync
{
get { return _useAsyncIO; }
}
/// Gets the length of the stream in bytes.
public override long Length
{
get
{
if (_fileHandle.IsClosed) throw Error.GetFileNotOpen();
if (!CanSeek) throw Error.GetSeekNotSupported();
return GetLengthInternal();
}
}
///
/// Verify that the actual position of the OS's handle equals what we expect it to.
/// This will fail if someone else moved the UnixFileStream's handle or if
/// our position updating code is incorrect.
///
private void VerifyOSHandlePosition()
{
bool verifyPosition = _exposedHandle; // in release, only verify if we've given out the handle such that someone else could be manipulating it
#if DEBUG
verifyPosition = true; // in debug, always make sure our position matches what the OS says it should be
#endif
if (verifyPosition && CanSeek)
{
long oldPos = _filePosition; // SeekCore will override the current _position, so save it now
long curPos = SeekCore(0, SeekOrigin.Current);
if (oldPos != curPos)
{
// For reads, this is non-fatal but we still could have returned corrupted
// data in some cases, so discard the internal buffer. For writes,
// this is a problem; discard the buffer and error out.
_readPos = _readLength = 0;
if (_writePos > 0)
{
_writePos = 0;
throw new IOException(SR.IO_FileStreamHandlePosition);
}
}
}
}
/// Verifies that state relating to the read/write buffer is consistent.
[Conditional("DEBUG")]
private void AssertBufferInvariants()
{
// Read buffer values must be in range: 0 <= _bufferReadPos <= _bufferReadLength <= _bufferLength
Debug.Assert(0 <= _readPos && _readPos <= _readLength && _readLength <= _bufferLength);
// Write buffer values must be in range: 0 <= _bufferWritePos <= _bufferLength
Debug.Assert(0 <= _writePos && _writePos <= _bufferLength);
// Read buffering and write buffering can't both be active
Debug.Assert((_readPos == 0 && _readLength == 0) || _writePos == 0);
}
/// Validates that we're ready to read from the stream.
private void PrepareForReading()
{
if (_fileHandle.IsClosed)
throw Error.GetFileNotOpen();
if (_readLength == 0 && !CanRead)
throw Error.GetReadNotSupported();
AssertBufferInvariants();
}
/// Gets or sets the position within the current stream
public override long Position
{
get
{
if (_fileHandle.IsClosed)
throw Error.GetFileNotOpen();
if (!CanSeek)
throw Error.GetSeekNotSupported();
AssertBufferInvariants();
VerifyOSHandlePosition();
// We may have read data into our buffer from the handle, such that the handle position
// is artificially further along than the consumer's view of the stream's position.
// Thus, when reading, our position is really starting from the handle position negatively
// offset by the number of bytes in the buffer and positively offset by the number of
// bytes into that buffer we've read. When writing, both the read length and position
// must be zero, and our position is just the handle position offset positive by how many
// bytes we've written into the buffer.
return (_filePosition - _readLength) + _readPos + _writePos;
}
set
{
if (value < 0)
throw new ArgumentOutOfRangeException(nameof(value), SR.ArgumentOutOfRange_NeedNonNegNum);
Seek(value, SeekOrigin.Begin);
}
}
internal virtual bool IsClosed => _fileHandle.IsClosed;
///
/// Gets the array used for buffering reading and writing.
/// If the array hasn't been allocated, this will lazily allocate it.
///
/// The buffer.
private byte[] GetBuffer()
{
Debug.Assert(_buffer == null || _buffer.Length == _bufferLength);
if (_buffer == null)
{
_buffer = new byte[_bufferLength];
OnBufferAllocated();
}
return _buffer;
}
partial void OnBufferAllocated();
///
/// Flushes the internal read/write buffer for this stream. If write data has been buffered,
/// that data is written out to the underlying file. Or if data has been buffered for
/// reading from the stream, the data is dumped and our position in the underlying file
/// is rewound as necessary. This does not flush the OS buffer.
///
private void FlushInternalBuffer()
{
AssertBufferInvariants();
if (_writePos > 0)
{
FlushWriteBuffer();
}
else if (_readPos < _readLength && CanSeek)
{
FlushReadBuffer();
}
}
/// Dumps any read data in the buffer and rewinds our position in the stream, accordingly, as necessary.
private void FlushReadBuffer()
{
// Reading is done by blocks from the file, but someone could read
// 1 byte from the buffer then write. At that point, the OS's file
// pointer is out of sync with the stream's position. All write
// functions should call this function to preserve the position in the file.
AssertBufferInvariants();
Debug.Assert(_writePos == 0, "FileStream: Write buffer must be empty in FlushReadBuffer!");
int rewind = _readPos - _readLength;
if (rewind != 0)
{
Debug.Assert(CanSeek, "FileStream will lose buffered read data now.");
SeekCore(rewind, SeekOrigin.Current);
}
_readPos = _readLength = 0;
}
private int ReadByteCore()
{
PrepareForReading();
byte[] buffer = GetBuffer();
if (_readPos == _readLength)
{
FlushWriteBuffer();
Debug.Assert(_bufferLength > 0, "_bufferSize > 0");
_readLength = ReadNative(buffer, 0, _bufferLength);
_readPos = 0;
if (_readLength == 0)
{
return -1;
}
}
return buffer[_readPos++];
}
private void WriteByteCore(byte value)
{
PrepareForWriting();
// Flush the write buffer if it's full
if (_writePos == _bufferLength)
FlushWriteBuffer();
// We now have space in the buffer. Store the byte.
GetBuffer()[_writePos++] = value;
}
///
/// Validates that we're ready to write to the stream,
/// including flushing a read buffer if necessary.
///
private void PrepareForWriting()
{
if (_fileHandle.IsClosed)
throw Error.GetFileNotOpen();
// Make sure we're good to write. We only need to do this if there's nothing already
// in our write buffer, since if there is something in the buffer, we've already done
// this checking and flushing.
if (_writePos == 0)
{
if (!CanWrite) throw Error.GetWriteNotSupported();
FlushReadBuffer();
Debug.Assert(_bufferLength > 0, "_bufferSize > 0");
}
}
~FileStream()
{
// Preserved for compatibility since FileStream has defined a
// finalizer in past releases and derived classes may depend
// on Dispose(false) call.
Dispose(false);
}
public override IAsyncResult BeginRead(byte[] array, int offset, int numBytes, AsyncCallback callback, object state)
{
if (array == null)
throw new ArgumentNullException(nameof(array));
if (offset < 0)
throw new ArgumentOutOfRangeException(nameof(offset), SR.ArgumentOutOfRange_NeedNonNegNum);
if (numBytes < 0)
throw new ArgumentOutOfRangeException(nameof(numBytes), SR.ArgumentOutOfRange_NeedNonNegNum);
if (array.Length - offset < numBytes)
throw new ArgumentException(SR.Argument_InvalidOffLen);
if (IsClosed) throw new ObjectDisposedException(SR.ObjectDisposed_FileClosed);
if (!CanRead) throw new NotSupportedException(SR.NotSupported_UnreadableStream);
if (!IsAsync)
return base.BeginRead(array, offset, numBytes, callback, state);
else
return TaskToApm.Begin(ReadAsyncInternal(array, offset, numBytes, CancellationToken.None), callback, state);
}
public override IAsyncResult BeginWrite(byte[] array, int offset, int numBytes, AsyncCallback callback, object state)
{
if (array == null)
throw new ArgumentNullException(nameof(array));
if (offset < 0)
throw new ArgumentOutOfRangeException(nameof(offset), SR.ArgumentOutOfRange_NeedNonNegNum);
if (numBytes < 0)
throw new ArgumentOutOfRangeException(nameof(numBytes), SR.ArgumentOutOfRange_NeedNonNegNum);
if (array.Length - offset < numBytes)
throw new ArgumentException(SR.Argument_InvalidOffLen);
if (IsClosed) throw new ObjectDisposedException(SR.ObjectDisposed_FileClosed);
if (!CanWrite) throw new NotSupportedException(SR.NotSupported_UnwritableStream);
if (!IsAsync)
return base.BeginWrite(array, offset, numBytes, callback, state);
else
return TaskToApm.Begin(WriteAsyncInternal(array, offset, numBytes, CancellationToken.None), callback, state);
}
public override int EndRead(IAsyncResult asyncResult)
{
if (asyncResult == null)
throw new ArgumentNullException(nameof(asyncResult));
if (!IsAsync)
return base.EndRead(asyncResult);
else
return TaskToApm.End(asyncResult);
}
public override void EndWrite(IAsyncResult asyncResult)
{
if (asyncResult == null)
throw new ArgumentNullException(nameof(asyncResult));
if (!IsAsync)
base.EndWrite(asyncResult);
else
TaskToApm.End(asyncResult);
}
}
}