// Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System.ComponentModel; using System.IO.Strategies; using System.Runtime.Versioning; using System.Threading; using System.Threading.Tasks; using Microsoft.Win32.SafeHandles; namespace System.IO { public class FileStream : Stream { internal const int DefaultBufferSize = 4096; internal const FileShare DefaultShare = FileShare.Read; private const bool DefaultIsAsync = false; private readonly FileStreamStrategy _strategy; [EditorBrowsable(EditorBrowsableState.Never)] [Obsolete("This constructor has been deprecated. Use FileStream(SafeFileHandle handle, FileAccess access) instead.")] public FileStream(IntPtr handle, FileAccess access) : this(handle, access, true, DefaultBufferSize, DefaultIsAsync) { } [EditorBrowsable(EditorBrowsableState.Never)] [Obsolete("This constructor has been deprecated. Use FileStream(SafeFileHandle handle, FileAccess access) and optionally make a new SafeFileHandle with ownsHandle=false if needed instead.")] public FileStream(IntPtr handle, FileAccess access, bool ownsHandle) : this(handle, access, ownsHandle, DefaultBufferSize, DefaultIsAsync) { } [EditorBrowsable(EditorBrowsableState.Never)] [Obsolete("This constructor has been deprecated. Use FileStream(SafeFileHandle handle, FileAccess access, int bufferSize) and optionally make a new SafeFileHandle with ownsHandle=false if needed instead.")] public FileStream(IntPtr handle, FileAccess access, bool ownsHandle, int bufferSize) : this(handle, access, ownsHandle, bufferSize, DefaultIsAsync) { } [EditorBrowsable(EditorBrowsableState.Never)] [Obsolete("This constructor has been deprecated. Use FileStream(SafeFileHandle handle, FileAccess access, int bufferSize, bool isAsync) and optionally make a new SafeFileHandle with ownsHandle=false if needed instead.")] public FileStream(IntPtr handle, FileAccess access, bool ownsHandle, int bufferSize, bool isAsync) { SafeFileHandle safeHandle = new SafeFileHandle(handle, ownsHandle: ownsHandle); try { ValidateHandle(safeHandle, access, bufferSize); _strategy = FileStreamHelpers.ChooseStrategy(this, safeHandle, access, bufferSize, safeHandle.IsAsync); } catch { // We don't want to take ownership of closing passed in handles // *unless* the constructor completes successfully. GC.SuppressFinalize(safeHandle); // This would also prevent Close from being called, but is unnecessary // as we've removed the object from the finalizer queue. // // safeHandle.SetHandleAsInvalid(); throw; } } private static void ValidateHandle(SafeFileHandle handle, FileAccess access, int bufferSize) { if (handle.IsInvalid) { throw new ArgumentException(SR.Arg_InvalidHandle, nameof(handle)); } else if (access < FileAccess.Read || access > FileAccess.ReadWrite) { throw new ArgumentOutOfRangeException(nameof(access), SR.ArgumentOutOfRange_Enum); } else if (bufferSize < 0) { ThrowHelper.ThrowArgumentOutOfRangeException_NeedNonNegNum(nameof(bufferSize)); } else if (handle.IsClosed) { ThrowHelper.ThrowObjectDisposedException_FileClosed(); } } public FileStream(SafeFileHandle handle, FileAccess access) : this(handle, access, DefaultBufferSize) { } public FileStream(SafeFileHandle handle, FileAccess access, int bufferSize) { ValidateHandle(handle, access, bufferSize); _strategy = FileStreamHelpers.ChooseStrategy(this, handle, access, bufferSize, handle.IsAsync); } public FileStream(SafeFileHandle handle, FileAccess access, int bufferSize, bool isAsync) { ValidateHandle(handle, access, bufferSize); _strategy = FileStreamHelpers.ChooseStrategy(this, handle, access, bufferSize, handle.IsAsync); } 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) : this(path, mode, access, share, bufferSize, options, 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); } /// <summary> /// Initializes a new instance of the <see cref="FileStream" /> class with the specified path, creation mode, read/write and sharing permission, the access other FileStreams can have to the same file, the buffer size, additional file options and the allocation size. /// </summary> /// <param name="path">A relative or absolute path for the file that the current <see cref="FileStream" /> instance will encapsulate.</param> /// <param name="options">An object that describes optional <see cref="FileStream" /> parameters to use.</param> /// <exception cref="T:System.ArgumentNullException"><paramref name="path" /> or <paramref name="options" /> is <see langword="null" />.</exception> /// <exception cref="T:System.ArgumentException"><paramref name="path" /> is an empty string (""), contains only white space, or contains one or more invalid characters. /// -or- /// <paramref name="path" /> refers to a non-file device, such as <c>CON:</c>, <c>COM1:</c>, <c>LPT1:</c>, etc. in an NTFS environment.</exception> /// <exception cref="T:System.NotSupportedException"><paramref name="path" /> refers to a non-file device, such as <c>CON:</c>, <c>COM1:</c>, <c>LPT1:</c>, etc. in a non-NTFS environment.</exception> /// <exception cref="T:System.IO.FileNotFoundException">The file cannot be found, such as when <see cref="FileStreamOptions.Mode" /> is <see langword="FileMode.Truncate" /> or <see langword="FileMode.Open" />, and the file specified by <paramref name="path" /> does not exist. The file must already exist in these modes.</exception> /// <exception cref="T:System.IO.IOException">An I/O error, such as specifying <see langword="FileMode.CreateNew" /> when the file specified by <paramref name="path" /> already exists, occurred. /// -or- /// The stream has been closed. /// -or- /// The disk was full (when <see cref="FileStreamOptions.PreallocationSize" /> was provided and <paramref name="path" /> was pointing to a regular file). /// -or- /// The file was too large (when <see cref="FileStreamOptions.PreallocationSize" /> was provided and <paramref name="path" /> was pointing to a regular file).</exception> /// <exception cref="T:System.Security.SecurityException">The caller does not have the required permission.</exception> /// <exception cref="T:System.IO.DirectoryNotFoundException">The specified path is invalid, such as being on an unmapped drive.</exception> /// <exception cref="T:System.UnauthorizedAccessException">The <see cref="FileStreamOptions.Access" /> requested is not permitted by the operating system for the specified <paramref name="path" />, such as when <see cref="FileStreamOptions.Access" /> is <see cref="FileAccess.Write" /> or <see cref="FileAccess.ReadWrite" /> and the file or directory is set for read-only access. /// -or- /// <see cref="F:System.IO.FileOptions.Encrypted" /> is specified for <see cref="FileStreamOptions.Options" /> , but file encryption is not supported on the current platform.</exception> /// <exception cref="T:System.IO.PathTooLongException">The specified path, file name, or both exceed the system-defined maximum length. </exception> public FileStream(string path, FileStreamOptions options) { ArgumentException.ThrowIfNullOrEmpty(path); ArgumentNullException.ThrowIfNull(options); if ((options.Access & FileAccess.Read) != 0 && options.Mode == FileMode.Append) { throw new ArgumentException(SR.Argument_InvalidAppendMode, nameof(options)); } else if ((options.Access & FileAccess.Write) == 0) { if (options.Mode == FileMode.Truncate || options.Mode == FileMode.CreateNew || options.Mode == FileMode.Create || options.Mode == FileMode.Append) { throw new ArgumentException(SR.Format(SR.Argument_InvalidFileModeAndAccessCombo, options.Mode, options.Access), nameof(options)); } } if (options.PreallocationSize > 0) { FileStreamHelpers.ValidateArgumentsForPreallocation(options.Mode, options.Access); } if (options.UnixCreateMode.HasValue) { // Only allow UnixCreateMode for file modes that can create a new file. if (options.Mode == FileMode.Truncate || options.Mode == FileMode.Open) { throw new ArgumentException(SR.Argument_InvalidUnixCreateMode, nameof(options)); } } FileStreamHelpers.SerializationGuard(options.Access); _strategy = FileStreamHelpers.ChooseStrategy( this, path, options.Mode, options.Access, options.Share, options.BufferSize, options.Options, options.PreallocationSize, options.UnixCreateMode); } private FileStream(string path, FileMode mode, FileAccess access, FileShare share, int bufferSize, FileOptions options, long preallocationSize) { FileStreamHelpers.ValidateArguments(path, mode, access, share, bufferSize, options, preallocationSize); _strategy = FileStreamHelpers.ChooseStrategy(this, path, mode, access, share, bufferSize, options, preallocationSize, unixCreateMode: null); } [Obsolete("FileStream.Handle has been deprecated. Use FileStream's SafeFileHandle property instead.")] public virtual IntPtr Handle => _strategy.Handle; [UnsupportedOSPlatform("ios")] [UnsupportedOSPlatform("macos")] [UnsupportedOSPlatform("tvos")] [UnsupportedOSPlatform("freebsd")] public virtual void Lock(long position, long length) { if (position < 0 || length < 0) { ThrowHelper.ThrowArgumentOutOfRangeException_NeedNonNegNum(position < 0 ? nameof(position) : nameof(length)); } else if (_strategy.IsClosed) { ThrowHelper.ThrowObjectDisposedException_FileClosed(); } _strategy.Lock(position, length); } [UnsupportedOSPlatform("ios")] [UnsupportedOSPlatform("macos")] [UnsupportedOSPlatform("tvos")] [UnsupportedOSPlatform("freebsd")] public virtual void Unlock(long position, long length) { if (position < 0 || length < 0) { ThrowHelper.ThrowArgumentOutOfRangeException_NeedNonNegNum(position < 0 ? nameof(position) : nameof(length)); } else if (_strategy.IsClosed) { ThrowHelper.ThrowObjectDisposedException_FileClosed(); } _strategy.Unlock(position, length); } public override Task FlushAsync(CancellationToken cancellationToken) { if (cancellationToken.IsCancellationRequested) { return Task.FromCanceled(cancellationToken); } else if (_strategy.IsClosed) { ThrowHelper.ThrowObjectDisposedException_FileClosed(); } return _strategy.FlushAsync(cancellationToken); } public override int Read(byte[] buffer, int offset, int count) { ValidateReadWriteArgs(buffer, offset, count); return _strategy.Read(buffer, offset, count); } public override int Read(Span<byte> buffer) => _strategy.Read(buffer); public override Task<int> ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) { ValidateBufferArguments(buffer, offset, count); if (cancellationToken.IsCancellationRequested) { return Task.FromCanceled<int>(cancellationToken); } else if (!_strategy.CanRead) { if (_strategy.IsClosed) { ThrowHelper.ThrowObjectDisposedException_FileClosed(); } ThrowHelper.ThrowNotSupportedException_UnreadableStream(); } return _strategy.ReadAsync(buffer, offset, count, cancellationToken); } public override ValueTask<int> ReadAsync(Memory<byte> buffer, CancellationToken cancellationToken = default) { if (cancellationToken.IsCancellationRequested) { return ValueTask.FromCanceled<int>(cancellationToken); } else if (!_strategy.CanRead) { if (_strategy.IsClosed) { ThrowHelper.ThrowObjectDisposedException_FileClosed(); } ThrowHelper.ThrowNotSupportedException_UnreadableStream(); } return _strategy.ReadAsync(buffer, cancellationToken); } public override void Write(byte[] buffer, int offset, int count) { ValidateReadWriteArgs(buffer, offset, count); _strategy.Write(buffer, offset, count); } public override void Write(ReadOnlySpan<byte> buffer) => _strategy.Write(buffer); public override Task WriteAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) { ValidateBufferArguments(buffer, offset, count); if (cancellationToken.IsCancellationRequested) { return Task.FromCanceled(cancellationToken); } else if (!_strategy.CanWrite) { if (_strategy.IsClosed) { ThrowHelper.ThrowObjectDisposedException_FileClosed(); } ThrowHelper.ThrowNotSupportedException_UnwritableStream(); } return _strategy.WriteAsync(buffer, offset, count, cancellationToken); } public override ValueTask WriteAsync(ReadOnlyMemory<byte> buffer, CancellationToken cancellationToken = default) { if (cancellationToken.IsCancellationRequested) { return ValueTask.FromCanceled(cancellationToken); } else if (!_strategy.CanWrite) { if (_strategy.IsClosed) { ThrowHelper.ThrowObjectDisposedException_FileClosed(); } ThrowHelper.ThrowNotSupportedException_UnwritableStream(); } return _strategy.WriteAsync(buffer, cancellationToken); } /// <summary> /// Clears buffers for this stream and causes any buffered data to be written to the file. /// </summary> public override void Flush() { // Make sure that we call through the public virtual API Flush(flushToDisk: false); } /// <summary> /// Clears buffers for this stream, and if <param name="flushToDisk"/> is true, /// causes any buffered data to be written to the file. /// </summary> public virtual void Flush(bool flushToDisk) { if (_strategy.IsClosed) { ThrowHelper.ThrowObjectDisposedException_FileClosed(); } _strategy.Flush(flushToDisk); } /// <summary>Gets a value indicating whether the current stream supports reading.</summary> public override bool CanRead => _strategy.CanRead; /// <summary>Gets a value indicating whether the current stream supports writing.</summary> public override bool CanWrite => _strategy.CanWrite; /// <summary>Validates arguments to Read and Write and throws resulting exceptions.</summary> /// <param name="buffer">The buffer to read from or write to.</param> /// <param name="offset">The zero-based offset into the buffer.</param> /// <param name="count">The maximum number of bytes to read or write.</param> private void ValidateReadWriteArgs(byte[] buffer, int offset, int count) { ValidateBufferArguments(buffer, offset, count); if (_strategy.IsClosed) { ThrowHelper.ThrowObjectDisposedException_FileClosed(); } } /// <summary>Sets the length of this stream to the given value.</summary> /// <param name="value">The new length of the stream.</param> public override void SetLength(long value) { if (value < 0) { ThrowHelper.ThrowArgumentOutOfRangeException(ExceptionArgument.value, ExceptionResource.ArgumentOutOfRange_NeedNonNegNum); } else if (_strategy.IsClosed) { ThrowHelper.ThrowObjectDisposedException_FileClosed(); } else if (!CanSeek) { ThrowHelper.ThrowNotSupportedException_UnseekableStream(); } else if (!CanWrite) { ThrowHelper.ThrowNotSupportedException_UnwritableStream(); } _strategy.SetLength(value); } public virtual SafeFileHandle SafeFileHandle => _strategy.SafeFileHandle; /// <summary>Gets the path that was passed to the constructor.</summary> public virtual string Name => _strategy.Name; /// <summary>Gets a value indicating whether the stream was opened for I/O to be performed synchronously or asynchronously.</summary> public virtual bool IsAsync => _strategy.IsAsync; /// <summary>Gets the length of the stream in bytes.</summary> public override long Length { get { if (_strategy.IsClosed) { ThrowHelper.ThrowObjectDisposedException_FileClosed(); } else if (!CanSeek) { ThrowHelper.ThrowNotSupportedException_UnseekableStream(); } return _strategy.Length; } } /// <summary>Gets or sets the position within the current stream</summary> public override long Position { get { if (_strategy.IsClosed) { ThrowHelper.ThrowObjectDisposedException_FileClosed(); } else if (!CanSeek) { ThrowHelper.ThrowNotSupportedException_UnseekableStream(); } return _strategy.Position; } set { if (value < 0) { ThrowHelper.ThrowArgumentOutOfRangeException(ExceptionArgument.value, ExceptionResource.ArgumentOutOfRange_NeedNonNegNum); } else if (!CanSeek) { if (_strategy.IsClosed) { ThrowHelper.ThrowObjectDisposedException_FileClosed(); } ThrowHelper.ThrowNotSupportedException_UnseekableStream(); } _strategy.Position = value; } } /// <summary> /// Reads a byte from the file stream. Returns the byte cast to an int /// or -1 if reading from the end of the stream. /// </summary> public override int ReadByte() => _strategy.ReadByte(); /// <summary> /// Writes a byte to the current position in the stream and advances the position /// within the stream by one byte. /// </summary> /// <param name="value">The byte to write to the stream.</param> public override void WriteByte(byte value) => _strategy.WriteByte(value); // _strategy can be null only when ctor has thrown protected override void Dispose(bool disposing) => _strategy?.DisposeInternal(disposing); public override async ValueTask DisposeAsync() { await _strategy.DisposeAsync().ConfigureAwait(false); // For compatibility, derived classes must only call base.DisposeAsync(), // otherwise we would end up calling Dispose twice (one from base.DisposeAsync() and one from here). if (!_strategy.IsDerived) { Dispose(false); GC.SuppressFinalize(this); } } public override void CopyTo(Stream destination, int bufferSize) { ValidateCopyToArguments(destination, bufferSize); _strategy.CopyTo(destination, bufferSize); } /// <inheritdoc /> public override Task CopyToAsync(Stream destination, int bufferSize, CancellationToken cancellationToken) { ValidateCopyToArguments(destination, bufferSize); return _strategy.CopyToAsync(destination, bufferSize, cancellationToken); } public override IAsyncResult BeginRead(byte[] buffer, int offset, int count, AsyncCallback? callback, object? state) { ValidateBufferArguments(buffer, offset, count); if (_strategy.IsClosed) { ThrowHelper.ThrowObjectDisposedException_FileClosed(); } else if (!CanRead) { ThrowHelper.ThrowNotSupportedException_UnreadableStream(); } return _strategy.BeginRead(buffer, offset, count, callback, state); } public override int EndRead(IAsyncResult asyncResult) { ArgumentNullException.ThrowIfNull(asyncResult); return _strategy.EndRead(asyncResult); } public override IAsyncResult BeginWrite(byte[] buffer, int offset, int count, AsyncCallback? callback, object? state) { ValidateBufferArguments(buffer, offset, count); if (_strategy.IsClosed) { ThrowHelper.ThrowObjectDisposedException_FileClosed(); } else if (!CanWrite) { ThrowHelper.ThrowNotSupportedException_UnwritableStream(); } return _strategy.BeginWrite(buffer, offset, count, callback, state); } public override void EndWrite(IAsyncResult asyncResult) { ArgumentNullException.ThrowIfNull(asyncResult); _strategy.EndWrite(asyncResult); } public override bool CanSeek => _strategy.CanSeek; public override long Seek(long offset, SeekOrigin origin) { if (origin < SeekOrigin.Begin || origin > SeekOrigin.End) { throw new ArgumentException(SR.Argument_InvalidSeekOrigin, nameof(origin)); } else if (!CanSeek) { if (_strategy.IsClosed) { ThrowHelper.ThrowObjectDisposedException_FileClosed(); } ThrowHelper.ThrowNotSupportedException_UnseekableStream(); } return _strategy.Seek(offset, origin); } internal Task BaseFlushAsync(CancellationToken cancellationToken) => base.FlushAsync(cancellationToken); internal int BaseRead(Span<byte> buffer) => base.Read(buffer); internal Task<int> BaseReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) => base.ReadAsync(buffer, offset, count, cancellationToken); internal ValueTask<int> BaseReadAsync(Memory<byte> buffer, CancellationToken cancellationToken = default) => base.ReadAsync(buffer, cancellationToken); internal void BaseWrite(ReadOnlySpan<byte> buffer) => base.Write(buffer); internal Task BaseWriteAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) => base.WriteAsync(buffer, offset, count, cancellationToken); internal ValueTask BaseWriteAsync(ReadOnlyMemory<byte> buffer, CancellationToken cancellationToken = default) => base.WriteAsync(buffer, cancellationToken); internal ValueTask BaseDisposeAsync() => base.DisposeAsync(); internal Task BaseCopyToAsync(Stream destination, int bufferSize, CancellationToken cancellationToken) => base.CopyToAsync(destination, bufferSize, cancellationToken); internal IAsyncResult BaseBeginRead(byte[] buffer, int offset, int count, AsyncCallback? callback, object? state) => base.BeginRead(buffer, offset, count, callback, state); internal int BaseEndRead(IAsyncResult asyncResult) => base.EndRead(asyncResult); internal IAsyncResult BaseBeginWrite(byte[] buffer, int offset, int count, AsyncCallback? callback, object? state) => base.BeginWrite(buffer, offset, count, callback, state); internal void BaseEndWrite(IAsyncResult asyncResult) => base.EndWrite(asyncResult); } }