diff options
Diffstat (limited to 'src/mscorlib/shared/System/IO')
29 files changed, 6606 insertions, 0 deletions
diff --git a/src/mscorlib/shared/System/IO/DirectoryNotFoundException.cs b/src/mscorlib/shared/System/IO/DirectoryNotFoundException.cs new file mode 100644 index 0000000000..786c2106a3 --- /dev/null +++ b/src/mscorlib/shared/System/IO/DirectoryNotFoundException.cs @@ -0,0 +1,41 @@ +// 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.Runtime.Serialization; + +namespace System.IO +{ + /* + * Thrown when trying to access a directory that doesn't exist on disk. + * From COM Interop, this exception is thrown for 2 HRESULTS: + * the Win32 errorcode-as-HRESULT ERROR_PATH_NOT_FOUND (0x80070003) + * and STG_E_PATHNOTFOUND (0x80030003). + */ + [Serializable] + public class DirectoryNotFoundException : IOException + { + public DirectoryNotFoundException() + : base(SR.Arg_DirectoryNotFoundException) + { + HResult = __HResults.COR_E_DIRECTORYNOTFOUND; + } + + public DirectoryNotFoundException(string message) + : base(message) + { + HResult = __HResults.COR_E_DIRECTORYNOTFOUND; + } + + public DirectoryNotFoundException(string message, Exception innerException) + : base(message, innerException) + { + HResult = __HResults.COR_E_DIRECTORYNOTFOUND; + } + + protected DirectoryNotFoundException(SerializationInfo info, StreamingContext context) + : base(info, context) + { + } + } +} diff --git a/src/mscorlib/shared/System/IO/EndOfStreamException.cs b/src/mscorlib/shared/System/IO/EndOfStreamException.cs new file mode 100644 index 0000000000..7c4b2b744f --- /dev/null +++ b/src/mscorlib/shared/System/IO/EndOfStreamException.cs @@ -0,0 +1,35 @@ +// 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.Runtime.Serialization; + +namespace System.IO +{ + [Serializable] + public class EndOfStreamException : IOException + { + public EndOfStreamException() + : base(SR.Arg_EndOfStreamException) + { + HResult = __HResults.COR_E_ENDOFSTREAM; + } + + public EndOfStreamException(string message) + : base(message) + { + HResult = __HResults.COR_E_ENDOFSTREAM; + } + + public EndOfStreamException(string message, Exception innerException) + : base(message, innerException) + { + HResult = __HResults.COR_E_ENDOFSTREAM; + } + + protected EndOfStreamException(SerializationInfo info, StreamingContext context) + : base(info, context) + { + } + } +} diff --git a/src/mscorlib/shared/System/IO/Error.cs b/src/mscorlib/shared/System/IO/Error.cs new file mode 100644 index 0000000000..2aef895181 --- /dev/null +++ b/src/mscorlib/shared/System/IO/Error.cs @@ -0,0 +1,49 @@ +// 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; +using System.Runtime.InteropServices; +using System.Text; +using System.Globalization; +using System.Diagnostics.Contracts; + +namespace System.IO +{ + /// <summary> + /// Provides centralized methods for creating exceptions for System.IO.FileSystem. + /// </summary> + [Pure] + internal static class Error + { + internal static Exception GetStreamIsClosed() + { + return new ObjectDisposedException(null, SR.ObjectDisposed_StreamClosed); + } + + internal static Exception GetEndOfFile() + { + return new EndOfStreamException(SR.IO_EOF_ReadBeyondEOF); + } + + internal static Exception GetFileNotOpen() + { + return new ObjectDisposedException(null, SR.ObjectDisposed_FileClosed); + } + + internal static Exception GetReadNotSupported() + { + return new NotSupportedException(SR.NotSupported_UnreadableStream); + } + + internal static Exception GetSeekNotSupported() + { + return new NotSupportedException(SR.NotSupported_UnseekableStream); + } + + internal static Exception GetWriteNotSupported() + { + return new NotSupportedException(SR.NotSupported_UnwritableStream); + } + } +} diff --git a/src/mscorlib/shared/System/IO/FileAccess.cs b/src/mscorlib/shared/System/IO/FileAccess.cs new file mode 100644 index 0000000000..c6e583b34a --- /dev/null +++ b/src/mscorlib/shared/System/IO/FileAccess.cs @@ -0,0 +1,29 @@ +// 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; + +namespace System.IO +{ + // Contains constants for specifying the access you want for a file. + // You can have Read, Write or ReadWrite access. + // + [Serializable] + [Flags] + public enum FileAccess + { + // Specifies read access to the file. Data can be read from the file and + // the file pointer can be moved. Combine with WRITE for read-write access. + Read = 1, + + // Specifies write access to the file. Data can be written to the file and + // the file pointer can be moved. Combine with READ for read-write access. + Write = 2, + + // Specifies read and write access to the file. Data can be written to the + // file and the file pointer can be moved. Data can also be read from the + // file. + ReadWrite = 3, + } +} diff --git a/src/mscorlib/shared/System/IO/FileLoadException.cs b/src/mscorlib/shared/System/IO/FileLoadException.cs new file mode 100644 index 0000000000..b5e197c143 --- /dev/null +++ b/src/mscorlib/shared/System/IO/FileLoadException.cs @@ -0,0 +1,102 @@ +// 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.Runtime.Serialization; + +namespace System.IO +{ + [Serializable] + public partial class FileLoadException : IOException + { + public FileLoadException() + : base(SR.IO_FileLoad) + { + HResult = __HResults.COR_E_FILELOAD; + } + + public FileLoadException(string message) + : base(message) + { + HResult = __HResults.COR_E_FILELOAD; + } + + public FileLoadException(string message, Exception inner) + : base(message, inner) + { + HResult = __HResults.COR_E_FILELOAD; + } + + public FileLoadException(string message, string fileName) : base(message) + { + HResult = __HResults.COR_E_FILELOAD; + FileName = fileName; + } + + public FileLoadException(string message, string fileName, Exception inner) + : base(message, inner) + { + HResult = __HResults.COR_E_FILELOAD; + FileName = fileName; + } + + public override string Message + { + get + { + if (_message == null) + { + _message = FormatFileLoadExceptionMessage(FileName, HResult); + } + return _message; + } + } + + public string FileName { get; } + public string FusionLog { get; } + + public override string ToString() + { + string s = GetType().ToString() + ": " + Message; + + if (FileName != null && FileName.Length != 0) + s += Environment.NewLine + SR.Format(SR.IO_FileName_Name, FileName); + + if (InnerException != null) + s = s + " ---> " + InnerException.ToString(); + + if (StackTrace != null) + s += Environment.NewLine + StackTrace; + + if (FusionLog != null) + { + if (s == null) + s = " "; + s += Environment.NewLine; + s += Environment.NewLine; + s += FusionLog; + } + + return s; + } + + protected FileLoadException(SerializationInfo info, StreamingContext context) + : base(info, context) + { + // Base class constructor will check info != null. + + FileName = info.GetString("FileLoad_FileName"); + FusionLog = info.GetString("FileLoad_FusionLog"); + } + + public override void GetObjectData(SerializationInfo info, StreamingContext context) + { + // Serialize data for our base classes. base will verify info != null. + base.GetObjectData(info, context); + + // Serialize data for this class + info.AddValue("FileLoad_FileName", FileName, typeof(string)); + info.AddValue("FileLoad_FusionLog", FusionLog, typeof(string)); + } + } +} diff --git a/src/mscorlib/shared/System/IO/FileMode.cs b/src/mscorlib/shared/System/IO/FileMode.cs new file mode 100644 index 0000000000..77f2fe6f20 --- /dev/null +++ b/src/mscorlib/shared/System/IO/FileMode.cs @@ -0,0 +1,38 @@ +// 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. + +namespace System.IO +{ + // Contains constants for specifying how the OS should open a file. + // These will control whether you overwrite a file, open an existing + // file, or some combination thereof. + // + // To append to a file, use Append (which maps to OpenOrCreate then we seek + // to the end of the file). To truncate a file or create it if it doesn't + // exist, use Create. + // + public enum FileMode + { + // Creates a new file. An exception is raised if the file already exists. + CreateNew = 1, + + // Creates a new file. If the file already exists, it is overwritten. + Create = 2, + + // Opens an existing file. An exception is raised if the file does not exist. + Open = 3, + + // Opens the file if it exists. Otherwise, creates a new file. + OpenOrCreate = 4, + + // Opens an existing file. Once opened, the file is truncated so that its + // size is zero bytes. The calling process must open the file with at least + // WRITE access. An exception is raised if the file does not exist. + Truncate = 5, + + // Opens the file if it exists and seeks to the end. Otherwise, + // creates a new file. + Append = 6, + } +} diff --git a/src/mscorlib/shared/System/IO/FileNotFoundException.cs b/src/mscorlib/shared/System/IO/FileNotFoundException.cs new file mode 100644 index 0000000000..5d86b8f635 --- /dev/null +++ b/src/mscorlib/shared/System/IO/FileNotFoundException.cs @@ -0,0 +1,114 @@ +// 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.Runtime.Serialization; + +namespace System.IO +{ + // Thrown when trying to access a file that doesn't exist on disk. + [Serializable] + public partial class FileNotFoundException : IOException + { + public FileNotFoundException() + : base(SR.IO_FileNotFound) + { + HResult = __HResults.COR_E_FILENOTFOUND; + } + + public FileNotFoundException(string message) + : base(message) + { + HResult = __HResults.COR_E_FILENOTFOUND; + } + + public FileNotFoundException(string message, Exception innerException) + : base(message, innerException) + { + HResult = __HResults.COR_E_FILENOTFOUND; + } + + public FileNotFoundException(string message, string fileName) + : base(message) + { + HResult = __HResults.COR_E_FILENOTFOUND; + FileName = fileName; + } + + public FileNotFoundException(string message, string fileName, Exception innerException) + : base(message, innerException) + { + HResult = __HResults.COR_E_FILENOTFOUND; + FileName = fileName; + } + + public override string Message + { + get + { + SetMessageField(); + return _message; + } + } + + private void SetMessageField() + { + if (_message == null) + { + if ((FileName == null) && + (HResult == System.__HResults.COR_E_EXCEPTION)) + _message = SR.IO_FileNotFound; + + else if (FileName != null) + _message = FileLoadException.FormatFileLoadExceptionMessage(FileName, HResult); + } + } + + public string FileName { get; } + public string FusionLog { get; } + + public override string ToString() + { + string s = GetType().ToString() + ": " + Message; + + if (FileName != null && FileName.Length != 0) + s += Environment.NewLine + SR.Format(SR.IO_FileName_Name, FileName); + + if (InnerException != null) + s = s + " ---> " + InnerException.ToString(); + + if (StackTrace != null) + s += Environment.NewLine + StackTrace; + + if (FusionLog != null) + { + if (s == null) + s = " "; + s += Environment.NewLine; + s += Environment.NewLine; + s += FusionLog; + } + return s; + } + + protected FileNotFoundException(SerializationInfo info, StreamingContext context) + : base(info, context) + { + // Base class constructor will check info != null. + + FileName = info.GetString("FileNotFound_FileName"); + FusionLog = info.GetString("FileNotFound_FusionLog"); + } + + public override void GetObjectData(SerializationInfo info, StreamingContext context) + { + // Serialize data for our base classes. base will verify info != null. + base.GetObjectData(info, context); + + // Serialize data for this class + info.AddValue("FileNotFound_FileName", FileName, typeof(string)); + info.AddValue("FileNotFound_FusionLog", FusionLog, typeof(string)); + } + } +} + diff --git a/src/mscorlib/shared/System/IO/FileOptions.cs b/src/mscorlib/shared/System/IO/FileOptions.cs new file mode 100644 index 0000000000..ae8396a588 --- /dev/null +++ b/src/mscorlib/shared/System/IO/FileOptions.cs @@ -0,0 +1,33 @@ +// 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; +using System.Runtime.InteropServices; + +namespace System.IO +{ + // Maps to FILE_FLAG_DELETE_ON_CLOSE and similar values from winbase.h. + // We didn't expose a number of these values because we didn't believe + // a number of them made sense in managed code, at least not yet. + [Flags] + public enum FileOptions + { + // NOTE: any change to FileOptions enum needs to be + // matched in the FileStream ctor for error validation + None = 0, + WriteThrough = unchecked((int)0x80000000), + Asynchronous = unchecked((int)0x40000000), // FILE_FLAG_OVERLAPPED + // NoBuffering = 0x20000000, + RandomAccess = 0x10000000, + DeleteOnClose = 0x04000000, + SequentialScan = 0x08000000, + // AllowPosix = 0x01000000, // FILE_FLAG_POSIX_SEMANTICS + // BackupOrRestore, + // DisallowReparsePoint = 0x00200000, // FILE_FLAG_OPEN_REPARSE_POINT + // NoRemoteRecall = 0x00100000, // FILE_FLAG_OPEN_NO_RECALL + // FirstPipeInstance = 0x00080000, // FILE_FLAG_FIRST_PIPE_INSTANCE + Encrypted = 0x00004000, // FILE_ATTRIBUTE_ENCRYPTED + } +} + diff --git a/src/mscorlib/shared/System/IO/FileShare.cs b/src/mscorlib/shared/System/IO/FileShare.cs new file mode 100644 index 0000000000..e9b9b5e32f --- /dev/null +++ b/src/mscorlib/shared/System/IO/FileShare.cs @@ -0,0 +1,45 @@ +// 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; + +namespace System.IO +{ + // Contains constants for controlling file sharing options while + // opening files. You can specify what access other processes trying + // to open the same file concurrently can have. + // + // Note these values currently match the values for FILE_SHARE_READ, + // FILE_SHARE_WRITE, and FILE_SHARE_DELETE in winnt.h + // + [Flags] + public enum FileShare + { + // No sharing. Any request to open the file (by this process or another + // process) will fail until the file is closed. + None = 0, + + // Allows subsequent opening of the file for reading. If this flag is not + // specified, any request to open the file for reading (by this process or + // another process) will fail until the file is closed. + Read = 1, + + // Allows subsequent opening of the file for writing. If this flag is not + // specified, any request to open the file for writing (by this process or + // another process) will fail until the file is closed. + Write = 2, + + // Allows subsequent opening of the file for writing or reading. If this flag + // is not specified, any request to open the file for writing or reading (by + // this process or another process) will fail until the file is closed. + ReadWrite = 3, + + // Open the file, but allow someone else to delete the file. + Delete = 4, + + // Whether the file handle should be inheritable by child processes. + // Note this is not directly supported like this by Win32. + Inheritable = 0x10, + } +} diff --git a/src/mscorlib/shared/System/IO/FileStream.Linux.cs b/src/mscorlib/shared/System/IO/FileStream.Linux.cs new file mode 100644 index 0000000000..873c4eb559 --- /dev/null +++ b/src/mscorlib/shared/System/IO/FileStream.Linux.cs @@ -0,0 +1,30 @@ +// 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 Microsoft.Win32.SafeHandles; +using System.Diagnostics; +using System.Threading; +using System.Threading.Tasks; + +namespace System.IO +{ + public partial class FileStream : Stream + { + /// <summary>Prevents other processes from reading from or writing to the FileStream.</summary> + /// <param name="position">The beginning of the range to lock.</param> + /// <param name="length">The range to be locked.</param> + private void LockInternal(long position, long length) + { + CheckFileCall(Interop.Sys.LockFileRegion(_fileHandle, position, length, Interop.Sys.LockType.F_WRLCK)); + } + + /// <summary>Allows access by other processes to all or part of a file that was previously locked.</summary> + /// <param name="position">The beginning of the range to unlock.</param> + /// <param name="length">The range to be unlocked.</param> + private void UnlockInternal(long position, long length) + { + CheckFileCall(Interop.Sys.LockFileRegion(_fileHandle, position, length, Interop.Sys.LockType.F_UNLCK)); + } + } +} diff --git a/src/mscorlib/shared/System/IO/FileStream.OSX.cs b/src/mscorlib/shared/System/IO/FileStream.OSX.cs new file mode 100644 index 0000000000..f29e922337 --- /dev/null +++ b/src/mscorlib/shared/System/IO/FileStream.OSX.cs @@ -0,0 +1,19 @@ +// 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. + +namespace System.IO +{ + public partial class FileStream : Stream + { + private void LockInternal(long position, long length) + { + throw new PlatformNotSupportedException(SR.PlatformNotSupported_OSXFileLocking); + } + + private void UnlockInternal(long position, long length) + { + throw new PlatformNotSupportedException(SR.PlatformNotSupported_OSXFileLocking); + } + } +} diff --git a/src/mscorlib/shared/System/IO/FileStream.Unix.cs b/src/mscorlib/shared/System/IO/FileStream.Unix.cs new file mode 100644 index 0000000000..7d860ac2fe --- /dev/null +++ b/src/mscorlib/shared/System/IO/FileStream.Unix.cs @@ -0,0 +1,933 @@ +// 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 Microsoft.Win32.SafeHandles; +using System.Diagnostics; +using System.Threading; +using System.Threading.Tasks; + +namespace System.IO +{ + /// <summary>Provides an implementation of a file stream for Unix files.</summary> + public partial class FileStream : Stream + { + /// <summary>File mode.</summary> + private FileMode _mode; + + /// <summary>Advanced options requested when opening the file.</summary> + private FileOptions _options; + + /// <summary>If the file was opened with FileMode.Append, the length of the file when opened; otherwise, -1.</summary> + private long _appendStart = -1; + + /// <summary> + /// Extra state used by the file stream when _useAsyncIO is true. This includes + /// the semaphore used to serialize all operation, the buffer/offset/count provided by the + /// caller for ReadAsync/WriteAsync operations, and the last successful task returned + /// synchronously from ReadAsync which can be reused if the count matches the next request. + /// Only initialized when <see cref="_useAsyncIO"/> is true. + /// </summary> + private AsyncState _asyncState; + + /// <summary>Lazily-initialized value for whether the file supports seeking.</summary> + private bool? _canSeek; + + private SafeFileHandle OpenHandle(FileMode mode, FileShare share, FileOptions options) + { + // FileStream performs most of the general argument validation. We can assume here that the arguments + // are all checked and consistent (e.g. non-null-or-empty path; valid enums in mode, access, share, and options; etc.) + // Store the arguments + _mode = mode; + _options = options; + + if (_useAsyncIO) + _asyncState = new AsyncState(); + + // Translate the arguments into arguments for an open call. + Interop.Sys.OpenFlags openFlags = PreOpenConfigurationFromOptions(mode, _access, share, options); + + // If the file gets created a new, we'll select the permissions for it. Most Unix utilities by default use 666 (read and + // write for all), so we do the same (even though this doesn't match Windows, where by default it's possible to write out + // a file and then execute it). No matter what we choose, it'll be subject to the umask applied by the system, such that the + // actual permissions will typically be less than what we select here. + const Interop.Sys.Permissions OpenPermissions = + Interop.Sys.Permissions.S_IRUSR | Interop.Sys.Permissions.S_IWUSR | + Interop.Sys.Permissions.S_IRGRP | Interop.Sys.Permissions.S_IWGRP | + Interop.Sys.Permissions.S_IROTH | Interop.Sys.Permissions.S_IWOTH; + + // Open the file and store the safe handle. + return SafeFileHandle.Open(_path, openFlags, (int)OpenPermissions); + } + + /// <summary>Initializes a stream for reading or writing a Unix file.</summary> + /// <param name="mode">How the file should be opened.</param> + /// <param name="share">What other access to the file should be allowed. This is currently ignored.</param> + private void Init(FileMode mode, FileShare share) + { + _fileHandle.IsAsync = _useAsyncIO; + + // Lock the file if requested via FileShare. This is only advisory locking. FileShare.None implies an exclusive + // lock on the file and all other modes use a shared lock. While this is not as granular as Windows, not mandatory, + // and not atomic with file opening, it's better than nothing. + Interop.Sys.LockOperations lockOperation = (share == FileShare.None) ? Interop.Sys.LockOperations.LOCK_EX : Interop.Sys.LockOperations.LOCK_SH; + if (Interop.Sys.FLock(_fileHandle, lockOperation | Interop.Sys.LockOperations.LOCK_NB) < 0) + { + // The only error we care about is EWOULDBLOCK, which indicates that the file is currently locked by someone + // else and we would block trying to access it. Other errors, such as ENOTSUP (locking isn't supported) or + // EACCES (the file system doesn't allow us to lock), will only hamper FileStream's usage without providing value, + // given again that this is only advisory / best-effort. + Interop.ErrorInfo errorInfo = Interop.Sys.GetLastErrorInfo(); + if (errorInfo.Error == Interop.Error.EWOULDBLOCK) + { + throw Interop.GetExceptionForIoErrno(errorInfo, _path, isDirectory: false); + } + } + + // These provide hints around how the file will be accessed. Specifying both RandomAccess + // and Sequential together doesn't make sense as they are two competing options on the same spectrum, + // so if both are specified, we prefer RandomAccess (behavior on Windows is unspecified if both are provided). + Interop.Sys.FileAdvice fadv = + (_options & FileOptions.RandomAccess) != 0 ? Interop.Sys.FileAdvice.POSIX_FADV_RANDOM : + (_options & FileOptions.SequentialScan) != 0 ? Interop.Sys.FileAdvice.POSIX_FADV_SEQUENTIAL : + 0; + if (fadv != 0) + { + CheckFileCall(Interop.Sys.PosixFAdvise(_fileHandle, 0, 0, fadv), + ignoreNotSupported: true); // just a hint. + } + + // Jump to the end of the file if opened as Append. + if (_mode == FileMode.Append) + { + _appendStart = SeekCore(0, SeekOrigin.End); + } + } + + /// <summary>Initializes a stream from an already open file handle (file descriptor).</summary> + /// <param name="handle">The handle to the file.</param> + /// <param name="bufferSize">The size of the buffer to use when buffering.</param> + /// <param name="useAsyncIO">Whether access to the stream is performed asynchronously.</param> + private void InitFromHandle(SafeFileHandle handle) + { + if (_useAsyncIO) + _asyncState = new AsyncState(); + + if (CanSeekCore) // use non-virtual CanSeekCore rather than CanSeek to avoid making virtual call during ctor + SeekCore(0, SeekOrigin.Current); + } + + /// <summary>Translates the FileMode, FileAccess, and FileOptions values into flags to be passed when opening the file.</summary> + /// <param name="mode">The FileMode provided to the stream's constructor.</param> + /// <param name="access">The FileAccess provided to the stream's constructor</param> + /// <param name="share">The FileShare provided to the stream's constructor</param> + /// <param name="options">The FileOptions provided to the stream's constructor</param> + /// <returns>The flags value to be passed to the open system call.</returns> + private static Interop.Sys.OpenFlags PreOpenConfigurationFromOptions(FileMode mode, FileAccess access, FileShare share, FileOptions options) + { + // Translate FileMode. Most of the values map cleanly to one or more options for open. + Interop.Sys.OpenFlags flags = default(Interop.Sys.OpenFlags); + switch (mode) + { + default: + case FileMode.Open: // Open maps to the default behavior for open(...). No flags needed. + break; + + case FileMode.Append: // Append is the same as OpenOrCreate, except that we'll also separately jump to the end later + case FileMode.OpenOrCreate: + flags |= Interop.Sys.OpenFlags.O_CREAT; + break; + + case FileMode.Create: + flags |= (Interop.Sys.OpenFlags.O_CREAT | Interop.Sys.OpenFlags.O_TRUNC); + break; + + case FileMode.CreateNew: + flags |= (Interop.Sys.OpenFlags.O_CREAT | Interop.Sys.OpenFlags.O_EXCL); + break; + + case FileMode.Truncate: + flags |= Interop.Sys.OpenFlags.O_TRUNC; + break; + } + + // Translate FileAccess. All possible values map cleanly to corresponding values for open. + switch (access) + { + case FileAccess.Read: + flags |= Interop.Sys.OpenFlags.O_RDONLY; + break; + + case FileAccess.ReadWrite: + flags |= Interop.Sys.OpenFlags.O_RDWR; + break; + + case FileAccess.Write: + flags |= Interop.Sys.OpenFlags.O_WRONLY; + break; + } + + // Handle Inheritable, other FileShare flags are handled by Init + if ((share & FileShare.Inheritable) == 0) + { + flags |= Interop.Sys.OpenFlags.O_CLOEXEC; + } + + // Translate some FileOptions; some just aren't supported, and others will be handled after calling open. + // - Asynchronous: Handled in ctor, setting _useAsync and SafeFileHandle.IsAsync to true + // - DeleteOnClose: Doesn't have a Unix equivalent, but we approximate it in Dispose + // - Encrypted: No equivalent on Unix and is ignored + // - RandomAccess: Implemented after open if posix_fadvise is available + // - SequentialScan: Implemented after open if posix_fadvise is available + // - WriteThrough: Handled here + if ((options & FileOptions.WriteThrough) != 0) + { + flags |= Interop.Sys.OpenFlags.O_SYNC; + } + + return flags; + } + + /// <summary>Gets a value indicating whether the current stream supports seeking.</summary> + public override bool CanSeek => CanSeekCore; + + /// <summary>Gets a value indicating whether the current stream supports seeking.</summary> + /// <remarks>Separated out of CanSeek to enable making non-virtual call to this logic.</remarks> + private bool CanSeekCore + { + get + { + if (_fileHandle.IsClosed) + { + return false; + } + + if (!_canSeek.HasValue) + { + // Lazily-initialize whether we're able to seek, tested by seeking to our current location. + _canSeek = Interop.Sys.LSeek(_fileHandle, 0, Interop.Sys.SeekWhence.SEEK_CUR) >= 0; + } + return _canSeek.Value; + } + } + + private long GetLengthInternal() + { + // Get the length of the file as reported by the OS + Interop.Sys.FileStatus status; + CheckFileCall(Interop.Sys.FStat(_fileHandle, out status)); + long length = status.Size; + + // But we may have buffered some data to be written that puts our length + // beyond what the OS is aware of. Update accordingly. + if (_writePos > 0 && _filePosition + _writePos > length) + { + length = _writePos + _filePosition; + } + + return length; + } + + /// <summary>Releases the unmanaged resources used by the stream.</summary> + /// <param name="disposing">true to release both managed and unmanaged resources; false to release only unmanaged resources.</param> + protected override void Dispose(bool disposing) + { + try + { + if (_fileHandle != null && !_fileHandle.IsClosed) + { + // Flush any remaining data in the file + try + { + FlushWriteBuffer(); + } + catch (IOException) when (!disposing) + { + // On finalization, ignore failures from trying to flush the write buffer, + // e.g. if this stream is wrapping a pipe and the pipe is now broken. + } + + // If DeleteOnClose was requested when constructed, delete the file now. + // (Unix doesn't directly support DeleteOnClose, so we mimic it here.) + if (_path != null && (_options & FileOptions.DeleteOnClose) != 0) + { + // Since we still have the file open, this will end up deleting + // it (assuming we're the only link to it) once it's closed, but the + // name will be removed immediately. + Interop.Sys.Unlink(_path); // ignore errors; it's valid that the path may no longer exist + } + } + } + finally + { + if (_fileHandle != null && !_fileHandle.IsClosed) + { + _fileHandle.Dispose(); + } + base.Dispose(disposing); + } + } + + /// <summary>Flushes the OS buffer. This does not flush the internal read/write buffer.</summary> + private void FlushOSBuffer() + { + if (Interop.Sys.FSync(_fileHandle) < 0) + { + Interop.ErrorInfo errorInfo = Interop.Sys.GetLastErrorInfo(); + switch (errorInfo.Error) + { + case Interop.Error.EROFS: + case Interop.Error.EINVAL: + case Interop.Error.ENOTSUP: + // Ignore failures due to the FileStream being bound to a special file that + // doesn't support synchronization. In such cases there's nothing to flush. + break; + default: + throw Interop.GetExceptionForIoErrno(errorInfo, _path, isDirectory: false); + } + } + } + + /// <summary>Writes any data in the write buffer to the underlying stream and resets the buffer.</summary> + private void FlushWriteBuffer() + { + AssertBufferInvariants(); + if (_writePos > 0) + { + WriteNative(GetBuffer(), 0, _writePos); + _writePos = 0; + } + } + + /// <summary>Asynchronously clears all buffers for this stream, causing any buffered data to be written to the underlying device.</summary> + /// <param name="cancellationToken">The token to monitor for cancellation requests.</param> + /// <returns>A task that represents the asynchronous flush operation.</returns> + private Task FlushAsyncInternal(CancellationToken cancellationToken) + { + if (cancellationToken.IsCancellationRequested) + { + return Task.FromCanceled(cancellationToken); + } + if (_fileHandle.IsClosed) + { + throw Error.GetFileNotOpen(); + } + + // As with Win32FileStream, flush the buffers synchronously to avoid race conditions. + try + { + FlushInternalBuffer(); + } + catch (Exception e) + { + return Task.FromException(e); + } + + // We then separately flush to disk asynchronously. This is only + // necessary if we support writing; otherwise, we're done. + if (CanWrite) + { + return Task.Factory.StartNew( + state => ((FileStream)state).FlushOSBuffer(), + this, + cancellationToken, + TaskCreationOptions.DenyChildAttach, + TaskScheduler.Default); + } + else + { + return Task.CompletedTask; + } + } + + /// <summary>Sets the length of this stream to the given value.</summary> + /// <param name="value">The new length of the stream.</param> + private void SetLengthInternal(long value) + { + FlushInternalBuffer(); + + if (_appendStart != -1 && value < _appendStart) + { + throw new IOException(SR.IO_SetLengthAppendTruncate); + } + + long origPos = _filePosition; + + VerifyOSHandlePosition(); + + if (_filePosition != value) + { + SeekCore(value, SeekOrigin.Begin); + } + + CheckFileCall(Interop.Sys.FTruncate(_fileHandle, value)); + + // Return file pointer to where it was before setting length + if (origPos != value) + { + if (origPos < value) + { + SeekCore(origPos, SeekOrigin.Begin); + } + else + { + SeekCore(0, SeekOrigin.End); + } + } + } + + /// <summary>Reads a block of bytes from the stream and writes the data in a given buffer.</summary> + /// <param name="array"> + /// When this method returns, contains the specified byte array with the values between offset and + /// (offset + count - 1) replaced by the bytes read from the current source. + /// </param> + /// <param name="offset">The byte offset in array at which the read bytes will be placed.</param> + /// <param name="count">The maximum number of bytes to read. </param> + /// <returns> + /// The total number of bytes read into the buffer. This might be less than the number of bytes requested + /// if that number of bytes are not currently available, or zero if the end of the stream is reached. + /// </returns> + public override int Read(byte[] array, int offset, int count) + { + ValidateReadWriteArgs(array, offset, count); + + if (_useAsyncIO) + { + _asyncState.Wait(); + try { return ReadCore(array, offset, count); } + finally { _asyncState.Release(); } + } + else + { + return ReadCore(array, offset, count); + } + } + + /// <summary>Reads a block of bytes from the stream and writes the data in a given buffer.</summary> + /// <param name="array"> + /// When this method returns, contains the specified byte array with the values between offset and + /// (offset + count - 1) replaced by the bytes read from the current source. + /// </param> + /// <param name="offset">The byte offset in array at which the read bytes will be placed.</param> + /// <param name="count">The maximum number of bytes to read. </param> + /// <returns> + /// The total number of bytes read into the buffer. This might be less than the number of bytes requested + /// if that number of bytes are not currently available, or zero if the end of the stream is reached. + /// </returns> + private int ReadCore(byte[] array, int offset, int count) + { + PrepareForReading(); + + // Are there any bytes available in the read buffer? If yes, + // we can just return from the buffer. If the buffer is empty + // or has no more available data in it, we can either refill it + // (and then read from the buffer into the user's buffer) or + // we can just go directly into the user's buffer, if they asked + // for more data than we'd otherwise buffer. + int numBytesAvailable = _readLength - _readPos; + bool readFromOS = false; + if (numBytesAvailable == 0) + { + // If we're not able to seek, then we're not able to rewind the stream (i.e. flushing + // a read buffer), in which case we don't want to use a read buffer. Similarly, if + // the user has asked for more data than we can buffer, we also want to skip the buffer. + if (!CanSeek || (count >= _bufferLength)) + { + // Read directly into the user's buffer + _readPos = _readLength = 0; + return ReadNative(array, offset, count); + } + else + { + // Read into our buffer. + _readLength = numBytesAvailable = ReadNative(GetBuffer(), 0, _bufferLength); + _readPos = 0; + if (numBytesAvailable == 0) + { + return 0; + } + + // Note that we did an OS read as part of this Read, so that later + // we don't try to do one again if what's in the buffer doesn't + // meet the user's request. + readFromOS = true; + } + } + + // Now that we know there's data in the buffer, read from it into the user's buffer. + Debug.Assert(numBytesAvailable > 0, "Data must be in the buffer to be here"); + int bytesRead = Math.Min(numBytesAvailable, count); + Buffer.BlockCopy(GetBuffer(), _readPos, array, offset, bytesRead); + _readPos += bytesRead; + + // We may not have had enough data in the buffer to completely satisfy the user's request. + // While Read doesn't require that we return as much data as the user requested (any amount + // up to the requested count is fine), FileStream on Windows tries to do so by doing a + // subsequent read from the file if we tried to satisfy the request with what was in the + // buffer but the buffer contained less than the requested count. To be consistent with that + // behavior, we do the same thing here on Unix. Note that we may still get less the requested + // amount, as the OS may give us back fewer than we request, either due to reaching the end of + // file, or due to its own whims. + if (!readFromOS && bytesRead < count) + { + Debug.Assert(_readPos == _readLength, "bytesToRead should only be < count if numBytesAvailable < count"); + _readPos = _readLength = 0; // no data left in the read buffer + bytesRead += ReadNative(array, offset + bytesRead, count - bytesRead); + } + + return bytesRead; + } + + /// <summary>Unbuffered, reads a block of bytes from the stream and writes the data in a given buffer.</summary> + /// <param name="array"> + /// When this method returns, contains the specified byte array with the values between offset and + /// (offset + count - 1) replaced by the bytes read from the current source. + /// </param> + /// <param name="offset">The byte offset in array at which the read bytes will be placed.</param> + /// <param name="count">The maximum number of bytes to read. </param> + /// <returns> + /// The total number of bytes read into the buffer. This might be less than the number of bytes requested + /// if that number of bytes are not currently available, or zero if the end of the stream is reached. + /// </returns> + private unsafe int ReadNative(byte[] array, int offset, int count) + { + FlushWriteBuffer(); // we're about to read; dump the write buffer + + VerifyOSHandlePosition(); + + int bytesRead; + fixed (byte* bufPtr = array) + { + bytesRead = CheckFileCall(Interop.Sys.Read(_fileHandle, bufPtr + offset, count)); + Debug.Assert(bytesRead <= count); + } + _filePosition += bytesRead; + return bytesRead; + } + + /// <summary> + /// Asynchronously reads a sequence of bytes from the current stream and advances + /// the position within the stream by the number of bytes read. + /// </summary> + /// <param name="buffer">The buffer to write the data into.</param> + /// <param name="offset">The byte offset in buffer at which to begin writing data from the stream.</param> + /// <param name="count">The maximum number of bytes to read.</param> + /// <param name="cancellationToken">The token to monitor for cancellation requests.</param> + /// <returns>A task that represents the asynchronous read operation.</returns> + private Task<int> ReadAsyncInternal(byte[] buffer, int offset, int count, CancellationToken cancellationToken) + { + if (_useAsyncIO) + { + if (!CanRead) // match Windows behavior; this gets thrown synchronously + { + throw Error.GetReadNotSupported(); + } + + // Serialize operations using the semaphore. + Task waitTask = _asyncState.WaitAsync(); + + // If we got ownership immediately, and if there's enough data in our buffer + // to satisfy the full request of the caller, hand back the buffered data. + // While it would be a legal implementation of the Read contract, we don't + // hand back here less than the amount requested so as to match the behavior + // in ReadCore that will make a native call to try to fulfill the remainder + // of the request. + if (waitTask.Status == TaskStatus.RanToCompletion) + { + int numBytesAvailable = _readLength - _readPos; + if (numBytesAvailable >= count) + { + try + { + PrepareForReading(); + + Buffer.BlockCopy(GetBuffer(), _readPos, buffer, offset, count); + _readPos += count; + + return _asyncState._lastSuccessfulReadTask != null && _asyncState._lastSuccessfulReadTask.Result == count ? + _asyncState._lastSuccessfulReadTask : + (_asyncState._lastSuccessfulReadTask = Task.FromResult(count)); + } + catch (Exception exc) + { + return Task.FromException<int>(exc); + } + finally + { + _asyncState.Release(); + } + } + } + + // Otherwise, issue the whole request asynchronously. + _asyncState.Update(buffer, offset, count); + return waitTask.ContinueWith((t, s) => + { + // The options available on Unix for writing asynchronously to an arbitrary file + // handle typically amount to just using another thread to do the synchronous write, + // which is exactly what this implementation does. This does mean there are subtle + // differences in certain FileStream behaviors between Windows and Unix when multiple + // asynchronous operations are issued against the stream to execute concurrently; on + // Unix the operations will be serialized due to the usage of a semaphore, but the + // position /length information won't be updated until after the write has completed, + // whereas on Windows it may happen before the write has completed. + + Debug.Assert(t.Status == TaskStatus.RanToCompletion); + var thisRef = (FileStream)s; + try + { + byte[] b = thisRef._asyncState._buffer; + thisRef._asyncState._buffer = null; // remove reference to user's buffer + return thisRef.ReadCore(b, thisRef._asyncState._offset, thisRef._asyncState._count); + } + finally { thisRef._asyncState.Release(); } + }, this, CancellationToken.None, TaskContinuationOptions.DenyChildAttach, TaskScheduler.Default); + } + else + { + return base.ReadAsync(buffer, offset, count, cancellationToken); + } + } + + /// <summary> + /// Reads a byte from the stream and advances the position within the stream + /// by one byte, or returns -1 if at the end of the stream. + /// </summary> + /// <returns>The unsigned byte cast to an Int32, or -1 if at the end of the stream.</returns> + public override int ReadByte() + { + if (_useAsyncIO) + { + _asyncState.Wait(); + try { return ReadByteCore(); } + finally { _asyncState.Release(); } + } + else + { + return ReadByteCore(); + } + } + + /// <summary>Writes a block of bytes to the file stream.</summary> + /// <param name="array">The buffer containing data to write to the stream.</param> + /// <param name="offset">The zero-based byte offset in array from which to begin copying bytes to the stream.</param> + /// <param name="count">The maximum number of bytes to write.</param> + public override void Write(byte[] array, int offset, int count) + { + ValidateReadWriteArgs(array, offset, count); + + if (_useAsyncIO) + { + _asyncState.Wait(); + try { WriteCore(array, offset, count); } + finally { _asyncState.Release(); } + } + else + { + WriteCore(array, offset, count); + } + } + + /// <summary>Writes a block of bytes to the file stream.</summary> + /// <param name="array">The buffer containing data to write to the stream.</param> + /// <param name="offset">The zero-based byte offset in array from which to begin copying bytes to the stream.</param> + /// <param name="count">The maximum number of bytes to write.</param> + private void WriteCore(byte[] array, int offset, int count) + { + PrepareForWriting(); + + // If no data is being written, nothing more to do. + if (count == 0) + { + return; + } + + // If there's already data in our write buffer, then we need to go through + // our buffer to ensure data isn't corrupted. + if (_writePos > 0) + { + // If there's space remaining in the buffer, then copy as much as + // we can from the user's buffer into ours. + int spaceRemaining = _bufferLength - _writePos; + if (spaceRemaining > 0) + { + int bytesToCopy = Math.Min(spaceRemaining, count); + Buffer.BlockCopy(array, offset, GetBuffer(), _writePos, bytesToCopy); + _writePos += bytesToCopy; + + // If we've successfully copied all of the user's data, we're done. + if (count == bytesToCopy) + { + return; + } + + // Otherwise, keep track of how much more data needs to be handled. + offset += bytesToCopy; + count -= bytesToCopy; + } + + // At this point, the buffer is full, so flush it out. + FlushWriteBuffer(); + } + + // Our buffer is now empty. If using the buffer would slow things down (because + // the user's looking to write more data than we can store in the buffer), + // skip the buffer. Otherwise, put the remaining data into the buffer. + Debug.Assert(_writePos == 0); + if (count >= _bufferLength) + { + WriteNative(array, offset, count); + } + else + { + Buffer.BlockCopy(array, offset, GetBuffer(), _writePos, count); + _writePos = count; + } + } + + /// <summary>Unbuffered, writes a block of bytes to the file stream.</summary> + /// <param name="array">The buffer containing data to write to the stream.</param> + /// <param name="offset">The zero-based byte offset in array from which to begin copying bytes to the stream.</param> + /// <param name="count">The maximum number of bytes to write.</param> + private unsafe void WriteNative(byte[] array, int offset, int count) + { + VerifyOSHandlePosition(); + + fixed (byte* bufPtr = array) + { + while (count > 0) + { + int bytesWritten = CheckFileCall(Interop.Sys.Write(_fileHandle, bufPtr + offset, count)); + Debug.Assert(bytesWritten <= count); + + _filePosition += bytesWritten; + count -= bytesWritten; + offset += bytesWritten; + } + } + } + + /// <summary> + /// Asynchronously writes a sequence of bytes to the current stream, advances + /// the current position within this stream by the number of bytes written, and + /// monitors cancellation requests. + /// </summary> + /// <param name="buffer">The buffer to write data from.</param> + /// <param name="offset">The zero-based byte offset in buffer from which to begin copying bytes to the stream.</param> + /// <param name="count">The maximum number of bytes to write.</param> + /// <param name="cancellationToken">The token to monitor for cancellation requests.</param> + /// <returns>A task that represents the asynchronous write operation.</returns> + private Task WriteAsyncInternal(byte[] buffer, int offset, int count, CancellationToken cancellationToken) + { + if (cancellationToken.IsCancellationRequested) + return Task.FromCanceled(cancellationToken); + + if (_fileHandle.IsClosed) + throw Error.GetFileNotOpen(); + + if (_useAsyncIO) + { + if (!CanWrite) // match Windows behavior; this gets thrown synchronously + { + throw Error.GetWriteNotSupported(); + } + + // Serialize operations using the semaphore. + Task waitTask = _asyncState.WaitAsync(); + + // If we got ownership immediately, and if there's enough space in our buffer + // to buffer the entire write request, then do so and we're done. + if (waitTask.Status == TaskStatus.RanToCompletion) + { + int spaceRemaining = _bufferLength - _writePos; + if (spaceRemaining >= count) + { + try + { + PrepareForWriting(); + + Buffer.BlockCopy(buffer, offset, GetBuffer(), _writePos, count); + _writePos += count; + + return Task.CompletedTask; + } + catch (Exception exc) + { + return Task.FromException(exc); + } + finally + { + _asyncState.Release(); + } + } + } + + // Otherwise, issue the whole request asynchronously. + _asyncState.Update(buffer, offset, count); + return waitTask.ContinueWith((t, s) => + { + // The options available on Unix for writing asynchronously to an arbitrary file + // handle typically amount to just using another thread to do the synchronous write, + // which is exactly what this implementation does. This does mean there are subtle + // differences in certain FileStream behaviors between Windows and Unix when multiple + // asynchronous operations are issued against the stream to execute concurrently; on + // Unix the operations will be serialized due to the usage of a semaphore, but the + // position /length information won't be updated until after the write has completed, + // whereas on Windows it may happen before the write has completed. + + Debug.Assert(t.Status == TaskStatus.RanToCompletion); + var thisRef = (FileStream)s; + try + { + byte[] b = thisRef._asyncState._buffer; + thisRef._asyncState._buffer = null; // remove reference to user's buffer + thisRef.WriteCore(b, thisRef._asyncState._offset, thisRef._asyncState._count); + } + finally { thisRef._asyncState.Release(); } + }, this, CancellationToken.None, TaskContinuationOptions.DenyChildAttach, TaskScheduler.Default); + } + else + { + return base.WriteAsync(buffer, offset, count, cancellationToken); + } + } + + /// <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) // avoids an array allocation in the base implementation + { + if (_useAsyncIO) + { + _asyncState.Wait(); + try { WriteByteCore(value); } + finally { _asyncState.Release(); } + } + else + { + WriteByteCore(value); + } + } + + /// <summary>Sets the current position of this stream to the given value.</summary> + /// <param name="offset">The point relative to origin from which to begin seeking. </param> + /// <param name="origin"> + /// Specifies the beginning, the end, or the current position as a reference + /// point for offset, using a value of type SeekOrigin. + /// </param> + /// <returns>The new position in the stream.</returns> + public override long Seek(long offset, SeekOrigin origin) + { + if (origin < SeekOrigin.Begin || origin > SeekOrigin.End) + { + throw new ArgumentException(SR.Argument_InvalidSeekOrigin, nameof(origin)); + } + if (_fileHandle.IsClosed) + { + throw Error.GetFileNotOpen(); + } + if (!CanSeek) + { + throw Error.GetSeekNotSupported(); + } + + VerifyOSHandlePosition(); + + // Flush our write/read buffer. FlushWrite will output any write buffer we have and reset _bufferWritePos. + // We don't call FlushRead, as that will do an unnecessary seek to rewind the read buffer, and since we're + // about to seek and update our position, we can simply update the offset as necessary and reset our read + // position and length to 0. (In the future, for some simple cases we could potentially add an optimization + // here to just move data around in the buffer for short jumps, to avoid re-reading the data from disk.) + FlushWriteBuffer(); + if (origin == SeekOrigin.Current) + { + offset -= (_readLength - _readPos); + } + _readPos = _readLength = 0; + + // Keep track of where we were, in case we're in append mode and need to verify + long oldPos = 0; + if (_appendStart >= 0) + { + oldPos = SeekCore(0, SeekOrigin.Current); + } + + // Jump to the new location + long pos = SeekCore(offset, origin); + + // Prevent users from overwriting data in a file that was opened in append mode. + if (_appendStart != -1 && pos < _appendStart) + { + SeekCore(oldPos, SeekOrigin.Begin); + throw new IOException(SR.IO_SeekAppendOverwrite); + } + + // Return the new position + return pos; + } + + /// <summary>Sets the current position of this stream to the given value.</summary> + /// <param name="offset">The point relative to origin from which to begin seeking. </param> + /// <param name="origin"> + /// Specifies the beginning, the end, or the current position as a reference + /// point for offset, using a value of type SeekOrigin. + /// </param> + /// <returns>The new position in the stream.</returns> + private long SeekCore(long offset, SeekOrigin origin) + { + Debug.Assert(!_fileHandle.IsClosed && (GetType() != typeof(FileStream) || CanSeek)); // verify that we can seek, but only if CanSeek won't be a virtual call (which could happen in the ctor) + Debug.Assert(origin >= SeekOrigin.Begin && origin <= SeekOrigin.End); + + long pos = CheckFileCall(Interop.Sys.LSeek(_fileHandle, offset, (Interop.Sys.SeekWhence)(int)origin)); // SeekOrigin values are the same as Interop.libc.SeekWhence values + _filePosition = pos; + return pos; + } + + private long CheckFileCall(long result, bool ignoreNotSupported = false) + { + if (result < 0) + { + Interop.ErrorInfo errorInfo = Interop.Sys.GetLastErrorInfo(); + if (!(ignoreNotSupported && errorInfo.Error == Interop.Error.ENOTSUP)) + { + throw Interop.GetExceptionForIoErrno(errorInfo, _path, isDirectory: false); + } + } + + return result; + } + + private int CheckFileCall(int result, bool ignoreNotSupported = false) + { + CheckFileCall((long)result, ignoreNotSupported); + + return result; + } + + /// <summary>State used when the stream is in async mode.</summary> + private sealed class AsyncState : SemaphoreSlim + { + /// <summary>The caller's buffer currently being used by the active async operation.</summary> + internal byte[] _buffer; + /// <summary>The caller's offset currently being used by the active async operation.</summary> + internal int _offset; + /// <summary>The caller's count currently being used by the active async operation.</summary> + internal int _count; + /// <summary>The last task successfully, synchronously returned task from ReadAsync.</summary> + internal Task<int> _lastSuccessfulReadTask; + + /// <summary>Initialize the AsyncState.</summary> + internal AsyncState() : base(initialCount: 1, maxCount: 1) { } + + /// <summary>Sets the active buffer, offset, and count.</summary> + internal void Update(byte[] buffer, int offset, int count) + { + _buffer = buffer; + _offset = offset; + _count = count; + } + } + } +} diff --git a/src/mscorlib/shared/System/IO/FileStream.Win32.cs b/src/mscorlib/shared/System/IO/FileStream.Win32.cs new file mode 100644 index 0000000000..0045ebeaf8 --- /dev/null +++ b/src/mscorlib/shared/System/IO/FileStream.Win32.cs @@ -0,0 +1,77 @@ +// 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.Buffers; +using System.Diagnostics; +using System.Runtime.InteropServices; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Win32.SafeHandles; +using System.Runtime.CompilerServices; + +namespace System.IO +{ + public partial class FileStream : Stream + { + private SafeFileHandle OpenHandle(FileMode mode, FileShare share, FileOptions options) + { + Interop.Kernel32.SECURITY_ATTRIBUTES secAttrs = GetSecAttrs(share); + + int fAccess = + ((_access & FileAccess.Read) == FileAccess.Read ? GENERIC_READ : 0) | + ((_access & FileAccess.Write) == FileAccess.Write ? GENERIC_WRITE : 0); + + // Our Inheritable bit was stolen from Windows, but should be set in + // the security attributes class. Don't leave this bit set. + share &= ~FileShare.Inheritable; + + // Must use a valid Win32 constant here... + if (mode == FileMode.Append) + mode = FileMode.OpenOrCreate; + + int flagsAndAttributes = (int)options; + + // For mitigating local elevation of privilege attack through named pipes + // make sure we always call CreateFile with SECURITY_ANONYMOUS so that the + // named pipe server can't impersonate a high privileged client security context + flagsAndAttributes |= (Interop.Kernel32.SecurityOptions.SECURITY_SQOS_PRESENT | Interop.Kernel32.SecurityOptions.SECURITY_ANONYMOUS); + + // Don't pop up a dialog for reading from an empty floppy drive + uint oldMode = Interop.Kernel32.SetErrorMode(Interop.Kernel32.SEM_FAILCRITICALERRORS); + try + { + SafeFileHandle fileHandle = Interop.Kernel32.CreateFile(_path, fAccess, share, ref secAttrs, mode, flagsAndAttributes, IntPtr.Zero); + fileHandle.IsAsync = _useAsyncIO; + + if (fileHandle.IsInvalid) + { + // Return a meaningful exception with the full path. + + // NT5 oddity - when trying to open "C:\" as a Win32FileStream, + // we usually get ERROR_PATH_NOT_FOUND from the OS. We should + // probably be consistent w/ every other directory. + int errorCode = Marshal.GetLastWin32Error(); + + if (errorCode == Interop.Errors.ERROR_PATH_NOT_FOUND && _path.Length == PathInternal.GetRootLength(_path)) + errorCode = Interop.Errors.ERROR_ACCESS_DENIED; + + throw Win32Marshal.GetExceptionForWin32Error(errorCode, _path); + } + + int fileType = Interop.Kernel32.GetFileType(fileHandle); + if (fileType != Interop.Kernel32.FileTypes.FILE_TYPE_DISK) + { + fileHandle.Dispose(); + throw new NotSupportedException(SR.NotSupported_FileStreamOnNonFiles); + } + + return fileHandle; + } + finally + { + Interop.Kernel32.SetErrorMode(oldMode); + } + } + } +} diff --git a/src/mscorlib/shared/System/IO/FileStream.WinRT.cs b/src/mscorlib/shared/System/IO/FileStream.WinRT.cs new file mode 100644 index 0000000000..062b160b57 --- /dev/null +++ b/src/mscorlib/shared/System/IO/FileStream.WinRT.cs @@ -0,0 +1,78 @@ +// 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 Microsoft.Win32.SafeHandles; +using System.Runtime.InteropServices; + +namespace System.IO +{ + public partial class FileStream : Stream + { + private unsafe SafeFileHandle OpenHandle(FileMode mode, FileShare share, FileOptions options) + { + Interop.Kernel32.SECURITY_ATTRIBUTES secAttrs = GetSecAttrs(share); + + int fAccess = + ((_access & FileAccess.Read) == FileAccess.Read ? GENERIC_READ : 0) | + ((_access & FileAccess.Write) == FileAccess.Write ? GENERIC_WRITE : 0); + + // Our Inheritable bit was stolen from Windows, but should be set in + // the security attributes class. Don't leave this bit set. + share &= ~FileShare.Inheritable; + + // Must use a valid Win32 constant here... + if (mode == FileMode.Append) + mode = FileMode.OpenOrCreate; + + Interop.Kernel32.CREATEFILE2_EXTENDED_PARAMETERS parameters = new Interop.Kernel32.CREATEFILE2_EXTENDED_PARAMETERS(); + parameters.dwSize = (uint)sizeof(Interop.Kernel32.CREATEFILE2_EXTENDED_PARAMETERS); + parameters.dwFileFlags = (uint)options; + parameters.lpSecurityAttributes = &secAttrs; + + SafeFileHandle fileHandle = Interop.Kernel32.CreateFile2( + lpFileName: _path, + dwDesiredAccess: fAccess, + dwShareMode: share, + dwCreationDisposition: mode, + pCreateExParams: ¶meters); + + fileHandle.IsAsync = _useAsyncIO; + + if (fileHandle.IsInvalid) + { + // Return a meaningful exception with the full path. + + // NT5 oddity - when trying to open "C:\" as a Win32FileStream, + // we usually get ERROR_PATH_NOT_FOUND from the OS. We should + // probably be consistent w/ every other directory. + int errorCode = Marshal.GetLastWin32Error(); + + if (errorCode == Interop.Errors.ERROR_PATH_NOT_FOUND && _path.Length == PathInternal.GetRootLength(_path)) + errorCode = Interop.Errors.ERROR_ACCESS_DENIED; + + throw Win32Marshal.GetExceptionForWin32Error(errorCode, $"{_path} {options} {fAccess} {share} {mode}"); + } + + return fileHandle; + } + +#if PROJECTN + // TODO: These internal methods should be removed once we start consuming updated CoreFX builds + public static FileStream InternalOpen(string path, int bufferSize = 4096, bool useAsync = true) + { + return new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.Read, bufferSize, useAsync); + } + + public static FileStream InternalCreate(string path, int bufferSize = 4096, bool useAsync = true) + { + return new FileStream(path, FileMode.Create, FileAccess.Write, FileShare.Read, bufferSize, useAsync); + } + + public static FileStream InternalAppend(string path, int bufferSize = 4096, bool useAsync = true) + { + return new FileStream(path, FileMode.Append, FileAccess.Write, FileShare.Read, bufferSize, useAsync); + } +#endif + } +} diff --git a/src/mscorlib/shared/System/IO/FileStream.Windows.cs b/src/mscorlib/shared/System/IO/FileStream.Windows.cs new file mode 100644 index 0000000000..7c09ae1a1c --- /dev/null +++ b/src/mscorlib/shared/System/IO/FileStream.Windows.cs @@ -0,0 +1,1717 @@ +// 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.Buffers; +using System.Diagnostics; +using System.Runtime.InteropServices; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Win32.SafeHandles; +using System.Runtime.CompilerServices; + +/* + * Win32FileStream supports different modes of accessing the disk - async mode + * and sync mode. They are two completely different codepaths in the + * sync & async methods (i.e. Read/Write vs. ReadAsync/WriteAsync). File + * handles in NT can be opened in only sync or overlapped (async) mode, + * and we have to deal with this pain. Stream has implementations of + * the sync methods in terms of the async ones, so we'll + * call through to our base class to get those methods when necessary. + * + * Also buffering is added into Win32FileStream as well. Folded in the + * code from BufferedStream, so all the comments about it being mostly + * aggressive (and the possible perf improvement) apply to Win32FileStream as + * well. Also added some buffering to the async code paths. + * + * Class Invariants: + * The class has one buffer, shared for reading & writing. It can only be + * used for one or the other at any point in time - not both. The following + * should be true: + * 0 <= _readPos <= _readLen < _bufferSize + * 0 <= _writePos < _bufferSize + * _readPos == _readLen && _readPos > 0 implies the read buffer is valid, + * but we're at the end of the buffer. + * _readPos == _readLen == 0 means the read buffer contains garbage. + * Either _writePos can be greater than 0, or _readLen & _readPos can be + * greater than zero, but neither can be greater than zero at the same time. + * + */ + +namespace System.IO +{ + public partial class FileStream : Stream + { + private bool _canSeek; + private bool _isPipe; // Whether to disable async buffering code. + private long _appendStart; // When appending, prevent overwriting file. + + private static unsafe IOCompletionCallback s_ioCallback = FileStreamCompletionSource.IOCallback; + + private Task<int> _lastSynchronouslyCompletedTask = null; // cached task for read ops that complete synchronously + private Task _activeBufferOperation = null; // tracks in-progress async ops using the buffer + private PreAllocatedOverlapped _preallocatedOverlapped; // optimization for async ops to avoid per-op allocations + private FileStreamCompletionSource _currentOverlappedOwner; // async op currently using the preallocated overlapped + + private void Init(FileMode mode, FileShare share) + { + // Disallow access to all non-file devices from the Win32FileStream + // constructors that take a String. Everyone else can call + // CreateFile themselves then use the constructor that takes an + // IntPtr. Disallows "con:", "com1:", "lpt1:", etc. + int fileType = Interop.Kernel32.GetFileType(_fileHandle); + if (fileType != Interop.Kernel32.FileTypes.FILE_TYPE_DISK) + { + _fileHandle.Dispose(); + throw new NotSupportedException(SR.NotSupported_FileStreamOnNonFiles); + } + + // This is necessary for async IO using IO Completion ports via our + // managed Threadpool API's. This (theoretically) calls the OS's + // BindIoCompletionCallback method, and passes in a stub for the + // LPOVERLAPPED_COMPLETION_ROUTINE. This stub looks at the Overlapped + // struct for this request and gets a delegate to a managed callback + // from there, which it then calls on a threadpool thread. (We allocate + // our native OVERLAPPED structs 2 pointers too large and store EE state + // & GC handles there, one to an IAsyncResult, the other to a delegate.) + if (_useAsyncIO) + { + try + { + _fileHandle.ThreadPoolBinding = ThreadPoolBoundHandle.BindHandle(_fileHandle); + } + catch (ArgumentException ex) + { + throw new IOException(SR.IO_BindHandleFailed, ex); + } + finally + { + if (_fileHandle.ThreadPoolBinding == null) + { + // We should close the handle so that the handle is not open until SafeFileHandle GC + Debug.Assert(!_exposedHandle, "Are we closing handle that we exposed/not own, how?"); + _fileHandle.Dispose(); + } + } + } + + _canSeek = true; + + // For Append mode... + if (mode == FileMode.Append) + { + _appendStart = SeekCore(0, SeekOrigin.End); + } + else + { + _appendStart = -1; + } + } + + private void InitFromHandle(SafeFileHandle handle) + { + int handleType = Interop.Kernel32.GetFileType(_fileHandle); + Debug.Assert(handleType == Interop.Kernel32.FileTypes.FILE_TYPE_DISK || handleType == Interop.Kernel32.FileTypes.FILE_TYPE_PIPE || handleType == Interop.Kernel32.FileTypes.FILE_TYPE_CHAR, "FileStream was passed an unknown file type!"); + + _canSeek = handleType == Interop.Kernel32.FileTypes.FILE_TYPE_DISK; + _isPipe = handleType == Interop.Kernel32.FileTypes.FILE_TYPE_PIPE; + + // This is necessary for async IO using IO Completion ports via our + // managed Threadpool API's. This calls the OS's + // BindIoCompletionCallback method, and passes in a stub for the + // LPOVERLAPPED_COMPLETION_ROUTINE. This stub looks at the Overlapped + // struct for this request and gets a delegate to a managed callback + // from there, which it then calls on a threadpool thread. (We allocate + // our native OVERLAPPED structs 2 pointers too large and store EE + // state & a handle to a delegate there.) + // + // If, however, we've already bound this file handle to our completion port, + // don't try to bind it again because it will fail. A handle can only be + // bound to a single completion port at a time. + if (_useAsyncIO && !GetSuppressBindHandle(handle)) + { + try + { + _fileHandle.ThreadPoolBinding = ThreadPoolBoundHandle.BindHandle(_fileHandle); + } + catch (Exception ex) + { + // If you passed in a synchronous handle and told us to use + // it asynchronously, throw here. + throw new ArgumentException(SR.Arg_HandleNotAsync, nameof(handle), ex); + } + } + else if (!_useAsyncIO) + { + if (handleType != Interop.Kernel32.FileTypes.FILE_TYPE_PIPE) + VerifyHandleIsSync(); + } + + if (_canSeek) + SeekCore(0, SeekOrigin.Current); + else + _filePosition = 0; + } + + private static bool GetSuppressBindHandle(SafeFileHandle handle) + { + return handle.IsAsync.HasValue ? handle.IsAsync.Value : false; + } + + private unsafe static Interop.Kernel32.SECURITY_ATTRIBUTES GetSecAttrs(FileShare share) + { + Interop.Kernel32.SECURITY_ATTRIBUTES secAttrs = default(Interop.Kernel32.SECURITY_ATTRIBUTES); + if ((share & FileShare.Inheritable) != 0) + { + secAttrs = new Interop.Kernel32.SECURITY_ATTRIBUTES(); + secAttrs.nLength = (uint)sizeof(Interop.Kernel32.SECURITY_ATTRIBUTES); + + secAttrs.bInheritHandle = Interop.BOOL.TRUE; + } + return secAttrs; + } + + // Verifies that this handle supports synchronous IO operations (unless you + // didn't open it for either reading or writing). + private unsafe void VerifyHandleIsSync() + { + Debug.Assert(!_useAsyncIO); + + // Do NOT use this method on pipes. Reading or writing to a pipe may + // cause an app to block incorrectly, introducing a deadlock (depending + // on whether a write will wake up an already-blocked thread or this + // Win32FileStream's thread). + Debug.Assert(Interop.Kernel32.GetFileType(_fileHandle) != Interop.Kernel32.FileTypes.FILE_TYPE_PIPE); + + byte* bytes = stackalloc byte[1]; + int numBytesReadWritten; + int r = -1; + + // If the handle is a pipe, ReadFile will block until there + // has been a write on the other end. We'll just have to deal with it, + // For the read end of a pipe, you can mess up and + // accidentally read synchronously from an async pipe. + if ((_access & FileAccess.Read) != 0) // don't use the virtual CanRead or CanWrite, as this may be used in the ctor + { + r = Interop.Kernel32.ReadFile(_fileHandle, bytes, 0, out numBytesReadWritten, IntPtr.Zero); + } + else if ((_access & FileAccess.Write) != 0) // don't use the virtual CanRead or CanWrite, as this may be used in the ctor + { + r = Interop.Kernel32.WriteFile(_fileHandle, bytes, 0, out numBytesReadWritten, IntPtr.Zero); + } + + if (r == 0) + { + int errorCode = GetLastWin32ErrorAndDisposeHandleIfInvalid(throwIfInvalidHandle: true); + if (errorCode == ERROR_INVALID_PARAMETER) + throw new ArgumentException(SR.Arg_HandleNotSync, "handle"); + } + } + + private bool HasActiveBufferOperation + { + get { return _activeBufferOperation != null && !_activeBufferOperation.IsCompleted; } + } + + public override bool CanSeek + { + get { return _canSeek; } + } + + private unsafe long GetLengthInternal() + { + Interop.Kernel32.FILE_STANDARD_INFO info = new Interop.Kernel32.FILE_STANDARD_INFO(); + + if (!Interop.Kernel32.GetFileInformationByHandleEx(_fileHandle, Interop.Kernel32.FILE_INFO_BY_HANDLE_CLASS.FileStandardInfo, out info, (uint)sizeof(Interop.Kernel32.FILE_STANDARD_INFO))) + throw Win32Marshal.GetExceptionForLastWin32Error(); + long len = info.EndOfFile; + // If we're writing near the end of the file, we must include our + // internal buffer in our Length calculation. Don't flush because + // we use the length of the file in our async write method. + if (_writePos > 0 && _filePosition + _writePos > len) + len = _writePos + _filePosition; + return len; + } + + protected override void Dispose(bool disposing) + { + // Nothing will be done differently based on whether we are + // disposing vs. finalizing. This is taking advantage of the + // weak ordering between normal finalizable objects & critical + // finalizable objects, which I included in the SafeHandle + // design for Win32FileStream, which would often "just work" when + // finalized. + try + { + if (_fileHandle != null && !_fileHandle.IsClosed) + { + // Flush data to disk iff we were writing. After + // thinking about this, we also don't need to flush + // our read position, regardless of whether the handle + // was exposed to the user. They probably would NOT + // want us to do this. + if (_writePos > 0) + { + FlushWriteBuffer(!disposing); + } + } + } + finally + { + if (_fileHandle != null && !_fileHandle.IsClosed) + { + if (_fileHandle.ThreadPoolBinding != null) + _fileHandle.ThreadPoolBinding.Dispose(); + + _fileHandle.Dispose(); + } + + if (_preallocatedOverlapped != null) + _preallocatedOverlapped.Dispose(); + + _canSeek = false; + + // Don't set the buffer to null, to avoid a NullReferenceException + // when users have a race condition in their code (i.e. they call + // Close when calling another method on Stream like Read). + //_buffer = null; + base.Dispose(disposing); + } + } + + private void FlushOSBuffer() + { + if (!Interop.Kernel32.FlushFileBuffers(_fileHandle)) + { + throw Win32Marshal.GetExceptionForLastWin32Error(); + } + } + + // Returns a task that flushes the internal write buffer + private Task FlushWriteAsync(CancellationToken cancellationToken) + { + Debug.Assert(_useAsyncIO); + Debug.Assert(_readPos == 0 && _readLength == 0, "FileStream: Read buffer must be empty in FlushWriteAsync!"); + + // If the buffer is already flushed, don't spin up the OS write + if (_writePos == 0) return Task.CompletedTask; + + Task flushTask = WriteInternalCoreAsync(GetBuffer(), 0, _writePos, cancellationToken); + _writePos = 0; + + // Update the active buffer operation + _activeBufferOperation = HasActiveBufferOperation ? + Task.WhenAll(_activeBufferOperation, flushTask) : + flushTask; + + return flushTask; + } + + // Writes are buffered. Anytime the buffer fills up + // (_writePos + delta > _bufferSize) or the buffer switches to reading + // and there is left over data (_writePos > 0), this function must be called. + private void FlushWriteBuffer(bool calledFromFinalizer = false) + { + if (_writePos == 0) return; + Debug.Assert(_readPos == 0 && _readLength == 0, "FileStream: Read buffer must be empty in FlushWrite!"); + + if (_useAsyncIO) + { + Task writeTask = FlushWriteAsync(CancellationToken.None); + // With our Whidbey async IO & overlapped support for AD unloads, + // we don't strictly need to block here to release resources + // since that support takes care of the pinning & freeing the + // overlapped struct. We need to do this when called from + // Close so that the handle is closed when Close returns, but + // we don't need to call EndWrite from the finalizer. + // Additionally, if we do call EndWrite, we block forever + // because AD unloads prevent us from running the managed + // callback from the IO completion port. Blocking here when + // called from the finalizer during AD unload is clearly wrong, + // but we can't use any sort of test for whether the AD is + // unloading because if we weren't unloading, an AD unload + // could happen on a separate thread before we call EndWrite. + if (!calledFromFinalizer) + { + writeTask.GetAwaiter().GetResult(); + } + } + else + { + WriteCore(GetBuffer(), 0, _writePos); + } + + _writePos = 0; + } + + private void SetLengthInternal(long value) + { + // Handle buffering updates. + if (_writePos > 0) + { + FlushWriteBuffer(); + } + else if (_readPos < _readLength) + { + FlushReadBuffer(); + } + _readPos = 0; + _readLength = 0; + + if (_appendStart != -1 && value < _appendStart) + throw new IOException(SR.IO_SetLengthAppendTruncate); + SetLengthCore(value); + } + + // We absolutely need this method broken out so that WriteInternalCoreAsync can call + // a method without having to go through buffering code that might call FlushWrite. + private void SetLengthCore(long value) + { + Debug.Assert(value >= 0, "value >= 0"); + long origPos = _filePosition; + + VerifyOSHandlePosition(); + if (_filePosition != value) + SeekCore(value, SeekOrigin.Begin); + if (!Interop.Kernel32.SetEndOfFile(_fileHandle)) + { + int errorCode = Marshal.GetLastWin32Error(); + if (errorCode == Interop.Errors.ERROR_INVALID_PARAMETER) + throw new ArgumentOutOfRangeException(nameof(value), SR.ArgumentOutOfRange_FileLengthTooBig); + throw Win32Marshal.GetExceptionForWin32Error(errorCode); + } + // Return file pointer to where it was before setting length + if (origPos != value) + { + if (origPos < value) + SeekCore(origPos, SeekOrigin.Begin); + else + SeekCore(0, SeekOrigin.End); + } + } + + // Instance method to help code external to this MarshalByRefObject avoid + // accessing its fields by ref. This avoids a compiler warning. + private FileStreamCompletionSource CompareExchangeCurrentOverlappedOwner(FileStreamCompletionSource newSource, FileStreamCompletionSource existingSource) => Interlocked.CompareExchange(ref _currentOverlappedOwner, newSource, existingSource); + + public override int Read(byte[] array, int offset, int count) + { + ValidateReadWriteArgs(array, offset, count); + return ReadCore(array, offset, count); + } + + private int ReadCore(byte[] array, int offset, int count) + { + Debug.Assert((_readPos == 0 && _readLength == 0 && _writePos >= 0) || (_writePos == 0 && _readPos <= _readLength), + "We're either reading or writing, but not both."); + + bool isBlocked = false; + int n = _readLength - _readPos; + // if the read buffer is empty, read into either user's array or our + // buffer, depending on number of bytes user asked for and buffer size. + if (n == 0) + { + if (!CanRead) throw Error.GetReadNotSupported(); + if (_writePos > 0) FlushWriteBuffer(); + if (!CanSeek || (count >= _bufferLength)) + { + n = ReadNative(array, offset, count); + // Throw away read buffer. + _readPos = 0; + _readLength = 0; + return n; + } + n = ReadNative(GetBuffer(), 0, _bufferLength); + if (n == 0) return 0; + isBlocked = n < _bufferLength; + _readPos = 0; + _readLength = n; + } + // Now copy min of count or numBytesAvailable (i.e. near EOF) to array. + if (n > count) n = count; + Buffer.BlockCopy(GetBuffer(), _readPos, array, offset, n); + _readPos += n; + + // We may have read less than the number of bytes the user asked + // for, but that is part of the Stream contract. Reading again for + // more data may cause us to block if we're using a device with + // no clear end of file, such as a serial port or pipe. If we + // blocked here & this code was used with redirected pipes for a + // process's standard output, this can lead to deadlocks involving + // two processes. But leave this here for files to avoid what would + // probably be a breaking change. -- + + // If we are reading from a device with no clear EOF like a + // serial port or a pipe, this will cause us to block incorrectly. + if (!_isPipe) + { + // If we hit the end of the buffer and didn't have enough bytes, we must + // read some more from the underlying stream. However, if we got + // fewer bytes from the underlying stream than we asked for (i.e. we're + // probably blocked), don't ask for more bytes. + if (n < count && !isBlocked) + { + Debug.Assert(_readPos == _readLength, "Read buffer should be empty!"); + int moreBytesRead = ReadNative(array, offset + n, count - n); + n += moreBytesRead; + // We've just made our buffer inconsistent with our position + // pointer. We must throw away the read buffer. + _readPos = 0; + _readLength = 0; + } + } + + return n; + } + + [Conditional("DEBUG")] + private void AssertCanRead(byte[] buffer, int offset, int count) + { + Debug.Assert(!_fileHandle.IsClosed, "!_fileHandle.IsClosed"); + Debug.Assert(CanRead, "CanRead"); + Debug.Assert(buffer != null, "buffer != null"); + Debug.Assert(_writePos == 0, "_writePos == 0"); + Debug.Assert(offset >= 0, "offset is negative"); + Debug.Assert(count >= 0, "count is negative"); + } + + private unsafe int ReadNative(byte[] buffer, int offset, int count) + { + AssertCanRead(buffer, offset, count); + + if (_useAsyncIO) + return ReadNativeAsync(buffer, offset, count, 0, CancellationToken.None).GetAwaiter().GetResult(); + + // Make sure we are reading from the right spot + VerifyOSHandlePosition(); + + int errorCode = 0; + int r = ReadFileNative(_fileHandle, buffer, offset, count, null, out errorCode); + + if (r == -1) + { + // For pipes, ERROR_BROKEN_PIPE is the normal end of the pipe. + if (errorCode == ERROR_BROKEN_PIPE) + { + r = 0; + } + else + { + if (errorCode == ERROR_INVALID_PARAMETER) + throw new ArgumentException(SR.Arg_HandleNotSync, "_fileHandle"); + + throw Win32Marshal.GetExceptionForWin32Error(errorCode); + } + } + Debug.Assert(r >= 0, "FileStream's ReadNative is likely broken."); + _filePosition += r; + + return r; + } + + public override long Seek(long offset, SeekOrigin origin) + { + if (origin < SeekOrigin.Begin || origin > SeekOrigin.End) + throw new ArgumentException(SR.Argument_InvalidSeekOrigin, nameof(origin)); + if (_fileHandle.IsClosed) throw Error.GetFileNotOpen(); + if (!CanSeek) throw Error.GetSeekNotSupported(); + + Debug.Assert((_readPos == 0 && _readLength == 0 && _writePos >= 0) || (_writePos == 0 && _readPos <= _readLength), "We're either reading or writing, but not both."); + + // If we've got bytes in our buffer to write, write them out. + // If we've read in and consumed some bytes, we'll have to adjust + // our seek positions ONLY IF we're seeking relative to the current + // position in the stream. This simulates doing a seek to the new + // position, then a read for the number of bytes we have in our buffer. + if (_writePos > 0) + { + FlushWriteBuffer(); + } + else if (origin == SeekOrigin.Current) + { + // Don't call FlushRead here, which would have caused an infinite + // loop. Simply adjust the seek origin. This isn't necessary + // if we're seeking relative to the beginning or end of the stream. + offset -= (_readLength - _readPos); + } + _readPos = _readLength = 0; + + // Verify that internal position is in sync with the handle + VerifyOSHandlePosition(); + + long oldPos = _filePosition + (_readPos - _readLength); + long pos = SeekCore(offset, origin); + + // Prevent users from overwriting data in a file that was opened in + // append mode. + if (_appendStart != -1 && pos < _appendStart) + { + SeekCore(oldPos, SeekOrigin.Begin); + throw new IOException(SR.IO_SeekAppendOverwrite); + } + + // We now must update the read buffer. We can in some cases simply + // update _readPos within the buffer, copy around the buffer so our + // Position property is still correct, and avoid having to do more + // reads from the disk. Otherwise, discard the buffer's contents. + if (_readLength > 0) + { + // We can optimize the following condition: + // oldPos - _readPos <= pos < oldPos + _readLen - _readPos + if (oldPos == pos) + { + if (_readPos > 0) + { + //Console.WriteLine("Seek: seeked for 0, adjusting buffer back by: "+_readPos+" _readLen: "+_readLen); + Buffer.BlockCopy(GetBuffer(), _readPos, GetBuffer(), 0, _readLength - _readPos); + _readLength -= _readPos; + _readPos = 0; + } + // If we still have buffered data, we must update the stream's + // position so our Position property is correct. + if (_readLength > 0) + SeekCore(_readLength, SeekOrigin.Current); + } + else if (oldPos - _readPos < pos && pos < oldPos + _readLength - _readPos) + { + int diff = (int)(pos - oldPos); + //Console.WriteLine("Seek: diff was "+diff+", readpos was "+_readPos+" adjusting buffer - shrinking by "+ (_readPos + diff)); + Buffer.BlockCopy(GetBuffer(), _readPos + diff, GetBuffer(), 0, _readLength - (_readPos + diff)); + _readLength -= (_readPos + diff); + _readPos = 0; + if (_readLength > 0) + SeekCore(_readLength, SeekOrigin.Current); + } + else + { + // Lose the read buffer. + _readPos = 0; + _readLength = 0; + } + Debug.Assert(_readLength >= 0 && _readPos <= _readLength, "_readLen should be nonnegative, and _readPos should be less than or equal _readLen"); + Debug.Assert(pos == Position, "Seek optimization: pos != Position! Buffer math was mangled."); + } + return pos; + } + + // This doesn't do argument checking. Necessary for SetLength, which must + // set the file pointer beyond the end of the file. This will update the + // internal position + // This is called during construction so it should avoid any virtual + // calls + private long SeekCore(long offset, SeekOrigin origin) + { + Debug.Assert(!_fileHandle.IsClosed && _canSeek, "!_handle.IsClosed && _parent.CanSeek"); + Debug.Assert(origin >= SeekOrigin.Begin && origin <= SeekOrigin.End, "origin>=SeekOrigin.Begin && origin<=SeekOrigin.End"); + long ret = 0; + + if (!Interop.Kernel32.SetFilePointerEx(_fileHandle, offset, out ret, (uint)origin)) + { + int errorCode = GetLastWin32ErrorAndDisposeHandleIfInvalid(); + throw Win32Marshal.GetExceptionForWin32Error(errorCode); + } + + _filePosition = ret; + return ret; + } + + partial void OnBufferAllocated() + { + Debug.Assert(_buffer != null); + Debug.Assert(_preallocatedOverlapped == null); + + if (_useAsyncIO) + _preallocatedOverlapped = new PreAllocatedOverlapped(s_ioCallback, this, _buffer); + } + + public override void Write(byte[] array, int offset, int count) + { + ValidateReadWriteArgs(array, offset, count); + + if (_writePos == 0) + { + // Ensure we can write to the stream, and ready buffer for writing. + if (!CanWrite) throw Error.GetWriteNotSupported(); + if (_readPos < _readLength) FlushReadBuffer(); + _readPos = 0; + _readLength = 0; + } + + // If our buffer has data in it, copy data from the user's array into + // the buffer, and if we can fit it all there, return. Otherwise, write + // the buffer to disk and copy any remaining data into our buffer. + // The assumption here is memcpy is cheaper than disk (or net) IO. + // (10 milliseconds to disk vs. ~20-30 microseconds for a 4K memcpy) + // So the extra copying will reduce the total number of writes, in + // non-pathological cases (i.e. write 1 byte, then write for the buffer + // size repeatedly) + if (_writePos > 0) + { + int numBytes = _bufferLength - _writePos; // space left in buffer + if (numBytes > 0) + { + if (numBytes > count) + numBytes = count; + Buffer.BlockCopy(array, offset, GetBuffer(), _writePos, numBytes); + _writePos += numBytes; + if (count == numBytes) return; + offset += numBytes; + count -= numBytes; + } + // Reset our buffer. We essentially want to call FlushWrite + // without calling Flush on the underlying Stream. + + if (_useAsyncIO) + { + WriteInternalCoreAsync(GetBuffer(), 0, _writePos, CancellationToken.None).GetAwaiter().GetResult(); + } + else + { + WriteCore(GetBuffer(), 0, _writePos); + } + _writePos = 0; + } + // If the buffer would slow writes down, avoid buffer completely. + if (count >= _bufferLength) + { + Debug.Assert(_writePos == 0, "FileStream cannot have buffered data to write here! Your stream will be corrupted."); + WriteCore(array, offset, count); + return; + } + else if (count == 0) + { + return; // Don't allocate a buffer then call memcpy for 0 bytes. + } + + // Copy remaining bytes into buffer, to write at a later date. + Buffer.BlockCopy(array, offset, GetBuffer(), _writePos, count); + _writePos = count; + return; + } + + private unsafe void WriteCore(byte[] buffer, int offset, int count) + { + Debug.Assert(!_fileHandle.IsClosed, "!_handle.IsClosed"); + Debug.Assert(CanWrite, "_parent.CanWrite"); + + Debug.Assert(buffer != null, "buffer != null"); + Debug.Assert(_readPos == _readLength, "_readPos == _readLen"); + Debug.Assert(offset >= 0, "offset is negative"); + Debug.Assert(count >= 0, "count is negative"); + if (_useAsyncIO) + { + WriteInternalCoreAsync(buffer, offset, count, CancellationToken.None).GetAwaiter().GetResult(); + return; + } + + // Make sure we are writing to the position that we think we are + VerifyOSHandlePosition(); + + int errorCode = 0; + int r = WriteFileNative(_fileHandle, buffer, offset, count, null, out errorCode); + + if (r == -1) + { + // For pipes, ERROR_NO_DATA is not an error, but the pipe is closing. + if (errorCode == ERROR_NO_DATA) + { + r = 0; + } + else + { + // ERROR_INVALID_PARAMETER may be returned for writes + // where the position is too large (i.e. writing at Int64.MaxValue + // on Win9x) OR for synchronous writes to a handle opened + // asynchronously. + if (errorCode == ERROR_INVALID_PARAMETER) + throw new IOException(SR.IO_FileTooLongOrHandleNotSync); + throw Win32Marshal.GetExceptionForWin32Error(errorCode); + } + } + Debug.Assert(r >= 0, "FileStream's WriteCore is likely broken."); + _filePosition += r; + return; + } + + private Task<int> ReadAsyncInternal(byte[] array, int offset, int numBytes, CancellationToken cancellationToken) + { + // If async IO is not supported on this platform or + // if this Win32FileStream was not opened with FileOptions.Asynchronous. + if (!_useAsyncIO) + { + return base.ReadAsync(array, offset, numBytes, cancellationToken); + } + + if (!CanRead) throw Error.GetReadNotSupported(); + + Debug.Assert((_readPos == 0 && _readLength == 0 && _writePos >= 0) || (_writePos == 0 && _readPos <= _readLength), "We're either reading or writing, but not both."); + + if (_isPipe) + { + // Pipes are tricky, at least when you have 2 different pipes + // that you want to use simultaneously. When redirecting stdout + // & stderr with the Process class, it's easy to deadlock your + // parent & child processes when doing writes 4K at a time. The + // OS appears to use a 4K buffer internally. If you write to a + // pipe that is full, you will block until someone read from + // that pipe. If you try reading from an empty pipe and + // Win32FileStream's ReadAsync blocks waiting for data to fill it's + // internal buffer, you will be blocked. In a case where a child + // process writes to stdout & stderr while a parent process tries + // reading from both, you can easily get into a deadlock here. + // To avoid this deadlock, don't buffer when doing async IO on + // pipes. But don't completely ignore buffered data either. + if (_readPos < _readLength) + { + int n = _readLength - _readPos; + if (n > numBytes) n = numBytes; + Buffer.BlockCopy(GetBuffer(), _readPos, array, offset, n); + _readPos += n; + + // Return a completed task + return TaskFromResultOrCache(n); + } + else + { + Debug.Assert(_writePos == 0, "Win32FileStream must not have buffered write data here! Pipes should be unidirectional."); + return ReadNativeAsync(array, offset, numBytes, 0, cancellationToken); + } + } + + Debug.Assert(!_isPipe, "Should not be a pipe."); + + // Handle buffering. + if (_writePos > 0) FlushWriteBuffer(); + if (_readPos == _readLength) + { + // I can't see how to handle buffering of async requests when + // filling the buffer asynchronously, without a lot of complexity. + // The problems I see are issuing an async read, we do an async + // read to fill the buffer, then someone issues another read + // (either synchronously or asynchronously) before the first one + // returns. This would involve some sort of complex buffer locking + // that we probably don't want to get into, at least not in V1. + // If we did a sync read to fill the buffer, we could avoid the + // problem, and any async read less than 64K gets turned into a + // synchronous read by NT anyways... -- + + if (numBytes < _bufferLength) + { + Task<int> readTask = ReadNativeAsync(GetBuffer(), 0, _bufferLength, 0, cancellationToken); + _readLength = readTask.GetAwaiter().GetResult(); + int n = _readLength; + if (n > numBytes) n = numBytes; + Buffer.BlockCopy(GetBuffer(), 0, array, offset, n); + _readPos = n; + + // Return a completed task (recycling the one above if possible) + return (_readLength == n ? readTask : TaskFromResultOrCache(n)); + } + else + { + // Here we're making our position pointer inconsistent + // with our read buffer. Throw away the read buffer's contents. + _readPos = 0; + _readLength = 0; + return ReadNativeAsync(array, offset, numBytes, 0, cancellationToken); + } + } + else + { + int n = _readLength - _readPos; + if (n > numBytes) n = numBytes; + Buffer.BlockCopy(GetBuffer(), _readPos, array, offset, n); + _readPos += n; + + if (n >= numBytes) + { + // Return a completed task + return TaskFromResultOrCache(n); + } + else + { + // For streams with no clear EOF like serial ports or pipes + // we cannot read more data without causing an app to block + // incorrectly. Pipes don't go down this path + // though. This code needs to be fixed. + // Throw away read buffer. + _readPos = 0; + _readLength = 0; + return ReadNativeAsync(array, offset + n, numBytes - n, n, cancellationToken); + } + } + } + + unsafe private Task<int> ReadNativeAsync(byte[] bytes, int offset, int numBytes, int numBufferedBytesRead, CancellationToken cancellationToken) + { + AssertCanRead(bytes, offset, numBytes); + Debug.Assert(_useAsyncIO, "ReadNativeAsync doesn't work on synchronous file streams!"); + + // Create and store async stream class library specific data in the async result + + FileStreamCompletionSource completionSource = new FileStreamCompletionSource(this, numBufferedBytesRead, bytes, cancellationToken); + NativeOverlapped* intOverlapped = completionSource.Overlapped; + + // Calculate position in the file we should be at after the read is done + if (CanSeek) + { + long len = Length; + + // Make sure we are reading from the position that we think we are + VerifyOSHandlePosition(); + + if (_filePosition + numBytes > len) + { + if (_filePosition <= len) + numBytes = (int)(len - _filePosition); + else + numBytes = 0; + } + + // Now set the position to read from in the NativeOverlapped struct + // For pipes, we should leave the offset fields set to 0. + intOverlapped->OffsetLow = unchecked((int)_filePosition); + intOverlapped->OffsetHigh = (int)(_filePosition >> 32); + + // When using overlapped IO, the OS is not supposed to + // touch the file pointer location at all. We will adjust it + // ourselves. This isn't threadsafe. + + // WriteFile should not update the file pointer when writing + // in overlapped mode, according to MSDN. But it does update + // the file pointer when writing to a UNC path! + // So changed the code below to seek to an absolute + // location, not a relative one. ReadFile seems consistent though. + SeekCore(numBytes, SeekOrigin.Current); + } + + // queue an async ReadFile operation and pass in a packed overlapped + int errorCode = 0; + int r = ReadFileNative(_fileHandle, bytes, offset, numBytes, intOverlapped, out errorCode); + // ReadFile, the OS version, will return 0 on failure. But + // my ReadFileNative wrapper returns -1. My wrapper will return + // the following: + // On error, r==-1. + // On async requests that are still pending, r==-1 w/ errorCode==ERROR_IO_PENDING + // on async requests that completed sequentially, r==0 + // You will NEVER RELIABLY be able to get the number of bytes + // read back from this call when using overlapped structures! You must + // not pass in a non-null lpNumBytesRead to ReadFile when using + // overlapped structures! This is by design NT behavior. + if (r == -1 && numBytes != -1) + { + // For pipes, when they hit EOF, they will come here. + if (errorCode == ERROR_BROKEN_PIPE) + { + // Not an error, but EOF. AsyncFSCallback will NOT be + // called. Call the user callback here. + + // We clear the overlapped status bit for this special case. + // Failure to do so looks like we are freeing a pending overlapped later. + intOverlapped->InternalLow = IntPtr.Zero; + completionSource.SetCompletedSynchronously(0); + } + else if (errorCode != ERROR_IO_PENDING) + { + if (!_fileHandle.IsClosed && CanSeek) // Update Position - It could be anywhere. + { + SeekCore(0, SeekOrigin.Current); + } + + completionSource.ReleaseNativeResource(); + + if (errorCode == ERROR_HANDLE_EOF) + { + throw Error.GetEndOfFile(); + } + else + { + throw Win32Marshal.GetExceptionForWin32Error(errorCode); + } + } + else + { + // Only once the IO is pending do we register for cancellation + completionSource.RegisterForCancellation(); + } + } + else + { + // Due to a workaround for a race condition in NT's ReadFile & + // WriteFile routines, we will always be returning 0 from ReadFileNative + // when we do async IO instead of the number of bytes read, + // irregardless of whether the operation completed + // synchronously or asynchronously. We absolutely must not + // set asyncResult._numBytes here, since will never have correct + // results. + //Console.WriteLine("ReadFile returned: "+r+" (0x"+Int32.Format(r, "x")+") The IO completed synchronously, but the user callback was called on a separate thread"); + } + + return completionSource.Task; + } + + // Reads a byte from the file stream. Returns the byte cast to an int + // or -1 if reading from the end of the stream. + public override int ReadByte() + { + return ReadByteCore(); + } + + private Task WriteAsyncInternal(byte[] array, int offset, int numBytes, CancellationToken cancellationToken) + { + // If async IO is not supported on this platform or + // if this Win32FileStream was not opened with FileOptions.Asynchronous. + if (!_useAsyncIO) + { + return base.WriteAsync(array, offset, numBytes, cancellationToken); + } + + if (!CanWrite) throw Error.GetWriteNotSupported(); + + Debug.Assert((_readPos == 0 && _readLength == 0 && _writePos >= 0) || (_writePos == 0 && _readPos <= _readLength), "We're either reading or writing, but not both."); + Debug.Assert(!_isPipe || (_readPos == 0 && _readLength == 0), "Win32FileStream must not have buffered data here! Pipes should be unidirectional."); + + bool writeDataStoredInBuffer = false; + if (!_isPipe) // avoid async buffering with pipes, as doing so can lead to deadlocks (see comments in ReadInternalAsyncCore) + { + // Ensure the buffer is clear for writing + if (_writePos == 0) + { + if (_readPos < _readLength) + { + FlushReadBuffer(); + } + _readPos = 0; + _readLength = 0; + } + + // Determine how much space remains in the buffer + int remainingBuffer = _bufferLength - _writePos; + Debug.Assert(remainingBuffer >= 0); + + // Simple/common case: + // - The write is smaller than our buffer, such that it's worth considering buffering it. + // - There's no active flush operation, such that we don't have to worry about the existing buffer being in use. + // - And the data we're trying to write fits in the buffer, meaning it wasn't already filled by previous writes. + // In that case, just store it in the buffer. + if (numBytes < _bufferLength && !HasActiveBufferOperation && numBytes <= remainingBuffer) + { + Buffer.BlockCopy(array, offset, GetBuffer(), _writePos, numBytes); + _writePos += numBytes; + writeDataStoredInBuffer = true; + + // There is one special-but-common case, common because devs often use + // byte[] sizes that are powers of 2 and thus fit nicely into our buffer, which is + // also a power of 2. If after our write the buffer still has remaining space, + // then we're done and can return a completed task now. But if we filled the buffer + // completely, we want to do the asynchronous flush/write as part of this operation + // rather than waiting until the next write that fills the buffer. + if (numBytes != remainingBuffer) + return Task.CompletedTask; + + Debug.Assert(_writePos == _bufferLength); + } + } + + // At this point, at least one of the following is true: + // 1. There was an active flush operation (it could have completed by now, though). + // 2. The data doesn't fit in the remaining buffer (or it's a pipe and we chose not to try). + // 3. We wrote all of the data to the buffer, filling it. + // + // If there's an active operation, we can't touch the current buffer because it's in use. + // That gives us a choice: we can either allocate a new buffer, or we can skip the buffer + // entirely (even if the data would otherwise fit in it). For now, for simplicity, we do + // the latter; it could also have performance wins due to OS-level optimizations, and we could + // potentially add support for PreAllocatedOverlapped due to having a single buffer. (We can + // switch to allocating a new buffer, potentially experimenting with buffer pooling, should + // performance data suggest it's appropriate.) + // + // If the data doesn't fit in the remaining buffer, it could be because it's so large + // it's greater than the entire buffer size, in which case we'd always skip the buffer, + // or it could be because there's more data than just the space remaining. For the latter + // case, we need to issue an asynchronous write to flush that data, which then turns this into + // the first case above with an active operation. + // + // If we already stored the data, then we have nothing additional to write beyond what + // we need to flush. + // + // In any of these cases, we have the same outcome: + // - If there's data in the buffer, flush it by writing it out asynchronously. + // - Then, if there's any data to be written, issue a write for it concurrently. + // We return a Task that represents one or both. + + // Flush the buffer asynchronously if there's anything to flush + Task flushTask = null; + if (_writePos > 0) + { + flushTask = FlushWriteAsync(cancellationToken); + + // If we already copied all of the data into the buffer, + // simply return the flush task here. Same goes for if the task has + // already completed and was unsuccessful. + if (writeDataStoredInBuffer || + flushTask.IsFaulted || + flushTask.IsCanceled) + { + return flushTask; + } + } + + Debug.Assert(!writeDataStoredInBuffer); + Debug.Assert(_writePos == 0); + + // Finally, issue the write asynchronously, and return a Task that logically + // represents the write operation, including any flushing done. + Task writeTask = WriteInternalCoreAsync(array, offset, numBytes, cancellationToken); + return + (flushTask == null || flushTask.Status == TaskStatus.RanToCompletion) ? writeTask : + (writeTask.Status == TaskStatus.RanToCompletion) ? flushTask : + Task.WhenAll(flushTask, writeTask); + } + + private unsafe Task WriteInternalCoreAsync(byte[] bytes, int offset, int numBytes, CancellationToken cancellationToken) + { + Debug.Assert(!_fileHandle.IsClosed, "!_handle.IsClosed"); + Debug.Assert(CanWrite, "_parent.CanWrite"); + Debug.Assert(bytes != null, "bytes != null"); + Debug.Assert(_readPos == _readLength, "_readPos == _readLen"); + Debug.Assert(_useAsyncIO, "WriteInternalCoreAsync doesn't work on synchronous file streams!"); + Debug.Assert(offset >= 0, "offset is negative"); + Debug.Assert(numBytes >= 0, "numBytes is negative"); + + // Create and store async stream class library specific data in the async result + FileStreamCompletionSource completionSource = new FileStreamCompletionSource(this, 0, bytes, cancellationToken); + NativeOverlapped* intOverlapped = completionSource.Overlapped; + + if (CanSeek) + { + // Make sure we set the length of the file appropriately. + long len = Length; + //Console.WriteLine("WriteInternalCoreAsync - Calculating end pos. pos: "+pos+" len: "+len+" numBytes: "+numBytes); + + // Make sure we are writing to the position that we think we are + VerifyOSHandlePosition(); + + if (_filePosition + numBytes > len) + { + //Console.WriteLine("WriteInternalCoreAsync - Setting length to: "+(pos + numBytes)); + SetLengthCore(_filePosition + numBytes); + } + + // Now set the position to read from in the NativeOverlapped struct + // For pipes, we should leave the offset fields set to 0. + intOverlapped->OffsetLow = (int)_filePosition; + intOverlapped->OffsetHigh = (int)(_filePosition >> 32); + + // When using overlapped IO, the OS is not supposed to + // touch the file pointer location at all. We will adjust it + // ourselves. This isn't threadsafe. + SeekCore(numBytes, SeekOrigin.Current); + } + + //Console.WriteLine("WriteInternalCoreAsync finishing. pos: "+pos+" numBytes: "+numBytes+" _pos: "+_pos+" Position: "+Position); + + int errorCode = 0; + // queue an async WriteFile operation and pass in a packed overlapped + int r = WriteFileNative(_fileHandle, bytes, offset, numBytes, intOverlapped, out errorCode); + + // WriteFile, the OS version, will return 0 on failure. But + // my WriteFileNative wrapper returns -1. My wrapper will return + // the following: + // On error, r==-1. + // On async requests that are still pending, r==-1 w/ errorCode==ERROR_IO_PENDING + // On async requests that completed sequentially, r==0 + // You will NEVER RELIABLY be able to get the number of bytes + // written back from this call when using overlapped IO! You must + // not pass in a non-null lpNumBytesWritten to WriteFile when using + // overlapped structures! This is ByDesign NT behavior. + if (r == -1 && numBytes != -1) + { + //Console.WriteLine("WriteFile returned 0; Write will complete asynchronously (if errorCode==3e5) errorCode: 0x{0:x}", errorCode); + + // For pipes, when they are closed on the other side, they will come here. + if (errorCode == ERROR_NO_DATA) + { + // Not an error, but EOF. AsyncFSCallback will NOT be called. + // Completing TCS and return cached task allowing the GC to collect TCS. + completionSource.SetCompletedSynchronously(0); + return Task.CompletedTask; + } + else if (errorCode != ERROR_IO_PENDING) + { + if (!_fileHandle.IsClosed && CanSeek) // Update Position - It could be anywhere. + { + SeekCore(0, SeekOrigin.Current); + } + + completionSource.ReleaseNativeResource(); + + if (errorCode == ERROR_HANDLE_EOF) + { + throw Error.GetEndOfFile(); + } + else + { + throw Win32Marshal.GetExceptionForWin32Error(errorCode); + } + } + else // ERROR_IO_PENDING + { + // Only once the IO is pending do we register for cancellation + completionSource.RegisterForCancellation(); + } + } + else + { + // Due to a workaround for a race condition in NT's ReadFile & + // WriteFile routines, we will always be returning 0 from WriteFileNative + // when we do async IO instead of the number of bytes written, + // irregardless of whether the operation completed + // synchronously or asynchronously. We absolutely must not + // set asyncResult._numBytes here, since will never have correct + // results. + //Console.WriteLine("WriteFile returned: "+r+" (0x"+Int32.Format(r, "x")+") The IO completed synchronously, but the user callback was called on another thread."); + } + + return completionSource.Task; + } + + public override void WriteByte(byte value) + { + WriteByteCore(value); + } + + // Windows API definitions, from winbase.h and others + + private const int FILE_ATTRIBUTE_NORMAL = 0x00000080; + private const int FILE_ATTRIBUTE_ENCRYPTED = 0x00004000; + private const int FILE_FLAG_OVERLAPPED = 0x40000000; + internal const int GENERIC_READ = unchecked((int)0x80000000); + private const int GENERIC_WRITE = 0x40000000; + + private const int FILE_BEGIN = 0; + private const int FILE_CURRENT = 1; + private const int FILE_END = 2; + + // Error codes (not HRESULTS), from winerror.h + internal const int ERROR_BROKEN_PIPE = 109; + internal const int ERROR_NO_DATA = 232; + private const int ERROR_HANDLE_EOF = 38; + private const int ERROR_INVALID_PARAMETER = 87; + private const int ERROR_IO_PENDING = 997; + + // __ConsoleStream also uses this code. + private unsafe int ReadFileNative(SafeFileHandle handle, byte[] bytes, int offset, int count, NativeOverlapped* overlapped, out int errorCode) + { + Debug.Assert(handle != null, "handle != null"); + Debug.Assert(offset >= 0, "offset >= 0"); + Debug.Assert(count >= 0, "count >= 0"); + Debug.Assert(bytes != null, "bytes != null"); + // Don't corrupt memory when multiple threads are erroneously writing + // to this stream simultaneously. + if (bytes.Length - offset < count) + throw new IndexOutOfRangeException(SR.IndexOutOfRange_IORaceCondition); + + Debug.Assert((_useAsyncIO && overlapped != null) || (!_useAsyncIO && overlapped == null), "Async IO and overlapped parameters inconsistent in call to ReadFileNative."); + + // You can't use the fixed statement on an array of length 0. + if (bytes.Length == 0) + { + errorCode = 0; + return 0; + } + + int r = 0; + int numBytesRead = 0; + + fixed (byte* p = &bytes[0]) + { + if (_useAsyncIO) + r = Interop.Kernel32.ReadFile(handle, p + offset, count, IntPtr.Zero, overlapped); + else + r = Interop.Kernel32.ReadFile(handle, p + offset, count, out numBytesRead, IntPtr.Zero); + } + + if (r == 0) + { + errorCode = GetLastWin32ErrorAndDisposeHandleIfInvalid(); + return -1; + } + else + { + errorCode = 0; + return numBytesRead; + } + } + + private unsafe int WriteFileNative(SafeFileHandle handle, byte[] bytes, int offset, int count, NativeOverlapped* overlapped, out int errorCode) + { + Debug.Assert(handle != null, "handle != null"); + Debug.Assert(offset >= 0, "offset >= 0"); + Debug.Assert(count >= 0, "count >= 0"); + Debug.Assert(bytes != null, "bytes != null"); + // Don't corrupt memory when multiple threads are erroneously writing + // to this stream simultaneously. (the OS is reading from + // the array we pass to WriteFile, but if we read beyond the end and + // that memory isn't allocated, we could get an AV.) + if (bytes.Length - offset < count) + throw new IndexOutOfRangeException(SR.IndexOutOfRange_IORaceCondition); + + Debug.Assert((_useAsyncIO && overlapped != null) || (!_useAsyncIO && overlapped == null), "Async IO and overlapped parameters inconsistent in call to WriteFileNative."); + + // You can't use the fixed statement on an array of length 0. + if (bytes.Length == 0) + { + errorCode = 0; + return 0; + } + + int numBytesWritten = 0; + int r = 0; + + fixed (byte* p = &bytes[0]) + { + if (_useAsyncIO) + r = Interop.Kernel32.WriteFile(handle, p + offset, count, IntPtr.Zero, overlapped); + else + r = Interop.Kernel32.WriteFile(handle, p + offset, count, out numBytesWritten, IntPtr.Zero); + } + + if (r == 0) + { + errorCode = GetLastWin32ErrorAndDisposeHandleIfInvalid(); + return -1; + } + else + { + errorCode = 0; + return numBytesWritten; + } + } + + private int GetLastWin32ErrorAndDisposeHandleIfInvalid(bool throwIfInvalidHandle = false) + { + int errorCode = Marshal.GetLastWin32Error(); + + // If ERROR_INVALID_HANDLE is returned, it doesn't suffice to set + // the handle as invalid; the handle must also be closed. + // + // Marking the handle as invalid but not closing the handle + // resulted in exceptions during finalization and locked column + // values (due to invalid but unclosed handle) in SQL Win32FileStream + // scenarios. + // + // A more mainstream scenario involves accessing a file on a + // network share. ERROR_INVALID_HANDLE may occur because the network + // connection was dropped and the server closed the handle. However, + // the client side handle is still open and even valid for certain + // operations. + // + // Note that _parent.Dispose doesn't throw so we don't need to special case. + // SetHandleAsInvalid only sets _closed field to true (without + // actually closing handle) so we don't need to call that as well. + if (errorCode == Interop.Errors.ERROR_INVALID_HANDLE) + { + _fileHandle.Dispose(); + + if (throwIfInvalidHandle) + throw Win32Marshal.GetExceptionForWin32Error(errorCode); + } + + return errorCode; + } + + public override Task CopyToAsync(Stream destination, int bufferSize, CancellationToken cancellationToken) + { + // If we're in sync mode, just use the shared CopyToAsync implementation that does + // typical read/write looping. 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. + if (!_useAsyncIO || GetType() != typeof(FileStream)) + { + return base.CopyToAsync(destination, bufferSize, cancellationToken); + } + + StreamHelpers.ValidateCopyToArgs(this, destination, bufferSize); + + // Bail early for cancellation if cancellation has been requested + if (cancellationToken.IsCancellationRequested) + { + return Task.FromCanceled<int>(cancellationToken); + } + + // Fail if the file was closed + if (_fileHandle.IsClosed) + { + throw Error.GetFileNotOpen(); + } + + // Do the async copy, with differing implementations based on whether the FileStream was opened as async or sync + Debug.Assert((_readPos == 0 && _readLength == 0 && _writePos >= 0) || (_writePos == 0 && _readPos <= _readLength), "We're either reading or writing, but not both."); + return AsyncModeCopyToAsync(destination, bufferSize, cancellationToken); + } + + private async Task AsyncModeCopyToAsync(Stream destination, int bufferSize, CancellationToken cancellationToken) + { + Debug.Assert(_useAsyncIO, "This implementation is for async mode only"); + Debug.Assert(!_fileHandle.IsClosed, "!_handle.IsClosed"); + Debug.Assert(CanRead, "_parent.CanRead"); + + // Make sure any pending writes have been flushed before we do a read. + if (_writePos > 0) + { + await FlushWriteAsync(cancellationToken).ConfigureAwait(false); + } + + // Typically CopyToAsync would be invoked as the only "read" on the stream, but it's possible some reading is + // done and then the CopyToAsync is issued. For that case, see if we have any data available in the buffer. + if (GetBuffer() != null) + { + int bufferedBytes = _readLength - _readPos; + if (bufferedBytes > 0) + { + await destination.WriteAsync(GetBuffer(), _readPos, bufferedBytes, cancellationToken).ConfigureAwait(false); + _readPos = _readLength = 0; + } + } + + // For efficiency, we avoid creating a new task and associated state for each asynchronous read. + // Instead, we create a single reusable awaitable object that will be triggered when an await completes + // and reset before going again. + var readAwaitable = new AsyncCopyToAwaitable(this); + + // Make sure we are reading from the position that we think we are. + // Only set the position in the awaitable if we can seek (e.g. not for pipes). + bool canSeek = CanSeek; + if (canSeek) + { + VerifyOSHandlePosition(); + readAwaitable._position = _filePosition; + } + + // Get the buffer to use for the copy operation, as the base CopyToAsync does. We don't try to use + // _buffer here, even if it's not null, as concurrent operations are allowed, and another operation may + // actually be using the buffer already. Plus, it'll be rare for _buffer to be non-null, as typically + // CopyToAsync is used as the only operation performed on the stream, and the buffer is lazily initialized. + // Further, typically the CopyToAsync buffer size will be larger than that used by the FileStream, such that + // we'd likely be unable to use it anyway. Instead, we rent the buffer from a pool. + byte[] copyBuffer = ArrayPool<byte>.Shared.Rent(bufferSize); + bufferSize = 0; // repurpose bufferSize to be the high water mark for the buffer, to avoid an extra field in the state machine + + // Allocate an Overlapped we can use repeatedly for all operations + var awaitableOverlapped = new PreAllocatedOverlapped(AsyncCopyToAwaitable.s_callback, readAwaitable, copyBuffer); + var cancellationReg = default(CancellationTokenRegistration); + try + { + // Register for cancellation. We do this once for the whole copy operation, and just try to cancel + // whatever read operation may currently be in progress, if there is one. It's possible the cancellation + // request could come in between operations, in which case we flag that with explicit calls to ThrowIfCancellationRequested + // in the read/write copy loop. + if (cancellationToken.CanBeCanceled) + { + cancellationReg = cancellationToken.Register(s => + { + var innerAwaitable = (AsyncCopyToAwaitable)s; + unsafe + { + lock (innerAwaitable.CancellationLock) // synchronize with cleanup of the overlapped + { + if (innerAwaitable._nativeOverlapped != null) + { + // Try to cancel the I/O. We ignore the return value, as cancellation is opportunistic and we + // don't want to fail the operation because we couldn't cancel it. + Interop.Kernel32.CancelIoEx(innerAwaitable._fileStream._fileHandle, innerAwaitable._nativeOverlapped); + } + } + } + }, readAwaitable); + } + + // Repeatedly read from this FileStream and write the results to the destination stream. + while (true) + { + cancellationToken.ThrowIfCancellationRequested(); + readAwaitable.ResetForNextOperation(); + + try + { + bool synchronousSuccess; + int errorCode; + unsafe + { + // Allocate a native overlapped for our reusable overlapped, and set position to read based on the next + // desired address stored in the awaitable. (This position may be 0, if either we're at the beginning or + // if the stream isn't seekable.) + readAwaitable._nativeOverlapped = _fileHandle.ThreadPoolBinding.AllocateNativeOverlapped(awaitableOverlapped); + if (canSeek) + { + readAwaitable._nativeOverlapped->OffsetLow = unchecked((int)readAwaitable._position); + readAwaitable._nativeOverlapped->OffsetHigh = (int)(readAwaitable._position >> 32); + } + + // Kick off the read. + synchronousSuccess = ReadFileNative(_fileHandle, copyBuffer, 0, copyBuffer.Length, readAwaitable._nativeOverlapped, out errorCode) >= 0; + } + + // If the operation did not synchronously succeed, it either failed or initiated the asynchronous operation. + if (!synchronousSuccess) + { + switch (errorCode) + { + case ERROR_IO_PENDING: + // Async operation in progress. + break; + case ERROR_BROKEN_PIPE: + case ERROR_HANDLE_EOF: + // We're at or past the end of the file, and the overlapped callback + // won't be raised in these cases. Mark it as completed so that the await + // below will see it as such. + readAwaitable.MarkCompleted(); + break; + default: + // Everything else is an error (and there won't be a callback). + throw Win32Marshal.GetExceptionForWin32Error(errorCode); + } + } + + // Wait for the async operation (which may or may not have already completed), then throw if it failed. + await readAwaitable; + switch (readAwaitable._errorCode) + { + case 0: // success + Debug.Assert(readAwaitable._numBytes >= 0, $"Expected non-negative numBytes, got {readAwaitable._numBytes}"); + break; + case ERROR_BROKEN_PIPE: // logically success with 0 bytes read (write end of pipe closed) + case ERROR_HANDLE_EOF: // logically success with 0 bytes read (read at end of file) + Debug.Assert(readAwaitable._numBytes == 0, $"Expected 0 bytes read, got {readAwaitable._numBytes}"); + break; + case Interop.Errors.ERROR_OPERATION_ABORTED: // canceled + throw new OperationCanceledException(cancellationToken.IsCancellationRequested ? cancellationToken : new CancellationToken(true)); + default: // error + throw Win32Marshal.GetExceptionForWin32Error((int)readAwaitable._errorCode); + } + + // Successful operation. If we got zero bytes, we're done: exit the read/write loop. + int numBytesRead = (int)readAwaitable._numBytes; + if (numBytesRead == 0) + { + break; + } + + // Otherwise, update the read position for next time accordingly. + if (canSeek) + { + readAwaitable._position += numBytesRead; + } + + // (and keep track of the maximum number of bytes in the buffer we used, to avoid excessive and unnecessary + // clearing of the buffer before we return it to the pool) + if (numBytesRead > bufferSize) + { + bufferSize = numBytesRead; + } + } + finally + { + // Free the resources for this read operation + unsafe + { + NativeOverlapped* overlapped; + lock (readAwaitable.CancellationLock) // just an Exchange, but we need this to be synchronized with cancellation, so using the same lock + { + overlapped = readAwaitable._nativeOverlapped; + readAwaitable._nativeOverlapped = null; + } + if (overlapped != null) + { + _fileHandle.ThreadPoolBinding.FreeNativeOverlapped(overlapped); + } + } + } + + // Write out the read data. + await destination.WriteAsync(copyBuffer, 0, (int)readAwaitable._numBytes, cancellationToken).ConfigureAwait(false); + } + } + finally + { + // Cleanup from the whole copy operation + cancellationReg.Dispose(); + awaitableOverlapped.Dispose(); + + Array.Clear(copyBuffer, 0, bufferSize); + ArrayPool<byte>.Shared.Return(copyBuffer, clearArray: false); + + // Make sure the stream's current position reflects where we ended up + if (!_fileHandle.IsClosed && CanSeek) + { + SeekCore(0, SeekOrigin.End); + } + } + } + + /// <summary>Used by CopyToAsync to enable awaiting the result of an overlapped I/O operation with minimal overhead.</summary> + private sealed unsafe class AsyncCopyToAwaitable : ICriticalNotifyCompletion + { + /// <summary>Sentinel object used to indicate that the I/O operation has completed before being awaited.</summary> + private readonly static Action s_sentinel = () => { }; + /// <summary>Cached delegate to IOCallback.</summary> + internal static readonly IOCompletionCallback s_callback = IOCallback; + + /// <summary>The FileStream that owns this instance.</summary> + internal readonly FileStream _fileStream; + + /// <summary>Tracked position representing the next location from which to read.</summary> + internal long _position; + /// <summary>The current native overlapped pointer. This changes for each operation.</summary> + internal NativeOverlapped* _nativeOverlapped; + /// <summary> + /// null if the operation is still in progress, + /// s_sentinel if the I/O operation completed before the await, + /// s_callback if it completed after the await yielded. + /// </summary> + internal Action _continuation; + /// <summary>Last error code from completed operation.</summary> + internal uint _errorCode; + /// <summary>Last number of read bytes from completed operation.</summary> + internal uint _numBytes; + + /// <summary>Lock object used to protect cancellation-related access to _nativeOverlapped.</summary> + internal object CancellationLock => this; + + /// <summary>Initialize the awaitable.</summary> + internal unsafe AsyncCopyToAwaitable(FileStream fileStream) + { + _fileStream = fileStream; + } + + /// <summary>Reset state to prepare for the next read operation.</summary> + internal void ResetForNextOperation() + { + Debug.Assert(_position >= 0, $"Expected non-negative position, got {_position}"); + _continuation = null; + _errorCode = 0; + _numBytes = 0; + } + + /// <summary>Overlapped callback: store the results, then invoke the continuation delegate.</summary> + internal unsafe static void IOCallback(uint errorCode, uint numBytes, NativeOverlapped* pOVERLAP) + { + var awaitable = (AsyncCopyToAwaitable)ThreadPoolBoundHandle.GetNativeOverlappedState(pOVERLAP); + + Debug.Assert(awaitable._continuation != s_sentinel, "Sentinel must not have already been set as the continuation"); + awaitable._errorCode = errorCode; + awaitable._numBytes = numBytes; + + (awaitable._continuation ?? Interlocked.CompareExchange(ref awaitable._continuation, s_sentinel, null))?.Invoke(); + } + + /// <summary> + /// Called when it's known that the I/O callback for an operation will not be invoked but we'll + /// still be awaiting the awaitable. + /// </summary> + internal void MarkCompleted() + { + Debug.Assert(_continuation == null, "Expected null continuation"); + _continuation = s_sentinel; + } + + public AsyncCopyToAwaitable GetAwaiter() => this; + public bool IsCompleted => _continuation == s_sentinel; + public void GetResult() { } + public void OnCompleted(Action continuation) => UnsafeOnCompleted(continuation); + public void UnsafeOnCompleted(Action continuation) + { + if (_continuation == s_sentinel || + Interlocked.CompareExchange(ref _continuation, continuation, null) != null) + { + Debug.Assert(_continuation == s_sentinel, $"Expected continuation set to s_sentinel, got ${_continuation}"); + Task.Run(continuation); + } + } + } + + // Unlike Flush(), FlushAsync() always flushes to disk. This is intentional. + // Legend is that we chose not to flush the OS file buffers in Flush() in fear of + // perf problems with frequent, long running FlushFileBuffers() calls. But we don't + // have that problem with FlushAsync() because we will call FlushFileBuffers() in the background. + private Task FlushAsyncInternal(CancellationToken cancellationToken) + { + if (cancellationToken.IsCancellationRequested) + return Task.FromCanceled(cancellationToken); + + if (_fileHandle.IsClosed) + throw Error.GetFileNotOpen(); + + // The always synchronous data transfer between the OS and the internal buffer is intentional + // because this is needed to allow concurrent async IO requests. Concurrent data transfer + // between the OS and the internal buffer will result in race conditions. Since FlushWrite and + // FlushRead modify internal state of the stream and transfer data between the OS and the + // internal buffer, they cannot be truly async. We will, however, flush the OS file buffers + // asynchronously because it doesn't modify any internal state of the stream and is potentially + // a long running process. + try + { + FlushInternalBuffer(); + } + catch (Exception e) + { + return Task.FromException(e); + } + + if (CanWrite) + { + return Task.Factory.StartNew( + state => ((FileStream)state).FlushOSBuffer(), + this, + cancellationToken, + TaskCreationOptions.DenyChildAttach, + TaskScheduler.Default); + } + else + { + return Task.CompletedTask; + } + } + + private Task<int> TaskFromResultOrCache(int result) + { + Task<int> completedTask = _lastSynchronouslyCompletedTask; + Debug.Assert(completedTask == null || completedTask.Status == TaskStatus.RanToCompletion, "Cached task should have completed successfully"); + + if ((completedTask == null) || (completedTask.Result != result)) + { + completedTask = Task.FromResult(result); + _lastSynchronouslyCompletedTask = completedTask; + } + + return completedTask; + } + + private void LockInternal(long position, long length) + { + int positionLow = unchecked((int)(position)); + int positionHigh = unchecked((int)(position >> 32)); + int lengthLow = unchecked((int)(length)); + int lengthHigh = unchecked((int)(length >> 32)); + + if (!Interop.Kernel32.LockFile(_fileHandle, positionLow, positionHigh, lengthLow, lengthHigh)) + { + throw Win32Marshal.GetExceptionForLastWin32Error(); + } + } + + private void UnlockInternal(long position, long length) + { + int positionLow = unchecked((int)(position)); + int positionHigh = unchecked((int)(position >> 32)); + int lengthLow = unchecked((int)(length)); + int lengthHigh = unchecked((int)(length >> 32)); + + if (!Interop.Kernel32.UnlockFile(_fileHandle, positionLow, positionHigh, lengthLow, lengthHigh)) + { + throw Win32Marshal.GetExceptionForLastWin32Error(); + } + } + } +} diff --git a/src/mscorlib/shared/System/IO/FileStream.cs b/src/mscorlib/shared/System/IO/FileStream.cs new file mode 100644 index 0000000000..7db8518435 --- /dev/null +++ b/src/mscorlib/shared/System/IO/FileStream.cs @@ -0,0 +1,684 @@ +// 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; + + /// <summary>Whether the file is opened for reading, writing, or both.</summary> + private readonly FileAccess _access; + + /// <summary>The path to the opened file.</summary> + private readonly string _path; + + /// <summary>The next available byte to be read from the _buffer.</summary> + private int _readPos; + + /// <summary>The number of valid bytes in _buffer.</summary> + private int _readLength; + + /// <summary>The next location in which a write should occur to the buffer.</summary> + private int _writePos; + + /// <summary> + /// 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. + /// </summary> + private readonly bool _useAsyncIO; + + /// <summary> + /// 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. + /// </summary> + private long _filePosition; + + /// <summary>Whether the file stream's handle has been exposed.</summary> + 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<int> 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<int>(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); + } + + /// <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 (IsClosed) throw Error.GetFileNotOpen(); + + FlushInternalBuffer(); + + if (flushToDisk && CanWrite) + { + FlushOSBuffer(); + } + } + + /// <summary>Gets a value indicating whether the current stream supports reading.</summary> + public override bool CanRead + { + get { return !_fileHandle.IsClosed && (_access & FileAccess.Read) != 0; } + } + + /// <summary>Gets a value indicating whether the current stream supports writing.</summary> + public override bool CanWrite + { + get { return !_fileHandle.IsClosed && (_access & FileAccess.Write) != 0; } + } + + /// <summary>Validates arguments to Read and Write and throws resulting exceptions.</summary> + /// <param name="array">The buffer to read from or write to.</param> + /// <param name="offset">The zero-based offset into the array.</param> + /// <param name="count">The maximum number of bytes to read or write.</param> + 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(); + } + + /// <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) + 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; + } + } + + /// <summary>Gets the path that was passed to the constructor.</summary> + public virtual string Name { get { return _path ?? SR.IO_UnknownFileName; } } + + /// <summary>Gets a value indicating whether the stream was opened for I/O to be performed synchronously or asynchronously.</summary> + public virtual bool IsAsync + { + get { return _useAsyncIO; } + } + + /// <summary>Gets the length of the stream in bytes.</summary> + public override long Length + { + get + { + if (_fileHandle.IsClosed) throw Error.GetFileNotOpen(); + if (!CanSeek) throw Error.GetSeekNotSupported(); + return GetLengthInternal(); + } + } + + /// <summary> + /// 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. + /// </summary> + 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); + } + } + } + } + + /// <summary>Verifies that state relating to the read/write buffer is consistent.</summary> + [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); + } + + /// <summary>Validates that we're ready to read from the stream.</summary> + private void PrepareForReading() + { + if (_fileHandle.IsClosed) + throw Error.GetFileNotOpen(); + if (_readLength == 0 && !CanRead) + throw Error.GetReadNotSupported(); + + AssertBufferInvariants(); + } + + /// <summary>Gets or sets the position within the current stream</summary> + 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; + + /// <summary> + /// Gets the array used for buffering reading and writing. + /// If the array hasn't been allocated, this will lazily allocate it. + /// </summary> + /// <returns>The buffer.</returns> + private byte[] GetBuffer() + { + Debug.Assert(_buffer == null || _buffer.Length == _bufferLength); + if (_buffer == null) + { + _buffer = new byte[_bufferLength]; + OnBufferAllocated(); + } + + return _buffer; + } + + partial void OnBufferAllocated(); + + /// <summary> + /// 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. + /// </summary> + private void FlushInternalBuffer() + { + AssertBufferInvariants(); + if (_writePos > 0) + { + FlushWriteBuffer(); + } + else if (_readPos < _readLength && CanSeek) + { + FlushReadBuffer(); + } + } + + /// <summary>Dumps any read data in the buffer and rewinds our position in the stream, accordingly, as necessary.</summary> + 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; + } + + /// <summary> + /// Validates that we're ready to write to the stream, + /// including flushing a read buffer if necessary. + /// </summary> + 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<int>(asyncResult); + } + + public override void EndWrite(IAsyncResult asyncResult) + { + if (asyncResult == null) + throw new ArgumentNullException(nameof(asyncResult)); + + if (!IsAsync) + base.EndWrite(asyncResult); + else + TaskToApm.End(asyncResult); + } + } +} diff --git a/src/mscorlib/shared/System/IO/FileStreamCompletionSource.Win32.cs b/src/mscorlib/shared/System/IO/FileStreamCompletionSource.Win32.cs new file mode 100644 index 0000000000..7dca13335e --- /dev/null +++ b/src/mscorlib/shared/System/IO/FileStreamCompletionSource.Win32.cs @@ -0,0 +1,222 @@ +// 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.Security; +using System.Threading; +using System.Threading.Tasks; +using System.Runtime.InteropServices; +using System.Diagnostics; + +namespace System.IO +{ + public partial class FileStream : Stream + { + // This is an internal object extending TaskCompletionSource with fields + // for all of the relevant data necessary to complete the IO operation. + // This is used by IOCallback and all of the async methods. + unsafe private sealed class FileStreamCompletionSource : TaskCompletionSource<int> + { + private const long NoResult = 0; + private const long ResultSuccess = (long)1 << 32; + private const long ResultError = (long)2 << 32; + private const long RegisteringCancellation = (long)4 << 32; + private const long CompletedCallback = (long)8 << 32; + private const ulong ResultMask = ((ulong)uint.MaxValue) << 32; + + private static Action<object> s_cancelCallback; + + private readonly FileStream _stream; + private readonly int _numBufferedBytes; + private readonly CancellationToken _cancellationToken; + private CancellationTokenRegistration _cancellationRegistration; +#if DEBUG + private bool _cancellationHasBeenRegistered; +#endif + private NativeOverlapped* _overlapped; // Overlapped class responsible for operations in progress when an appdomain unload occurs + private long _result; // Using long since this needs to be used in Interlocked APIs + + // Using RunContinuationsAsynchronously for compat reasons (old API used Task.Factory.StartNew for continuations) + internal FileStreamCompletionSource(FileStream stream, int numBufferedBytes, byte[] bytes, CancellationToken cancellationToken) + : base(TaskCreationOptions.RunContinuationsAsynchronously) + { + _numBufferedBytes = numBufferedBytes; + _stream = stream; + _result = NoResult; + _cancellationToken = cancellationToken; + + // Create the native overlapped. We try to use the preallocated overlapped if possible: + // it's possible if the byte buffer is the same one that's associated with the preallocated overlapped + // and if no one else is currently using the preallocated overlapped. This is the fast-path for cases + // where the user-provided buffer is smaller than the FileStream's buffer (such that the FileStream's + // buffer is used) and where operations on the FileStream are not being performed concurrently. + _overlapped = ReferenceEquals(bytes, _stream._buffer) && _stream.CompareExchangeCurrentOverlappedOwner(this, null) == null ? + _stream._fileHandle.ThreadPoolBinding.AllocateNativeOverlapped(_stream._preallocatedOverlapped) : + _stream._fileHandle.ThreadPoolBinding.AllocateNativeOverlapped(s_ioCallback, this, bytes); + Debug.Assert(_overlapped != null, "AllocateNativeOverlapped returned null"); + } + + internal NativeOverlapped* Overlapped + { + get { return _overlapped; } + } + + public void SetCompletedSynchronously(int numBytes) + { + ReleaseNativeResource(); + TrySetResult(numBytes + _numBufferedBytes); + } + + public void RegisterForCancellation() + { +#if DEBUG + Debug.Assert(!_cancellationHasBeenRegistered, "Cannot register for cancellation twice"); + _cancellationHasBeenRegistered = true; +#endif + + // Quick check to make sure that the cancellation token supports cancellation, and that the IO hasn't completed + if ((_cancellationToken.CanBeCanceled) && (_overlapped != null)) + { + var cancelCallback = s_cancelCallback; + if (cancelCallback == null) s_cancelCallback = cancelCallback = Cancel; + + // Register the cancellation only if the IO hasn't completed + long packedResult = Interlocked.CompareExchange(ref _result, RegisteringCancellation, NoResult); + if (packedResult == NoResult) + { + _cancellationRegistration = _cancellationToken.Register(cancelCallback, this); + + // Switch the result, just in case IO completed while we were setting the registration + packedResult = Interlocked.Exchange(ref _result, NoResult); + } + else if (packedResult != CompletedCallback) + { + // Failed to set the result, IO is in the process of completing + // Attempt to take the packed result + packedResult = Interlocked.Exchange(ref _result, NoResult); + } + + // If we have a callback that needs to be completed + if ((packedResult != NoResult) && (packedResult != CompletedCallback) && (packedResult != RegisteringCancellation)) + { + CompleteCallback((ulong)packedResult); + } + } + } + + internal void ReleaseNativeResource() + { + // Ensure that cancellation has been completed and cleaned up. + _cancellationRegistration.Dispose(); + + // Free the overlapped. + // NOTE: The cancellation must *NOT* be running at this point, or it may observe freed memory + // (this is why we disposed the registration above). + if (_overlapped != null) + { + _stream._fileHandle.ThreadPoolBinding.FreeNativeOverlapped(_overlapped); + _overlapped = null; + } + + // Ensure we're no longer set as the current completion source (we may not have been to begin with). + // Only one operation at a time is eligible to use the preallocated overlapped, + _stream.CompareExchangeCurrentOverlappedOwner(null, this); + } + + // When doing IO asynchronously (i.e. _isAsync==true), this callback is + // called by a free thread in the threadpool when the IO operation + // completes. + internal static unsafe void IOCallback(uint errorCode, uint numBytes, NativeOverlapped* pOverlapped) + { + // Extract the completion source from the overlapped. The state in the overlapped + // will either be a Win32FileStream (in the case where the preallocated overlapped was used), + // in which case the operation being completed is its _currentOverlappedOwner, or it'll + // be directly the FileStreamCompletion that's completing (in the case where the preallocated + // overlapped was already in use by another operation). + object state = ThreadPoolBoundHandle.GetNativeOverlappedState(pOverlapped); + FileStream fs = state as FileStream; + FileStreamCompletionSource completionSource = fs != null ? + fs._currentOverlappedOwner : + (FileStreamCompletionSource)state; + Debug.Assert(completionSource._overlapped == pOverlapped, "Overlaps don't match"); + + // Handle reading from & writing to closed pipes. While I'm not sure + // this is entirely necessary anymore, maybe it's possible for + // an async read on a pipe to be issued and then the pipe is closed, + // returning this error. This may very well be necessary. + ulong packedResult; + if (errorCode != 0 && errorCode != ERROR_BROKEN_PIPE && errorCode != ERROR_NO_DATA) + { + packedResult = ((ulong)ResultError | errorCode); + } + else + { + packedResult = ((ulong)ResultSuccess | numBytes); + } + + // Stow the result so that other threads can observe it + // And, if no other thread is registering cancellation, continue + if (NoResult == Interlocked.Exchange(ref completionSource._result, (long)packedResult)) + { + // Successfully set the state, attempt to take back the callback + if (Interlocked.Exchange(ref completionSource._result, CompletedCallback) != NoResult) + { + // Successfully got the callback, finish the callback + completionSource.CompleteCallback(packedResult); + } + // else: Some other thread stole the result, so now it is responsible to finish the callback + } + // else: Some other thread is registering a cancellation, so it *must* finish the callback + } + + private void CompleteCallback(ulong packedResult) + { + // Free up the native resource and cancellation registration + ReleaseNativeResource(); + + // Unpack the result and send it to the user + long result = (long)(packedResult & ResultMask); + if (result == ResultError) + { + int errorCode = unchecked((int)(packedResult & uint.MaxValue)); + if (errorCode == Interop.Errors.ERROR_OPERATION_ABORTED) + { + TrySetCanceled(_cancellationToken.IsCancellationRequested ? _cancellationToken : new CancellationToken(true)); + } + else + { + TrySetException(Win32Marshal.GetExceptionForWin32Error(errorCode)); + } + } + else + { + Debug.Assert(result == ResultSuccess, "Unknown result"); + TrySetResult((int)(packedResult & uint.MaxValue) + _numBufferedBytes); + } + } + + private static void Cancel(object state) + { + // WARNING: This may potentially be called under a lock (during cancellation registration) + + FileStreamCompletionSource completionSource = state as FileStreamCompletionSource; + Debug.Assert(completionSource != null, "Unknown state passed to cancellation"); + Debug.Assert(completionSource._overlapped != null && !completionSource.Task.IsCompleted, "IO should not have completed yet"); + + // If the handle is still valid, attempt to cancel the IO + if (!completionSource._stream._fileHandle.IsInvalid && + !Interop.Kernel32.CancelIoEx(completionSource._stream._fileHandle, completionSource._overlapped)) + { + int errorCode = Marshal.GetLastWin32Error(); + + // ERROR_NOT_FOUND is returned if CancelIoEx cannot find the request to cancel. + // This probably means that the IO operation has completed. + if (errorCode != Interop.Errors.ERROR_NOT_FOUND) + { + throw Win32Marshal.GetExceptionForWin32Error(errorCode); + } + } + } + } + } +} diff --git a/src/mscorlib/shared/System/IO/Path.Unix.cs b/src/mscorlib/shared/System/IO/Path.Unix.cs new file mode 100644 index 0000000000..500c60aa8c --- /dev/null +++ b/src/mscorlib/shared/System/IO/Path.Unix.cs @@ -0,0 +1,215 @@ +// 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.Diagnostics; +using System.Runtime.InteropServices; +using System.Text; + +namespace System.IO +{ + public static partial class Path + { + public static char[] GetInvalidFileNameChars() => new char[] { '\0', '/' }; + + public static char[] GetInvalidPathChars() => new char[] { '\0' }; + + internal static int MaxPath => Interop.Sys.MaxPath; + + // Expands the given path to a fully qualified path. + public static string GetFullPath(string path) + { + if (path == null) + throw new ArgumentNullException(nameof(path)); + + if (path.Length == 0) + throw new ArgumentException(SR.Arg_PathIllegal); + + PathInternal.CheckInvalidPathChars(path); + + // Expand with current directory if necessary + if (!IsPathRooted(path)) + { + path = Combine(Interop.Sys.GetCwd(), path); + } + + // We would ideally use realpath to do this, but it resolves symlinks, requires that the file actually exist, + // and turns it into a full path, which we only want if fullCheck is true. + string collapsedString = RemoveRelativeSegments(path); + + Debug.Assert(collapsedString.Length < path.Length || collapsedString.ToString() == path, + "Either we've removed characters, or the string should be unmodified from the input path."); + + if (collapsedString.Length > Interop.Sys.MaxPath) + { + throw new PathTooLongException(SR.IO_PathTooLong); + } + + string result = collapsedString.Length == 0 ? PathInternal.DirectorySeparatorCharAsString : collapsedString; + + return result; + } + + /// <summary> + /// Try to remove relative segments from the given path (without combining with a root). + /// </summary> + /// <param name="skip">Skip the specified number of characters before evaluating.</param> + private static string RemoveRelativeSegments(string path, int skip = 0) + { + bool flippedSeparator = false; + + // Remove "//", "/./", and "/../" from the path by copying each character to the output, + // except the ones we're removing, such that the builder contains the normalized path + // at the end. + var sb = StringBuilderCache.Acquire(path.Length); + if (skip > 0) + { + sb.Append(path, 0, skip); + } + + int componentCharCount = 0; + for (int i = skip; i < path.Length; i++) + { + char c = path[i]; + + if (PathInternal.IsDirectorySeparator(c) && i + 1 < path.Length) + { + componentCharCount = 0; + + // Skip this character if it's a directory separator and if the next character is, too, + // e.g. "parent//child" => "parent/child" + if (PathInternal.IsDirectorySeparator(path[i + 1])) + { + continue; + } + + // Skip this character and the next if it's referring to the current directory, + // e.g. "parent/./child" =? "parent/child" + if ((i + 2 == path.Length || PathInternal.IsDirectorySeparator(path[i + 2])) && + path[i + 1] == '.') + { + i++; + continue; + } + + // Skip this character and the next two if it's referring to the parent directory, + // e.g. "parent/child/../grandchild" => "parent/grandchild" + if (i + 2 < path.Length && + (i + 3 == path.Length || PathInternal.IsDirectorySeparator(path[i + 3])) && + path[i + 1] == '.' && path[i + 2] == '.') + { + // Unwind back to the last slash (and if there isn't one, clear out everything). + int s; + for (s = sb.Length - 1; s >= 0; s--) + { + if (PathInternal.IsDirectorySeparator(sb[s])) + { + sb.Length = s; + break; + } + } + if (s < 0) + { + sb.Length = 0; + } + + i += 2; + continue; + } + } + + if (++componentCharCount > Interop.Sys.MaxName) + { + throw new PathTooLongException(SR.IO_PathTooLong); + } + + // Normalize the directory separator if needed + if (c != PathInternal.DirectorySeparatorChar && c == PathInternal.AltDirectorySeparatorChar) + { + c = PathInternal.DirectorySeparatorChar; + flippedSeparator = true; + } + + sb.Append(c); + } + + if (flippedSeparator || sb.Length != path.Length) + { + return StringBuilderCache.GetStringAndRelease(sb); + } + else + { + // We haven't changed the source path, return the original + StringBuilderCache.Release(sb); + return path; + } + } + + private static string RemoveLongPathPrefix(string path) + { + return path; // nop. There's nothing special about "long" paths on Unix. + } + + public static string GetTempPath() + { + const string TempEnvVar = "TMPDIR"; + const string DefaultTempPath = "/tmp/"; + + // Get the temp path from the TMPDIR environment variable. + // If it's not set, just return the default path. + // If it is, return it, ensuring it ends with a slash. + string path = Environment.GetEnvironmentVariable(TempEnvVar); + return + string.IsNullOrEmpty(path) ? DefaultTempPath : + PathInternal.IsDirectorySeparator(path[path.Length - 1]) ? path : + path + PathInternal.DirectorySeparatorChar; + } + + public static string GetTempFileName() + { + const string Suffix = ".tmp"; + const int SuffixByteLength = 4; + + // mkstemps takes a char* and overwrites the XXXXXX with six characters + // that'll result in a unique file name. + string template = GetTempPath() + "tmpXXXXXX" + Suffix + "\0"; + byte[] name = Encoding.UTF8.GetBytes(template); + + // Create, open, and close the temp file. + IntPtr fd = Interop.CheckIo(Interop.Sys.MksTemps(name, SuffixByteLength)); + Interop.Sys.Close(fd); // ignore any errors from close; nothing to do if cleanup isn't possible + + // 'name' is now the name of the file + Debug.Assert(name[name.Length - 1] == '\0'); + return Encoding.UTF8.GetString(name, 0, name.Length - 1); // trim off the trailing '\0' + } + + public static bool IsPathRooted(string path) + { + if (path == null) + return false; + + PathInternal.CheckInvalidPathChars(path); + return path.Length > 0 && path[0] == PathInternal.DirectorySeparatorChar; + } + + public static string GetPathRoot(string path) + { + if (path == null) return null; + return IsPathRooted(path) ? PathInternal.DirectorySeparatorCharAsString : String.Empty; + } + + /// <summary>Gets whether the system is case-sensitive.</summary> + internal static bool IsCaseSensitive + { + get + { + #if PLATFORM_OSX + return false; + #else + return true; + #endif + } + } + } +} diff --git a/src/mscorlib/shared/System/IO/Path.Windows.cs b/src/mscorlib/shared/System/IO/Path.Windows.cs new file mode 100644 index 0000000000..d6f0c628c3 --- /dev/null +++ b/src/mscorlib/shared/System/IO/Path.Windows.cs @@ -0,0 +1,155 @@ +// 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.Diagnostics; +using System.Text; + +namespace System.IO +{ + public static partial class Path + { + public static char[] GetInvalidFileNameChars() => new char[] + { + '\"', '<', '>', '|', '\0', + (char)1, (char)2, (char)3, (char)4, (char)5, (char)6, (char)7, (char)8, (char)9, (char)10, + (char)11, (char)12, (char)13, (char)14, (char)15, (char)16, (char)17, (char)18, (char)19, (char)20, + (char)21, (char)22, (char)23, (char)24, (char)25, (char)26, (char)27, (char)28, (char)29, (char)30, + (char)31, ':', '*', '?', '\\', '/' + }; + + public static char[] GetInvalidPathChars() => new char[] + { + '|', '\0', + (char)1, (char)2, (char)3, (char)4, (char)5, (char)6, (char)7, (char)8, (char)9, (char)10, + (char)11, (char)12, (char)13, (char)14, (char)15, (char)16, (char)17, (char)18, (char)19, (char)20, + (char)21, (char)22, (char)23, (char)24, (char)25, (char)26, (char)27, (char)28, (char)29, (char)30, + (char)31 + }; + + // The max total path is 260, and the max individual component length is 255. + // For example, D:\<256 char file name> isn't legal, even though it's under 260 chars. + internal const int MaxPath = 260; + + // Expands the given path to a fully qualified path. + public static string GetFullPath(string path) + { + if (path == null) + throw new ArgumentNullException(nameof(path)); + + // Embedded null characters are the only invalid character case we want to check up front. + // This is because the nulls will signal the end of the string to Win32 and therefore have + // unpredictable results. Other invalid characters we give a chance to be normalized out. + if (path.IndexOf('\0') != -1) + throw new ArgumentException(SR.Argument_InvalidPathChars, nameof(path)); + + if (PathInternal.IsExtended(path)) + { + // We can't really know what is valid for all cases of extended paths. + // + // - object names can include other characters as well (':', '/', etc.) + // - even file objects have different rules (pipe names can contain most characters) + // + // As such we will do no further analysis of extended paths to avoid blocking known and unknown + // scenarios as well as minimizing compat breaks should we block now and need to unblock later. + return path; + } + + bool isDevice = PathInternal.IsDevice(path); + if (!isDevice) + { + // Toss out paths with colons that aren't a valid drive specifier. + // Cannot start with a colon and can only be of the form "C:". + // (Note that we used to explicitly check "http:" and "file:"- these are caught by this check now.) + int startIndex = PathInternal.PathStartSkip(path); + + // Move past the colon + startIndex += 2; + + if ((path.Length > 0 && path[0] == PathInternal.VolumeSeparatorChar) + || (path.Length >= startIndex && path[startIndex - 1] == PathInternal.VolumeSeparatorChar && !PathInternal.IsValidDriveChar(path[startIndex - 2])) + || (path.Length > startIndex && path.IndexOf(PathInternal.VolumeSeparatorChar, startIndex) != -1)) + { + throw new NotSupportedException(SR.Argument_PathFormatNotSupported); + } + } + + // Technically this doesn't matter but we used to throw for this case + if (string.IsNullOrWhiteSpace(path)) + throw new ArgumentException(SR.Arg_PathIllegal); + + // We don't want to check invalid characters for device format- see comments for extended above + string fullPath = PathHelper.Normalize(path, checkInvalidCharacters: !isDevice, expandShortPaths: true); + + if (!isDevice) + { + // Emulate FileIOPermissions checks, retained for compatibility (normal invalid characters have already been checked) + if (PathInternal.HasWildCardCharacters(fullPath)) + throw new ArgumentException(SR.Argument_InvalidPathChars, nameof(path)); + } + + return fullPath; + } + + public static string GetTempPath() + { + StringBuilder sb = StringBuilderCache.Acquire(MaxPath); + uint r = Interop.Kernel32.GetTempPathW(MaxPath, sb); + if (r == 0) + throw Win32Marshal.GetExceptionForLastWin32Error(); + return GetFullPath(StringBuilderCache.GetStringAndRelease(sb)); + } + + // Returns a unique temporary file name, and creates a 0-byte file by that + // name on disk. + public static string GetTempFileName() + { + string path = GetTempPath(); + + StringBuilder sb = StringBuilderCache.Acquire(MaxPath); + uint r = Interop.Kernel32.GetTempFileNameW(path, "tmp", 0, sb); + if (r == 0) + throw Win32Marshal.GetExceptionForLastWin32Error(); + return StringBuilderCache.GetStringAndRelease(sb); + } + + // Tests if the given path contains a root. A path is considered rooted + // if it starts with a backslash ("\") or a valid drive letter and a colon (":"). + public static bool IsPathRooted(string path) + { + if (path != null) + { + PathInternal.CheckInvalidPathChars(path); + + int length = path.Length; + if ((length >= 1 && PathInternal.IsDirectorySeparator(path[0])) || + (length >= 2 && PathInternal.IsValidDriveChar(path[0]) && path[1] == PathInternal.VolumeSeparatorChar)) + return true; + } + return false; + } + + // Returns the root portion of the given path. The resulting string + // consists of those rightmost characters of the path that constitute the + // root of the path. Possible patterns for the resulting string are: An + // empty string (a relative path on the current drive), "\" (an absolute + // path on the current drive), "X:" (a relative path on a given drive, + // where X is the drive letter), "X:\" (an absolute path on a given drive), + // and "\\server\share" (a UNC path for a given server and share name). + // The resulting string is null if path is null. + public static string GetPathRoot(string path) + { + if (path == null) return null; + PathInternal.CheckInvalidPathChars(path); + + // Need to return the normalized directory separator + path = PathInternal.NormalizeDirectorySeparators(path); + + int pathRoot = PathInternal.GetRootLength(path); + return pathRoot <= 0 ? string.Empty : path.Substring(0, pathRoot); + } + + /// <summary>Gets whether the system is case-sensitive.</summary> + internal static bool IsCaseSensitive { get { return false; } } + } +} diff --git a/src/mscorlib/shared/System/IO/Path.cs b/src/mscorlib/shared/System/IO/Path.cs new file mode 100644 index 0000000000..b3a8783c32 --- /dev/null +++ b/src/mscorlib/shared/System/IO/Path.cs @@ -0,0 +1,574 @@ +// 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.Diagnostics; +using System.Diagnostics.Contracts; +using System.Text; + +namespace System.IO +{ + // Provides methods for processing file system strings in a cross-platform manner. + // Most of the methods don't do a complete parsing (such as examining a UNC hostname), + // but they will handle most string operations. + public static partial class Path + { + // Public static readonly variant of the separators. The Path implementation itself is using + // internal const variant of the separators for better performance. + public static readonly char DirectorySeparatorChar = PathInternal.DirectorySeparatorChar; + public static readonly char AltDirectorySeparatorChar = PathInternal.AltDirectorySeparatorChar; + public static readonly char VolumeSeparatorChar = PathInternal.VolumeSeparatorChar; + public static readonly char PathSeparator = PathInternal.PathSeparator; + + // For generating random file names + // 8 random bytes provides 12 chars in our encoding for the 8.3 name. + private const int KeyLength = 8; + + [Obsolete("Please use GetInvalidPathChars or GetInvalidFileNameChars instead.")] + public static readonly char[] InvalidPathChars = GetInvalidPathChars(); + + // Changes the extension of a file path. The path parameter + // specifies a file path, and the extension parameter + // specifies a file extension (with a leading period, such as + // ".exe" or ".cs"). + // + // The function returns a file path with the same root, directory, and base + // name parts as path, but with the file extension changed to + // the specified extension. If path is null, the function + // returns null. If path does not contain a file extension, + // the new file extension is appended to the path. If extension + // is null, any existing extension is removed from path. + public static string ChangeExtension(string path, string extension) + { + if (path != null) + { + PathInternal.CheckInvalidPathChars(path); + + string s = path; + for (int i = path.Length - 1; i >= 0; i--) + { + char ch = path[i]; + if (ch == '.') + { + s = path.Substring(0, i); + break; + } + if (PathInternal.IsDirectoryOrVolumeSeparator(ch)) break; + } + + if (extension != null && path.Length != 0) + { + s = (extension.Length == 0 || extension[0] != '.') ? + s + "." + extension : + s + extension; + } + + return s; + } + return null; + } + + // Returns the directory path of a file path. This method effectively + // removes the last element of the given file path, i.e. it returns a + // string consisting of all characters up to but not including the last + // backslash ("\") in the file path. The returned value is null if the file + // path is null or if the file path denotes a root (such as "\", "C:", or + // "\\server\share"). + public static string GetDirectoryName(string path) + { + if (path != null) + { + PathInternal.CheckInvalidPathChars(path); + path = PathInternal.NormalizeDirectorySeparators(path); + int root = PathInternal.GetRootLength(path); + + int i = path.Length; + if (i > root) + { + while (i > root && !PathInternal.IsDirectorySeparator(path[--i])) ; + return path.Substring(0, i); + } + } + return null; + } + + // Returns the extension of the given path. The returned value includes the + // period (".") character of the extension except when you have a terminal period when you get string.Empty, such as ".exe" or + // ".cpp". The returned value is null if the given path is + // null or if the given path does not include an extension. + [Pure] + public static string GetExtension(string path) + { + if (path == null) + return null; + + PathInternal.CheckInvalidPathChars(path); + int length = path.Length; + for (int i = length - 1; i >= 0; i--) + { + char ch = path[i]; + if (ch == '.') + { + if (i != length - 1) + return path.Substring(i, length - i); + else + return string.Empty; + } + if (PathInternal.IsDirectoryOrVolumeSeparator(ch)) + break; + } + return string.Empty; + } + + // Returns the name and extension parts of the given path. The resulting + // string contains the characters of path that follow the last + // separator in path. The resulting string is null if path is null. + [Pure] + public static string GetFileName(string path) + { + if (path == null) + return null; + + int offset = PathInternal.FindFileNameIndex(path); + int count = path.Length - offset; + return path.Substring(offset, count); + } + + [Pure] + public static string GetFileNameWithoutExtension(string path) + { + if (path == null) + return null; + + int length = path.Length; + int offset = PathInternal.FindFileNameIndex(path); + + int end = path.LastIndexOf('.', length - 1, length - offset); + return end == -1 ? + path.Substring(offset) : // No extension was found + path.Substring(offset, end - offset); + } + + // Returns a cryptographically strong random 8.3 string that can be + // used as either a folder name or a file name. + public static unsafe string GetRandomFileName() + { + byte* pKey = stackalloc byte[KeyLength]; + Interop.GetRandomBytes(pKey, KeyLength); + + const int RandomFileNameLength = 12; + char* pRandomFileName = stackalloc char[RandomFileNameLength]; + Populate83FileNameFromRandomBytes(pKey, KeyLength, pRandomFileName, RandomFileNameLength); + return new string(pRandomFileName, 0, RandomFileNameLength); + } + + // Tests if a path includes a file extension. The result is + // true if the characters that follow the last directory + // separator ('\\' or '/') or volume separator (':') in the path include + // a period (".") other than a terminal period. The result is false otherwise. + [Pure] + public static bool HasExtension(string path) + { + if (path != null) + { + PathInternal.CheckInvalidPathChars(path); + + for (int i = path.Length - 1; i >= 0; i--) + { + char ch = path[i]; + if (ch == '.') + { + return i != path.Length - 1; + } + if (PathInternal.IsDirectoryOrVolumeSeparator(ch)) break; + } + } + return false; + } + + public static string Combine(string path1, string path2) + { + if (path1 == null || path2 == null) + throw new ArgumentNullException((path1 == null) ? nameof(path1) : nameof(path2)); + Contract.EndContractBlock(); + + PathInternal.CheckInvalidPathChars(path1); + PathInternal.CheckInvalidPathChars(path2); + + return CombineNoChecks(path1, path2); + } + + public static string Combine(string path1, string path2, string path3) + { + if (path1 == null || path2 == null || path3 == null) + throw new ArgumentNullException((path1 == null) ? nameof(path1) : (path2 == null) ? nameof(path2) : nameof(path3)); + Contract.EndContractBlock(); + + PathInternal.CheckInvalidPathChars(path1); + PathInternal.CheckInvalidPathChars(path2); + PathInternal.CheckInvalidPathChars(path3); + + return CombineNoChecks(path1, path2, path3); + } + + public static string Combine(string path1, string path2, string path3, string path4) + { + if (path1 == null || path2 == null || path3 == null || path4 == null) + throw new ArgumentNullException((path1 == null) ? nameof(path1) : (path2 == null) ? nameof(path2) : (path3 == null) ? nameof(path3) : nameof(path4)); + Contract.EndContractBlock(); + + PathInternal.CheckInvalidPathChars(path1); + PathInternal.CheckInvalidPathChars(path2); + PathInternal.CheckInvalidPathChars(path3); + PathInternal.CheckInvalidPathChars(path4); + + return CombineNoChecks(path1, path2, path3, path4); + } + + public static string Combine(params string[] paths) + { + if (paths == null) + { + throw new ArgumentNullException(nameof(paths)); + } + Contract.EndContractBlock(); + + int finalSize = 0; + int firstComponent = 0; + + // We have two passes, the first calculates how large a buffer to allocate and does some precondition + // checks on the paths passed in. The second actually does the combination. + + for (int i = 0; i < paths.Length; i++) + { + if (paths[i] == null) + { + throw new ArgumentNullException(nameof(paths)); + } + + if (paths[i].Length == 0) + { + continue; + } + + PathInternal.CheckInvalidPathChars(paths[i]); + + if (IsPathRooted(paths[i])) + { + firstComponent = i; + finalSize = paths[i].Length; + } + else + { + finalSize += paths[i].Length; + } + + char ch = paths[i][paths[i].Length - 1]; + if (!PathInternal.IsDirectoryOrVolumeSeparator(ch)) + finalSize++; + } + + StringBuilder finalPath = StringBuilderCache.Acquire(finalSize); + + for (int i = firstComponent; i < paths.Length; i++) + { + if (paths[i].Length == 0) + { + continue; + } + + if (finalPath.Length == 0) + { + finalPath.Append(paths[i]); + } + else + { + char ch = finalPath[finalPath.Length - 1]; + if (!PathInternal.IsDirectoryOrVolumeSeparator(ch)) + { + finalPath.Append(PathInternal.DirectorySeparatorChar); + } + + finalPath.Append(paths[i]); + } + } + + return StringBuilderCache.GetStringAndRelease(finalPath); + } + + private static string CombineNoChecks(string path1, string path2) + { + if (path2.Length == 0) + return path1; + + if (path1.Length == 0) + return path2; + + if (IsPathRooted(path2)) + return path2; + + char ch = path1[path1.Length - 1]; + return PathInternal.IsDirectoryOrVolumeSeparator(ch) ? + path1 + path2 : + path1 + PathInternal.DirectorySeparatorCharAsString + path2; + } + + private static string CombineNoChecks(string path1, string path2, string path3) + { + if (path1.Length == 0) + return CombineNoChecks(path2, path3); + if (path2.Length == 0) + return CombineNoChecks(path1, path3); + if (path3.Length == 0) + return CombineNoChecks(path1, path2); + + if (IsPathRooted(path3)) + return path3; + if (IsPathRooted(path2)) + return CombineNoChecks(path2, path3); + + bool hasSep1 = PathInternal.IsDirectoryOrVolumeSeparator(path1[path1.Length - 1]); + bool hasSep2 = PathInternal.IsDirectoryOrVolumeSeparator(path2[path2.Length - 1]); + + if (hasSep1 && hasSep2) + { + return path1 + path2 + path3; + } + else if (hasSep1) + { + return path1 + path2 + PathInternal.DirectorySeparatorCharAsString + path3; + } + else if (hasSep2) + { + return path1 + PathInternal.DirectorySeparatorCharAsString + path2 + path3; + } + else + { + // string.Concat only has string-based overloads up to four arguments; after that requires allocating + // a params string[]. Instead, try to use a cached StringBuilder. + StringBuilder sb = StringBuilderCache.Acquire(path1.Length + path2.Length + path3.Length + 2); + sb.Append(path1) + .Append(PathInternal.DirectorySeparatorChar) + .Append(path2) + .Append(PathInternal.DirectorySeparatorChar) + .Append(path3); + return StringBuilderCache.GetStringAndRelease(sb); + } + } + + private static string CombineNoChecks(string path1, string path2, string path3, string path4) + { + if (path1.Length == 0) + return CombineNoChecks(path2, path3, path4); + if (path2.Length == 0) + return CombineNoChecks(path1, path3, path4); + if (path3.Length == 0) + return CombineNoChecks(path1, path2, path4); + if (path4.Length == 0) + return CombineNoChecks(path1, path2, path3); + + if (IsPathRooted(path4)) + return path4; + if (IsPathRooted(path3)) + return CombineNoChecks(path3, path4); + if (IsPathRooted(path2)) + return CombineNoChecks(path2, path3, path4); + + bool hasSep1 = PathInternal.IsDirectoryOrVolumeSeparator(path1[path1.Length - 1]); + bool hasSep2 = PathInternal.IsDirectoryOrVolumeSeparator(path2[path2.Length - 1]); + bool hasSep3 = PathInternal.IsDirectoryOrVolumeSeparator(path3[path3.Length - 1]); + + if (hasSep1 && hasSep2 && hasSep3) + { + // Use string.Concat overload that takes four strings + return path1 + path2 + path3 + path4; + } + else + { + // string.Concat only has string-based overloads up to four arguments; after that requires allocating + // a params string[]. Instead, try to use a cached StringBuilder. + StringBuilder sb = StringBuilderCache.Acquire(path1.Length + path2.Length + path3.Length + path4.Length + 3); + + sb.Append(path1); + if (!hasSep1) + { + sb.Append(PathInternal.DirectorySeparatorChar); + } + + sb.Append(path2); + if (!hasSep2) + { + sb.Append(PathInternal.DirectorySeparatorChar); + } + + sb.Append(path3); + if (!hasSep3) + { + sb.Append(PathInternal.DirectorySeparatorChar); + } + + sb.Append(path4); + + return StringBuilderCache.GetStringAndRelease(sb); + } + } + + private static readonly char[] s_base32Char = { + 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', + 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', + 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', + 'y', 'z', '0', '1', '2', '3', '4', '5'}; + + private static unsafe void Populate83FileNameFromRandomBytes(byte* bytes, int byteCount, char* chars, int charCount) + { + Debug.Assert(bytes != null); + Debug.Assert(chars != null); + + // This method requires bytes of length 8 and chars of length 12. + Debug.Assert(byteCount == 8, $"Unexpected {nameof(byteCount)}"); + Debug.Assert(charCount == 12, $"Unexpected {nameof(charCount)}"); + + byte b0 = bytes[0]; + byte b1 = bytes[1]; + byte b2 = bytes[2]; + byte b3 = bytes[3]; + byte b4 = bytes[4]; + + // Consume the 5 Least significant bits of the first 5 bytes + chars[0] = s_base32Char[b0 & 0x1F]; + chars[1] = s_base32Char[b1 & 0x1F]; + chars[2] = s_base32Char[b2 & 0x1F]; + chars[3] = s_base32Char[b3 & 0x1F]; + chars[4] = s_base32Char[b4 & 0x1F]; + + // Consume 3 MSB of b0, b1, MSB bits 6, 7 of b3, b4 + chars[5] = s_base32Char[( + ((b0 & 0xE0) >> 5) | + ((b3 & 0x60) >> 2))]; + + chars[6] = s_base32Char[( + ((b1 & 0xE0) >> 5) | + ((b4 & 0x60) >> 2))]; + + // Consume 3 MSB bits of b2, 1 MSB bit of b3, b4 + b2 >>= 5; + + Debug.Assert(((b2 & 0xF8) == 0), "Unexpected set bits"); + + if ((b3 & 0x80) != 0) + b2 |= 0x08; + if ((b4 & 0x80) != 0) + b2 |= 0x10; + + chars[7] = s_base32Char[b2]; + + // Set the file extension separator + chars[8] = '.'; + + // Consume the 5 Least significant bits of the remaining 3 bytes + chars[9] = s_base32Char[(bytes[5] & 0x1F)]; + chars[10] = s_base32Char[(bytes[6] & 0x1F)]; + chars[11] = s_base32Char[(bytes[7] & 0x1F)]; + } + + /// <summary> + /// Create a relative path from one path to another. Paths will be resolved before calculating the difference. + /// Default path comparison for the active platform will be used (OrdinalIgnoreCase for Windows or Mac, Ordinal for Unix). + /// </summary> + /// <param name="relativeTo">The source path the output should be relative to. This path is always considered to be a directory.</param> + /// <param name="path">The destination path.</param> + /// <returns>The relative path or <paramref name="path"/> if the paths don't share the same root.</returns> + /// <exception cref="ArgumentNullException">Thrown if <paramref name="relativeTo"/> or <paramref name="path"/> is <c>null</c> or an empty string.</exception> + public static string GetRelativePath(string relativeTo, string path) + { + return GetRelativePath(relativeTo, path, StringComparison); + } + + private static string GetRelativePath(string relativeTo, string path, StringComparison comparisonType) + { + if (string.IsNullOrEmpty(relativeTo)) throw new ArgumentNullException(nameof(relativeTo)); + if (string.IsNullOrWhiteSpace(path)) throw new ArgumentNullException(nameof(path)); + Debug.Assert(comparisonType == StringComparison.Ordinal || comparisonType == StringComparison.OrdinalIgnoreCase); + + relativeTo = GetFullPath(relativeTo); + path = GetFullPath(path); + + // Need to check if the roots are different- if they are we need to return the "to" path. + if (!PathInternal.AreRootsEqual(relativeTo, path, comparisonType)) + return path; + + int commonLength = PathInternal.GetCommonPathLength(relativeTo, path, ignoreCase: comparisonType == StringComparison.OrdinalIgnoreCase); + + // If there is nothing in common they can't share the same root, return the "to" path as is. + if (commonLength == 0) + return path; + + // Trailing separators aren't significant for comparison + int relativeToLength = relativeTo.Length; + if (PathInternal.EndsInDirectorySeparator(relativeTo)) + relativeToLength--; + + bool pathEndsInSeparator = PathInternal.EndsInDirectorySeparator(path); + int pathLength = path.Length; + if (pathEndsInSeparator) + pathLength--; + + // If we have effectively the same path, return "." + if (relativeToLength == pathLength && commonLength >= relativeToLength) return "."; + + // We have the same root, we need to calculate the difference now using the + // common Length and Segment count past the length. + // + // Some examples: + // + // C:\Foo C:\Bar L3, S1 -> ..\Bar + // C:\Foo C:\Foo\Bar L6, S0 -> Bar + // C:\Foo\Bar C:\Bar\Bar L3, S2 -> ..\..\Bar\Bar + // C:\Foo\Foo C:\Foo\Bar L7, S1 -> ..\Bar + + StringBuilder sb = StringBuilderCache.Acquire(Math.Max(relativeTo.Length, path.Length)); + + // Add parent segments for segments past the common on the "from" path + if (commonLength < relativeToLength) + { + sb.Append(PathInternal.ParentDirectoryPrefix); + + for (int i = commonLength; i < relativeToLength; i++) + { + if (PathInternal.IsDirectorySeparator(relativeTo[i])) + { + sb.Append(PathInternal.ParentDirectoryPrefix); + } + } + } + else if (PathInternal.IsDirectorySeparator(path[commonLength])) + { + // No parent segments and we need to eat the initial separator + // (C:\Foo C:\Foo\Bar case) + commonLength++; + } + + // Now add the rest of the "to" path, adding back the trailing separator + int count = pathLength - commonLength; + if (pathEndsInSeparator) + count++; + + sb.Append(path, commonLength, count); + return StringBuilderCache.GetStringAndRelease(sb); + } + + // StringComparison and IsCaseSensitive are also available in PathInternal.CaseSensitivity but we are + // too low in System.Runtime.Extensions to use it (no FileStream, etc.) + + /// <summary>Returns a comparison that can be used to compare file and directory names for equality.</summary> + internal static StringComparison StringComparison + { + get + { + return IsCaseSensitive ? + StringComparison.Ordinal : + StringComparison.OrdinalIgnoreCase; + } + } + } +} diff --git a/src/mscorlib/shared/System/IO/PathHelper.Windows.cs b/src/mscorlib/shared/System/IO/PathHelper.Windows.cs new file mode 100644 index 0000000000..e2ead93185 --- /dev/null +++ b/src/mscorlib/shared/System/IO/PathHelper.Windows.cs @@ -0,0 +1,398 @@ +// 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.Diagnostics; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +namespace System.IO +{ + /// <summary> + /// Wrapper to help with path normalization. + /// </summary> + internal class PathHelper + { + // Can't be over 8.3 and be a short name + private const int MaxShortName = 12; + + private const char LastAnsi = (char)255; + private const char Delete = (char)127; + + /// <summary> + /// Normalize the given path. + /// </summary> + /// <remarks> + /// Normalizes via Win32 GetFullPathName(). It will also trim all "typical" whitespace at the end of the path (see s_trimEndChars). Will also trim initial + /// spaces if the path is determined to be rooted. + /// + /// Note that invalid characters will be checked after the path is normalized, which could remove bad characters. (C:\|\..\a.txt -- C:\a.txt) + /// </remarks> + /// <param name="path">Path to normalize</param> + /// <param name="checkInvalidCharacters">True to check for invalid characters</param> + /// <param name="expandShortPaths">Attempt to expand short paths if true</param> + /// <exception cref="ArgumentException">Thrown if the path is an illegal UNC (does not contain a full server/share) or contains illegal characters.</exception> + /// <exception cref="PathTooLongException">Thrown if the path or a path segment exceeds the filesystem limits.</exception> + /// <exception cref="FileNotFoundException">Thrown if Windows returns ERROR_FILE_NOT_FOUND. (See Win32Marshal.GetExceptionForWin32Error)</exception> + /// <exception cref="DirectoryNotFoundException">Thrown if Windows returns ERROR_PATH_NOT_FOUND. (See Win32Marshal.GetExceptionForWin32Error)</exception> + /// <exception cref="UnauthorizedAccessException">Thrown if Windows returns ERROR_ACCESS_DENIED. (See Win32Marshal.GetExceptionForWin32Error)</exception> + /// <exception cref="IOException">Thrown if Windows returns an error that doesn't map to the above. (See Win32Marshal.GetExceptionForWin32Error)</exception> + /// <returns>Normalized path</returns> + internal static string Normalize(string path, bool checkInvalidCharacters, bool expandShortPaths) + { + // Get the full path + StringBuffer fullPath = new StringBuffer(PathInternal.MaxShortPath); + + try + { + GetFullPathName(path, ref fullPath); + + // Trim whitespace off the end of the string. Win32 normalization trims only U+0020. + fullPath.TrimEnd(PathInternal.s_trimEndChars); + + if (fullPath.Length >= PathInternal.MaxLongPath) + { + // Fullpath is genuinely too long + throw new PathTooLongException(SR.IO_PathTooLong); + } + + // Checking path validity used to happen before getting the full path name. To avoid additional input allocation + // (to trim trailing whitespace) we now do it after the Win32 call. This will allow legitimate paths through that + // used to get kicked back (notably segments with invalid characters might get removed via ".."). + // + // There is no way that GetLongPath can invalidate the path so we'll do this (cheaper) check before we attempt to + // expand short file names. + + // Scan the path for: + // + // - Illegal path characters. + // - Invalid UNC paths like \\, \\server, \\server\. + // - Segments that are too long (over MaxComponentLength) + + // As the path could be > 30K, we'll combine the validity scan. None of these checks are performed by the Win32 + // GetFullPathName() API. + + bool possibleShortPath = false; + bool foundTilde = false; + + // We can get UNCs as device paths through this code (e.g. \\.\UNC\), we won't validate them as there isn't + // an easy way to normalize without extensive cost (we'd have to hunt down the canonical name for any device + // path that contains UNC or to see if the path was doing something like \\.\GLOBALROOT\Device\Mup\, + // \\.\GLOBAL\UNC\, \\.\GLOBALROOT\GLOBAL??\UNC\, etc. + bool specialPath = fullPath.Length > 1 && fullPath[0] == '\\' && fullPath[1] == '\\'; + bool isDevice = PathInternal.IsDevice(ref fullPath); + bool possibleBadUnc = specialPath && !isDevice; + int index = specialPath ? 2 : 0; + int lastSeparator = specialPath ? 1 : 0; + int segmentLength; + char current; + + while (index < fullPath.Length) + { + current = fullPath[index]; + + // Try to skip deeper analysis. '?' and higher are valid/ignorable except for '\', '|', and '~' + if (current < '?' || current == '\\' || current == '|' || current == '~') + { + switch (current) + { + case '|': + case '>': + case '<': + case '\"': + if (checkInvalidCharacters) throw new ArgumentException(SR.Argument_InvalidPathChars); + foundTilde = false; + break; + case '~': + foundTilde = true; + break; + case '\\': + segmentLength = index - lastSeparator - 1; + if (segmentLength > PathInternal.MaxComponentLength) + throw new PathTooLongException(SR.IO_PathTooLong + fullPath.ToString()); + lastSeparator = index; + + if (foundTilde) + { + if (segmentLength <= MaxShortName) + { + // Possibly a short path. + possibleShortPath = true; + } + + foundTilde = false; + } + + if (possibleBadUnc) + { + // If we're at the end of the path and this is the first separator, we're missing the share. + // Otherwise we're good, so ignore UNC tracking from here. + if (index == fullPath.Length - 1) + throw new ArgumentException(SR.Arg_PathIllegalUNC); + else + possibleBadUnc = false; + } + + break; + + default: + if (checkInvalidCharacters && current < ' ') throw new ArgumentException(SR.Argument_InvalidPathChars, nameof(path)); + break; + } + } + + index++; + } + + if (possibleBadUnc) + throw new ArgumentException(SR.Arg_PathIllegalUNC); + + segmentLength = fullPath.Length - lastSeparator - 1; + if (segmentLength > PathInternal.MaxComponentLength) + throw new PathTooLongException(SR.IO_PathTooLong); + + if (foundTilde && segmentLength <= MaxShortName) + possibleShortPath = true; + + // Check for a short filename path and try and expand it. Technically you don't need to have a tilde for a short name, but + // this is how we've always done this. This expansion is costly so we'll continue to let other short paths slide. + if (expandShortPaths && possibleShortPath) + { + return TryExpandShortFileName(ref fullPath, originalPath: path); + } + else + { + if (fullPath.Length == path.Length && fullPath.StartsWith(path)) + { + // If we have the exact same string we were passed in, don't bother to allocate another string from the StringBuilder. + return path; + } + else + { + return fullPath.ToString(); + } + } + } + finally + { + // Clear the buffer + fullPath.Free(); + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static bool IsDosUnc(ref StringBuffer buffer) + { + return !PathInternal.IsDevice(ref buffer) && buffer.Length > 1 && buffer[0] == '\\' && buffer[1] == '\\'; + } + + private static unsafe void GetFullPathName(string path, ref StringBuffer fullPath) + { + // If the string starts with an extended prefix we would need to remove it from the path before we call GetFullPathName as + // it doesn't root extended paths correctly. We don't currently resolve extended paths, so we'll just assert here. + Debug.Assert(PathInternal.IsPartiallyQualified(path) || !PathInternal.IsExtended(path)); + + // Historically we would skip leading spaces *only* if the path started with a drive " C:" or a UNC " \\" + int startIndex = PathInternal.PathStartSkip(path); + + fixed (char* pathStart = path) + { + uint result = 0; + while ((result = Interop.Kernel32.GetFullPathNameW(pathStart + startIndex, (uint)fullPath.Capacity, fullPath.UnderlyingArray, IntPtr.Zero)) > fullPath.Capacity) + { + // Reported size is greater than the buffer size. Increase the capacity. + fullPath.EnsureCapacity(checked((int)result)); + } + + if (result == 0) + { + // Failure, get the error and throw + int errorCode = Marshal.GetLastWin32Error(); + if (errorCode == 0) + errorCode = Interop.Errors.ERROR_BAD_PATHNAME; + throw Win32Marshal.GetExceptionForWin32Error(errorCode, path); + } + + fullPath.Length = checked((int)result); + } + } + + private static int GetInputBuffer(ref StringBuffer content, bool isDosUnc, ref StringBuffer buffer) + { + int length = content.Length; + + length += isDosUnc + ? PathInternal.UncExtendedPrefixLength - PathInternal.UncPrefixLength + : PathInternal.DevicePrefixLength; + + buffer.EnsureCapacity(length + 1); + + if (isDosUnc) + { + // Put the extended UNC prefix (\\?\UNC\) in front of the path + buffer.CopyFrom(bufferIndex: 0, source: PathInternal.UncExtendedPathPrefix); + + // Copy the source buffer over after the existing UNC prefix + content.CopyTo( + bufferIndex: PathInternal.UncPrefixLength, + destination: ref buffer, + destinationIndex: PathInternal.UncExtendedPrefixLength, + count: content.Length - PathInternal.UncPrefixLength); + + // Return the prefix difference + return PathInternal.UncExtendedPrefixLength - PathInternal.UncPrefixLength; + } + else + { + int prefixSize = PathInternal.ExtendedPathPrefix.Length; + buffer.CopyFrom(bufferIndex: 0, source: PathInternal.ExtendedPathPrefix); + content.CopyTo(bufferIndex: 0, destination: ref buffer, destinationIndex: prefixSize, count: content.Length); + return prefixSize; + } + } + + private static string TryExpandShortFileName(ref StringBuffer outputBuffer, string originalPath) + { + // We guarantee we'll expand short names for paths that only partially exist. As such, we need to find the part of the path that actually does exist. To + // avoid allocating like crazy we'll create only one input array and modify the contents with embedded nulls. + + Debug.Assert(!PathInternal.IsPartiallyQualified(ref outputBuffer), "should have resolved by now"); + + // We'll have one of a few cases by now (the normalized path will have already: + // + // 1. Dos path (C:\) + // 2. Dos UNC (\\Server\Share) + // 3. Dos device path (\\.\C:\, \\?\C:\) + // + // We want to put the extended syntax on the front if it doesn't already have it, which may mean switching from \\.\. + // + // Note that we will never get \??\ here as GetFullPathName() does not recognize \??\ and will return it as C:\??\ (or whatever the current drive is). + + int rootLength = PathInternal.GetRootLength(ref outputBuffer); + bool isDevice = PathInternal.IsDevice(ref outputBuffer); + + StringBuffer inputBuffer = new StringBuffer(0); + try + { + bool isDosUnc = false; + int rootDifference = 0; + bool wasDotDevice = false; + + // Add the extended prefix before expanding to allow growth over MAX_PATH + if (isDevice) + { + // We have one of the following (\\?\ or \\.\) + inputBuffer.Append(ref outputBuffer); + + if (outputBuffer[2] == '.') + { + wasDotDevice = true; + inputBuffer[2] = '?'; + } + } + else + { + isDosUnc = IsDosUnc(ref outputBuffer); + rootDifference = GetInputBuffer(ref outputBuffer, isDosUnc, ref inputBuffer); + } + + rootLength += rootDifference; + int inputLength = inputBuffer.Length; + + bool success = false; + int foundIndex = inputBuffer.Length - 1; + + while (!success) + { + uint result = Interop.Kernel32.GetLongPathNameW(inputBuffer.UnderlyingArray, outputBuffer.UnderlyingArray, (uint)outputBuffer.Capacity); + + // Replace any temporary null we added + if (inputBuffer[foundIndex] == '\0') inputBuffer[foundIndex] = '\\'; + + if (result == 0) + { + // Look to see if we couldn't find the file + int error = Marshal.GetLastWin32Error(); + if (error != Interop.Errors.ERROR_FILE_NOT_FOUND && error != Interop.Errors.ERROR_PATH_NOT_FOUND) + { + // Some other failure, give up + break; + } + + // We couldn't find the path at the given index, start looking further back in the string. + foundIndex--; + + for (; foundIndex > rootLength && inputBuffer[foundIndex] != '\\'; foundIndex--) ; + if (foundIndex == rootLength) + { + // Can't trim the path back any further + break; + } + else + { + // Temporarily set a null in the string to get Windows to look further up the path + inputBuffer[foundIndex] = '\0'; + } + } + else if (result > outputBuffer.Capacity) + { + // Not enough space. The result count for this API does not include the null terminator. + outputBuffer.EnsureCapacity(checked((int)result)); + result = Interop.Kernel32.GetLongPathNameW(inputBuffer.UnderlyingArray, outputBuffer.UnderlyingArray, (uint)outputBuffer.Capacity); + } + else + { + // Found the path + success = true; + outputBuffer.Length = checked((int)result); + if (foundIndex < inputLength - 1) + { + // It was a partial find, put the non-existent part of the path back + outputBuffer.Append(ref inputBuffer, foundIndex, inputBuffer.Length - foundIndex); + } + } + } + + // Strip out the prefix and return the string + ref StringBuffer bufferToUse = ref Choose(success, ref outputBuffer, ref inputBuffer); + + // Switch back from \\?\ to \\.\ if necessary + if (wasDotDevice) + bufferToUse[2] = '.'; + + string returnValue = null; + + int newLength = (int)(bufferToUse.Length - rootDifference); + if (isDosUnc) + { + // Need to go from \\?\UNC\ to \\?\UN\\ + bufferToUse[PathInternal.UncExtendedPrefixLength - PathInternal.UncPrefixLength] = '\\'; + } + + // We now need to strip out any added characters at the front of the string + if (bufferToUse.SubstringEquals(originalPath, rootDifference, newLength)) + { + // Use the original path to avoid allocating + returnValue = originalPath; + } + else + { + returnValue = bufferToUse.Substring(rootDifference, newLength); + } + + return returnValue; + } + finally + { + inputBuffer.Free(); + } + } + + // Helper method to workaround lack of operator ? support for ref values + private static ref StringBuffer Choose(bool condition, ref StringBuffer s1, ref StringBuffer s2) + { + if (condition) return ref s1; + else return ref s2; + } + } +} diff --git a/src/mscorlib/shared/System/IO/PathInternal.Unix.cs b/src/mscorlib/shared/System/IO/PathInternal.Unix.cs new file mode 100644 index 0000000000..08dc1d0251 --- /dev/null +++ b/src/mscorlib/shared/System/IO/PathInternal.Unix.cs @@ -0,0 +1,104 @@ +// 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.Diagnostics; +using System.Text; + +namespace System.IO +{ + /// <summary>Contains internal path helpers that are shared between many projects.</summary> + internal static partial class PathInternal + { + internal const char DirectorySeparatorChar = '/'; + internal const char AltDirectorySeparatorChar = '/'; + internal const char VolumeSeparatorChar = '/'; + internal const char PathSeparator = ':'; + + internal const string DirectorySeparatorCharAsString = "/"; + + // There is only one invalid path character in Unix + private const char InvalidPathChar = '\0'; + + internal const string ParentDirectoryPrefix = @"../"; + + /// <summary>Returns a value indicating if the given path contains invalid characters.</summary> + internal static bool HasIllegalCharacters(string path) + { + Debug.Assert(path != null); + return path.IndexOf(InvalidPathChar) >= 0; + } + + internal static int GetRootLength(string path) + { + return path.Length > 0 && IsDirectorySeparator(path[0]) ? 1 : 0; + } + + internal static bool IsDirectorySeparator(char c) + { + // The alternate directory separator char is the same as the directory separator, + // so we only need to check one. + Debug.Assert(DirectorySeparatorChar == AltDirectorySeparatorChar); + return c == DirectorySeparatorChar; + } + + /// <summary> + /// Normalize separators in the given path. Compresses forward slash runs. + /// </summary> + internal static string NormalizeDirectorySeparators(string path) + { + if (string.IsNullOrEmpty(path)) return path; + + // Make a pass to see if we need to normalize so we can potentially skip allocating + bool normalized = true; + + for (int i = 0; i < path.Length; i++) + { + if (IsDirectorySeparator(path[i]) + && (i + 1 < path.Length && IsDirectorySeparator(path[i + 1]))) + { + normalized = false; + break; + } + } + + if (normalized) return path; + + StringBuilder builder = new StringBuilder(path.Length); + + for (int i = 0; i < path.Length; i++) + { + char current = path[i]; + + // Skip if we have another separator following + if (IsDirectorySeparator(current) + && (i + 1 < path.Length && IsDirectorySeparator(path[i + 1]))) + continue; + + builder.Append(current); + } + + return builder.ToString(); + } + + /// <summary> + /// Returns true if the character is a directory or volume separator. + /// </summary> + /// <param name="ch">The character to test.</param> + internal static bool IsDirectoryOrVolumeSeparator(char ch) + { + // The directory separator, volume separator, and the alternate directory + // separator should be the same on Unix, so we only need to check one. + Debug.Assert(DirectorySeparatorChar == AltDirectorySeparatorChar); + Debug.Assert(DirectorySeparatorChar == VolumeSeparatorChar); + return ch == DirectorySeparatorChar; + } + + internal static bool IsPartiallyQualified(string path) + { + // This is much simpler than Windows where paths can be rooted, but not fully qualified (such as Drive Relative) + // As long as the path is rooted in Unix it doesn't use the current directory and therefore is fully qualified. + return !Path.IsPathRooted(path); + } + } +} diff --git a/src/mscorlib/shared/System/IO/PathInternal.Windows.StringBuffer.cs b/src/mscorlib/shared/System/IO/PathInternal.Windows.StringBuffer.cs new file mode 100644 index 0000000000..84953df37b --- /dev/null +++ b/src/mscorlib/shared/System/IO/PathInternal.Windows.StringBuffer.cs @@ -0,0 +1,93 @@ +// 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.Diagnostics; +using System.Runtime.InteropServices; + +namespace System.IO +{ + /// <summary>Contains internal path helpers that are shared between many projects.</summary> + internal static partial class PathInternal + { + /// <summary> + /// Returns true if the path uses the extended syntax (\\?\) + /// </summary> + internal static bool IsExtended(ref StringBuffer path) + { + // While paths like "//?/C:/" will work, they're treated the same as "\\.\" paths. + // Skipping of normalization will *only* occur if back slashes ('\') are used. + return path.Length >= DevicePrefixLength + && path[0] == '\\' + && (path[1] == '\\' || path[1] == '?') + && path[2] == '?' + && path[3] == '\\'; + } + + /// <summary> + /// Gets the length of the root of the path (drive, share, etc.). + /// </summary> + internal unsafe static int GetRootLength(ref StringBuffer path) + { + if (path.Length == 0) return 0; + + fixed (char* value = path.UnderlyingArray) + { + return GetRootLength(value, path.Length); + } + } + + /// <summary> + /// Returns true if the path uses any of the DOS device path syntaxes. ("\\.\", "\\?\", or "\??\") + /// </summary> + internal static bool IsDevice(ref StringBuffer path) + { + // If the path begins with any two separators is will be recognized and normalized and prepped with + // "\??\" for internal usage correctly. "\??\" is recognized and handled, "/??/" is not. + return IsExtended(ref path) + || + ( + path.Length >= DevicePrefixLength + && IsDirectorySeparator(path[0]) + && IsDirectorySeparator(path[1]) + && (path[2] == '.' || path[2] == '?') + && IsDirectorySeparator(path[3]) + ); + } + + /// <summary> + /// Returns true if the path specified is relative to the current drive or working directory. + /// Returns false if the path is fixed to a specific drive or UNC path. This method does no + /// validation of the path (URIs will be returned as relative as a result). + /// </summary> + /// <remarks> + /// Handles paths that use the alternate directory separator. It is a frequent mistake to + /// assume that rooted paths (Path.IsPathRooted) are not relative. This isn't the case. + /// "C:a" is drive relative- meaning that it will be resolved against the current directory + /// for C: (rooted, but relative). "C:\a" is rooted and not relative (the current directory + /// will not be used to modify the path). + /// </remarks> + internal static bool IsPartiallyQualified(ref StringBuffer path) + { + if (path.Length < 2) + { + // It isn't fixed, it must be relative. There is no way to specify a fixed + // path with one character (or less). + return true; + } + + if (IsDirectorySeparator(path[0])) + { + // There is no valid way to specify a relative path with two initial slashes or + // \? as ? isn't valid for drive relative paths and \??\ is equivalent to \\?\ + return !(path[1] == '?' || IsDirectorySeparator(path[1])); + } + + // The only way to specify a fixed path that doesn't begin with two slashes + // is the drive, colon, slash format- i.e. C:\ + return !((path.Length >= 3) + && (path[1] == VolumeSeparatorChar) + && IsDirectorySeparator(path[2])); + } + } +} diff --git a/src/mscorlib/shared/System/IO/PathInternal.Windows.cs b/src/mscorlib/shared/System/IO/PathInternal.Windows.cs new file mode 100644 index 0000000000..ee0dd54383 --- /dev/null +++ b/src/mscorlib/shared/System/IO/PathInternal.Windows.cs @@ -0,0 +1,442 @@ +// 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.Diagnostics; +using System.Runtime.CompilerServices; +using System.Text; + +namespace System.IO +{ + /// <summary>Contains internal path helpers that are shared between many projects.</summary> + internal static partial class PathInternal + { + // All paths in Win32 ultimately end up becoming a path to a File object in the Windows object manager. Passed in paths get mapped through + // DosDevice symbolic links in the object tree to actual File objects under \Devices. To illustrate, this is what happens with a typical + // path "Foo" passed as a filename to any Win32 API: + // + // 1. "Foo" is recognized as a relative path and is appended to the current directory (say, "C:\" in our example) + // 2. "C:\Foo" is prepended with the DosDevice namespace "\??\" + // 3. CreateFile tries to create an object handle to the requested file "\??\C:\Foo" + // 4. The Object Manager recognizes the DosDevices prefix and looks + // a. First in the current session DosDevices ("\Sessions\1\DosDevices\" for example, mapped network drives go here) + // b. If not found in the session, it looks in the Global DosDevices ("\GLOBAL??\") + // 5. "C:" is found in DosDevices (in our case "\GLOBAL??\C:", which is a symbolic link to "\Device\HarddiskVolume6") + // 6. The full path is now "\Device\HarddiskVolume6\Foo", "\Device\HarddiskVolume6" is a File object and parsing is handed off + // to the registered parsing method for Files + // 7. The registered open method for File objects is invoked to create the file handle which is then returned + // + // There are multiple ways to directly specify a DosDevices path. The final format of "\??\" is one way. It can also be specified + // as "\\.\" (the most commonly documented way) and "\\?\". If the question mark syntax is used the path will skip normalization + // (essentially GetFullPathName()) and path length checks. + + // Windows Kernel-Mode Object Manager + // https://msdn.microsoft.com/en-us/library/windows/hardware/ff565763.aspx + // https://channel9.msdn.com/Shows/Going+Deep/Windows-NT-Object-Manager + // + // Introduction to MS-DOS Device Names + // https://msdn.microsoft.com/en-us/library/windows/hardware/ff548088.aspx + // + // Local and Global MS-DOS Device Names + // https://msdn.microsoft.com/en-us/library/windows/hardware/ff554302.aspx + + internal const char DirectorySeparatorChar = '\\'; + internal const char AltDirectorySeparatorChar = '/'; + internal const char VolumeSeparatorChar = ':'; + internal const char PathSeparator = ';'; + + internal const string DirectorySeparatorCharAsString = "\\"; + + internal const string ExtendedPathPrefix = @"\\?\"; + internal const string UncPathPrefix = @"\\"; + internal const string UncExtendedPrefixToInsert = @"?\UNC\"; + internal const string UncExtendedPathPrefix = @"\\?\UNC\"; + internal const string DevicePathPrefix = @"\\.\"; + internal const string ParentDirectoryPrefix = @"..\"; + + internal const int MaxShortPath = 260; + internal const int MaxShortDirectoryPath = 248; + internal const int MaxLongPath = short.MaxValue; + // \\?\, \\.\, \??\ + internal const int DevicePrefixLength = 4; + // \\ + internal const int UncPrefixLength = 2; + // \\?\UNC\, \\.\UNC\ + internal const int UncExtendedPrefixLength = 8; + internal const int MaxComponentLength = 255; + + /// <summary> + /// Returns true if the given character is a valid drive letter + /// </summary> + internal static bool IsValidDriveChar(char value) + { + return ((value >= 'A' && value <= 'Z') || (value >= 'a' && value <= 'z')); + } + + /// <summary> + /// Adds the extended path prefix (\\?\) if not already a device path, IF the path is not relative, + /// AND the path is more than 259 characters. (> MAX_PATH + null) + /// </summary> + internal static string EnsureExtendedPrefixOverMaxPath(string path) + { + if (path != null && path.Length >= MaxShortPath) + { + return EnsureExtendedPrefix(path); + } + else + { + return path; + } + } + + /// <summary> + /// Adds the extended path prefix (\\?\) if not relative or already a device path. + /// </summary> + internal static string EnsureExtendedPrefix(string path) + { + // Putting the extended prefix on the path changes the processing of the path. It won't get normalized, which + // means adding to relative paths will prevent them from getting the appropriate current directory inserted. + + // If it already has some variant of a device path (\??\, \\?\, \\.\, //./, etc.) we don't need to change it + // as it is either correct or we will be changing the behavior. When/if Windows supports long paths implicitly + // in the future we wouldn't want normalization to come back and break existing code. + + // In any case, all internal usages should be hitting normalize path (Path.GetFullPath) before they hit this + // shimming method. (Or making a change that doesn't impact normalization, such as adding a filename to a + // normalized base path.) + if (IsPartiallyQualified(path) || IsDevice(path)) + return path; + + // Given \\server\share in longpath becomes \\?\UNC\server\share + if (path.StartsWith(UncPathPrefix, StringComparison.OrdinalIgnoreCase)) + return path.Insert(2, UncExtendedPrefixToInsert); + + return ExtendedPathPrefix + path; + } + + /// <summary> + /// Returns true if the path uses any of the DOS device path syntaxes. ("\\.\", "\\?\", or "\??\") + /// </summary> + internal static bool IsDevice(string path) + { + // If the path begins with any two separators is will be recognized and normalized and prepped with + // "\??\" for internal usage correctly. "\??\" is recognized and handled, "/??/" is not. + return IsExtended(path) + || + ( + path.Length >= DevicePrefixLength + && IsDirectorySeparator(path[0]) + && IsDirectorySeparator(path[1]) + && (path[2] == '.' || path[2] == '?') + && IsDirectorySeparator(path[3]) + ); + } + + /// <summary> + /// Returns true if the path uses the canonical form of extended syntax ("\\?\" or "\??\"). If the + /// path matches exactly (cannot use alternate directory separators) Windows will skip normalization + /// and path length checks. + /// </summary> + internal static bool IsExtended(string path) + { + // While paths like "//?/C:/" will work, they're treated the same as "\\.\" paths. + // Skipping of normalization will *only* occur if back slashes ('\') are used. + return path.Length >= DevicePrefixLength + && path[0] == '\\' + && (path[1] == '\\' || path[1] == '?') + && path[2] == '?' + && path[3] == '\\'; + } + + /// <summary> + /// Returns a value indicating if the given path contains invalid characters (", <, >, | + /// NUL, or any ASCII char whose integer representation is in the range of 1 through 31). + /// Does not check for wild card characters ? and *. + /// </summary> + internal static bool HasIllegalCharacters(string path) + { + // This is equivalent to IndexOfAny(InvalidPathChars) >= 0, + // except faster since IndexOfAny grows slower as the input + // array grows larger. + // Since we know that some of the characters we're looking + // for are contiguous in the alphabet-- the path cannot contain + // characters 0-31-- we can optimize this for our specific use + // case and use simple comparison operations. + + for (int i = 0; i < path.Length; i++) + { + char c = path[i]; + if (c <= '|') // fast path for common case - '|' is highest illegal character + { + if (c <= '\u001f' || c == '|') + { + return true; + } + } + } + + return false; + } + + /// <summary> + /// Check for known wildcard characters. '*' and '?' are the most common ones. + /// </summary> + internal static bool HasWildCardCharacters(string path) + { + // Question mark is part of dos device syntax so we have to skip if we are + int startIndex = IsDevice(path) ? ExtendedPathPrefix.Length : 0; + + // [MS - FSA] 2.1.4.4 Algorithm for Determining if a FileName Is in an Expression + // https://msdn.microsoft.com/en-us/library/ff469270.aspx + for (int i = startIndex; i < path.Length; i++) + { + char c = path[i]; + if (c <= '?') // fast path for common case - '?' is highest wildcard character + { + if (c == '\"' || c == '<' || c == '>' || c == '*' || c == '?') + return true; + } + } + + return false; + } + + /// <summary> + /// Gets the length of the root of the path (drive, share, etc.). + /// </summary> + internal unsafe static int GetRootLength(string path) + { + fixed (char* value = path) + { + return GetRootLength(value, path.Length); + } + } + + private unsafe static int GetRootLength(char* path, int pathLength) + { + int i = 0; + int volumeSeparatorLength = 2; // Length to the colon "C:" + int uncRootLength = 2; // Length to the start of the server name "\\" + + bool extendedSyntax = StartsWithOrdinal(path, pathLength, ExtendedPathPrefix); + bool extendedUncSyntax = StartsWithOrdinal(path, pathLength, UncExtendedPathPrefix); + if (extendedSyntax) + { + // Shift the position we look for the root from to account for the extended prefix + if (extendedUncSyntax) + { + // "\\" -> "\\?\UNC\" + uncRootLength = UncExtendedPathPrefix.Length; + } + else + { + // "C:" -> "\\?\C:" + volumeSeparatorLength += ExtendedPathPrefix.Length; + } + } + + if ((!extendedSyntax || extendedUncSyntax) && pathLength > 0 && IsDirectorySeparator(path[0])) + { + // UNC or simple rooted path (e.g. "\foo", NOT "\\?\C:\foo") + + i = 1; // Drive rooted (\foo) is one character + if (extendedUncSyntax || (pathLength > 1 && IsDirectorySeparator(path[1]))) + { + // UNC (\\?\UNC\ or \\), scan past the next two directory separators at most + // (e.g. to \\?\UNC\Server\Share or \\Server\Share\) + i = uncRootLength; + int n = 2; // Maximum separators to skip + while (i < pathLength && (!IsDirectorySeparator(path[i]) || --n > 0)) i++; + } + } + else if (pathLength >= volumeSeparatorLength && path[volumeSeparatorLength - 1] == VolumeSeparatorChar) + { + // Path is at least longer than where we expect a colon, and has a colon (\\?\A:, A:) + // If the colon is followed by a directory separator, move past it + i = volumeSeparatorLength; + if (pathLength >= volumeSeparatorLength + 1 && IsDirectorySeparator(path[volumeSeparatorLength])) i++; + } + return i; + } + + private unsafe static bool StartsWithOrdinal(char* source, int sourceLength, string value) + { + if (sourceLength < value.Length) return false; + for (int i = 0; i < value.Length; i++) + { + if (value[i] != source[i]) return false; + } + return true; + } + + /// <summary> + /// Returns true if the path specified is relative to the current drive or working directory. + /// Returns false if the path is fixed to a specific drive or UNC path. This method does no + /// validation of the path (URIs will be returned as relative as a result). + /// </summary> + /// <remarks> + /// Handles paths that use the alternate directory separator. It is a frequent mistake to + /// assume that rooted paths (Path.IsPathRooted) are not relative. This isn't the case. + /// "C:a" is drive relative- meaning that it will be resolved against the current directory + /// for C: (rooted, but relative). "C:\a" is rooted and not relative (the current directory + /// will not be used to modify the path). + /// </remarks> + internal static bool IsPartiallyQualified(string path) + { + if (path.Length < 2) + { + // It isn't fixed, it must be relative. There is no way to specify a fixed + // path with one character (or less). + return true; + } + + if (IsDirectorySeparator(path[0])) + { + // There is no valid way to specify a relative path with two initial slashes or + // \? as ? isn't valid for drive relative paths and \??\ is equivalent to \\?\ + return !(path[1] == '?' || IsDirectorySeparator(path[1])); + } + + // The only way to specify a fixed path that doesn't begin with two slashes + // is the drive, colon, slash format- i.e. C:\ + return !((path.Length >= 3) + && (path[1] == VolumeSeparatorChar) + && IsDirectorySeparator(path[2]) + // To match old behavior we'll check the drive character for validity as the path is technically + // not qualified if you don't have a valid drive. "=:\" is the "=" file's default data stream. + && IsValidDriveChar(path[0])); + } + + /// <summary> + /// Returns the characters to skip at the start of the path if it starts with space(s) and a drive or directory separator. + /// (examples are " C:", " \") + /// This is a legacy behavior of Path.GetFullPath(). + /// </summary> + /// <remarks> + /// Note that this conflicts with IsPathRooted() which doesn't (and never did) such a skip. + /// </remarks> + internal static int PathStartSkip(string path) + { + int startIndex = 0; + while (startIndex < path.Length && path[startIndex] == ' ') startIndex++; + + if (startIndex > 0 && (startIndex < path.Length && IsDirectorySeparator(path[startIndex])) + || (startIndex + 1 < path.Length && path[startIndex + 1] == ':' && IsValidDriveChar(path[startIndex]))) + { + // Go ahead and skip spaces as we're either " C:" or " \" + return startIndex; + } + + return 0; + } + + /// <summary> + /// True if the given character is a directory separator. + /// </summary> + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal static bool IsDirectorySeparator(char c) + { + return c == DirectorySeparatorChar || c == AltDirectorySeparatorChar; + } + + /// <summary> + /// Normalize separators in the given path. Converts forward slashes into back slashes and compresses slash runs, keeping initial 2 if present. + /// Also trims initial whitespace in front of "rooted" paths (see PathStartSkip). + /// + /// This effectively replicates the behavior of the legacy NormalizePath when it was called with fullCheck=false and expandShortpaths=false. + /// The current NormalizePath gets directory separator normalization from Win32's GetFullPathName(), which will resolve relative paths and as + /// such can't be used here (and is overkill for our uses). + /// + /// Like the current NormalizePath this will not try and analyze periods/spaces within directory segments. + /// </summary> + /// <remarks> + /// The only callers that used to use Path.Normalize(fullCheck=false) were Path.GetDirectoryName() and Path.GetPathRoot(). Both usages do + /// not need trimming of trailing whitespace here. + /// + /// GetPathRoot() could technically skip normalizing separators after the second segment- consider as a future optimization. + /// + /// For legacy desktop behavior with ExpandShortPaths: + /// - It has no impact on GetPathRoot() so doesn't need consideration. + /// - It could impact GetDirectoryName(), but only if the path isn't relative (C:\ or \\Server\Share). + /// + /// In the case of GetDirectoryName() the ExpandShortPaths behavior was undocumented and provided inconsistent results if the path was + /// fixed/relative. For example: "C:\PROGRA~1\A.TXT" would return "C:\Program Files" while ".\PROGRA~1\A.TXT" would return ".\PROGRA~1". If you + /// ultimately call GetFullPath() this doesn't matter, but if you don't or have any intermediate string handling could easily be tripped up by + /// this undocumented behavior. + /// + /// We won't match this old behavior because: + /// + /// 1. It was undocumented + /// 2. It was costly (extremely so if it actually contained '~') + /// 3. Doesn't play nice with string logic + /// 4. Isn't a cross-plat friendly concept/behavior + /// </remarks> + internal static string NormalizeDirectorySeparators(string path) + { + if (string.IsNullOrEmpty(path)) return path; + + char current; + int start = PathStartSkip(path); + + if (start == 0) + { + // Make a pass to see if we need to normalize so we can potentially skip allocating + bool normalized = true; + + for (int i = 0; i < path.Length; i++) + { + current = path[i]; + if (IsDirectorySeparator(current) + && (current != DirectorySeparatorChar + // Check for sequential separators past the first position (we need to keep initial two for UNC/extended) + || (i > 0 && i + 1 < path.Length && IsDirectorySeparator(path[i + 1])))) + { + normalized = false; + break; + } + } + + if (normalized) return path; + } + + StringBuilder builder = new StringBuilder(path.Length); + + if (IsDirectorySeparator(path[start])) + { + start++; + builder.Append(DirectorySeparatorChar); + } + + for (int i = start; i < path.Length; i++) + { + current = path[i]; + + // If we have a separator + if (IsDirectorySeparator(current)) + { + // If the next is a separator, skip adding this + if (i + 1 < path.Length && IsDirectorySeparator(path[i + 1])) + { + continue; + } + + // Ensure it is the primary separator + current = DirectorySeparatorChar; + } + + builder.Append(current); + } + + return builder.ToString(); + } + + /// <summary> + /// Returns true if the character is a directory or volume separator. + /// </summary> + /// <param name="ch">The character to test.</param> + internal static bool IsDirectoryOrVolumeSeparator(char ch) + { + return IsDirectorySeparator(ch) || VolumeSeparatorChar == ch; + } + } +} diff --git a/src/mscorlib/shared/System/IO/PathInternal.cs b/src/mscorlib/shared/System/IO/PathInternal.cs new file mode 100644 index 0000000000..0dab5b968a --- /dev/null +++ b/src/mscorlib/shared/System/IO/PathInternal.cs @@ -0,0 +1,171 @@ +// 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.Diagnostics; +using System.Text; + +namespace System.IO +{ + /// <summary>Contains internal path helpers that are shared between many projects.</summary> + internal static partial class PathInternal + { + // Trim trailing white spaces, tabs etc but don't be aggressive in removing everything that has UnicodeCategory of trailing space. + // string.WhitespaceChars will trim more aggressively than what the underlying FS does (for ex, NTFS, FAT). + // + // (This is for compatibility with old behavior.) + internal static readonly char[] s_trimEndChars = + { + (char)0x9, // Horizontal tab + (char)0xA, // Line feed + (char)0xB, // Vertical tab + (char)0xC, // Form feed + (char)0xD, // Carriage return + (char)0x20, // Space + (char)0x85, // Next line + (char)0xA0 // Non breaking space + }; + + /// <summary> + /// Checks for invalid path characters in the given path. + /// </summary> + /// <exception cref="System.ArgumentNullException">Thrown if the path is null.</exception> + /// <exception cref="System.ArgumentException">Thrown if the path has invalid characters.</exception> + /// <param name="path">The path to check for invalid characters.</param> + internal static void CheckInvalidPathChars(string path) + { + if (path == null) + throw new ArgumentNullException(nameof(path)); + + if (HasIllegalCharacters(path)) + throw new ArgumentException(SR.Argument_InvalidPathChars, nameof(path)); + } + + /// <summary> + /// Returns the start index of the filename + /// in the given path, or 0 if no directory + /// or volume separator is found. + /// </summary> + /// <param name="path">The path in which to find the index of the filename.</param> + /// <remarks> + /// This method returns path.Length for + /// inputs like "/usr/foo/" on Unix. As such, + /// it is not safe for being used to index + /// the string without additional verification. + /// </remarks> + internal static int FindFileNameIndex(string path) + { + Debug.Assert(path != null); + CheckInvalidPathChars(path); + + for (int i = path.Length - 1; i >= 0; i--) + { + char ch = path[i]; + if (IsDirectoryOrVolumeSeparator(ch)) + return i + 1; + } + + return 0; // the whole path is the filename + } + + /// <summary> + /// Returns true if the path ends in a directory separator. + /// </summary> + internal static bool EndsInDirectorySeparator(string path) => + !string.IsNullOrEmpty(path) && IsDirectorySeparator(path[path.Length - 1]); + + /// <summary> + /// Get the common path length from the start of the string. + /// </summary> + internal static int GetCommonPathLength(string first, string second, bool ignoreCase) + { + int commonChars = EqualStartingCharacterCount(first, second, ignoreCase: ignoreCase); + + // If nothing matches + if (commonChars == 0) + return commonChars; + + // Or we're a full string and equal length or match to a separator + if (commonChars == first.Length + && (commonChars == second.Length || IsDirectorySeparator(second[commonChars]))) + return commonChars; + + if (commonChars == second.Length && IsDirectorySeparator(first[commonChars])) + return commonChars; + + // It's possible we matched somewhere in the middle of a segment e.g. C:\Foodie and C:\Foobar. + while (commonChars > 0 && !IsDirectorySeparator(first[commonChars - 1])) + commonChars--; + + return commonChars; + } + + /// <summary> + /// Gets the count of common characters from the left optionally ignoring case + /// </summary> + unsafe internal static int EqualStartingCharacterCount(string first, string second, bool ignoreCase) + { + if (string.IsNullOrEmpty(first) || string.IsNullOrEmpty(second)) return 0; + + int commonChars = 0; + + fixed (char* f = first) + fixed (char* s = second) + { + char* l = f; + char* r = s; + char* leftEnd = l + first.Length; + char* rightEnd = r + second.Length; + + while (l != leftEnd && r != rightEnd + && (*l == *r || (ignoreCase && char.ToUpperInvariant((*l)) == char.ToUpperInvariant((*r))))) + { + commonChars++; + l++; + r++; + } + } + + return commonChars; + } + + /// <summary> + /// Returns true if the two paths have the same root + /// </summary> + internal static bool AreRootsEqual(string first, string second, StringComparison comparisonType) + { + int firstRootLength = GetRootLength(first); + int secondRootLength = GetRootLength(second); + + return firstRootLength == secondRootLength + && string.Compare( + strA: first, + indexA: 0, + strB: second, + indexB: 0, + length: firstRootLength, + comparisonType: comparisonType) == 0; + } + + /// <summary> + /// Returns false for ".." unless it is specified as a part of a valid File/Directory name. + /// (Used to avoid moving up directories.) + /// + /// Valid: a..b abc..d + /// Invalid: ..ab ab.. .. abc..d\abc.. + /// </summary> + internal static void CheckSearchPattern(string searchPattern) + { + int index; + while ((index = searchPattern.IndexOf("..", StringComparison.Ordinal)) != -1) + { + // Terminal ".." . Files names cannot end in ".." + if (index + 2 == searchPattern.Length + || IsDirectorySeparator(searchPattern[index + 2])) + throw new ArgumentException(SR.Arg_InvalidSearchPattern); + + searchPattern = searchPattern.Substring(index + 2); + } + } + } +} diff --git a/src/mscorlib/shared/System/IO/PathTooLongException.cs b/src/mscorlib/shared/System/IO/PathTooLongException.cs new file mode 100644 index 0000000000..613af051ca --- /dev/null +++ b/src/mscorlib/shared/System/IO/PathTooLongException.cs @@ -0,0 +1,37 @@ +// 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; +using System.Runtime.Serialization; + +namespace System.IO +{ + [Serializable] + public class PathTooLongException : IOException + { + public PathTooLongException() + : base(SR.IO_PathTooLong) + { + HResult = __HResults.COR_E_PATHTOOLONG; + } + + public PathTooLongException(string message) + : base(message) + { + HResult = __HResults.COR_E_PATHTOOLONG; + } + + public PathTooLongException(string message, Exception innerException) + : base(message, innerException) + { + HResult = __HResults.COR_E_PATHTOOLONG; + } + + protected PathTooLongException(SerializationInfo info, StreamingContext context) + : base(info, context) + { + } + } +} diff --git a/src/mscorlib/shared/System/IO/SeekOrigin.cs b/src/mscorlib/shared/System/IO/SeekOrigin.cs new file mode 100644 index 0000000000..3798a0ce70 --- /dev/null +++ b/src/mscorlib/shared/System/IO/SeekOrigin.cs @@ -0,0 +1,16 @@ +// 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. + +namespace System.IO +{ + // Provides seek reference points. To seek to the end of a stream, + // call stream.Seek(0, SeekOrigin.End). + public enum SeekOrigin + { + // These constants match Win32's FILE_BEGIN, FILE_CURRENT, and FILE_END + Begin = 0, + Current = 1, + End = 2, + } +} diff --git a/src/mscorlib/shared/System/IO/StreamHelpers.CopyValidation.cs b/src/mscorlib/shared/System/IO/StreamHelpers.CopyValidation.cs new file mode 100644 index 0000000000..45bbd816df --- /dev/null +++ b/src/mscorlib/shared/System/IO/StreamHelpers.CopyValidation.cs @@ -0,0 +1,46 @@ +// 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. + +namespace System.IO +{ + /// <summary>Provides methods to help in the implementation of Stream-derived types.</summary> + internal static partial class StreamHelpers + { + /// <summary>Validate the arguments to CopyTo, as would Stream.CopyTo.</summary> + public static void ValidateCopyToArgs(Stream source, Stream destination, int bufferSize) + { + if (destination == null) + { + throw new ArgumentNullException(nameof(destination)); + } + + if (bufferSize <= 0) + { + throw new ArgumentOutOfRangeException(nameof(bufferSize), bufferSize, SR.ArgumentOutOfRange_NeedPosNum); + } + + bool sourceCanRead = source.CanRead; + if (!sourceCanRead && !source.CanWrite) + { + throw new ObjectDisposedException(null, SR.ObjectDisposed_StreamClosed); + } + + bool destinationCanWrite = destination.CanWrite; + if (!destinationCanWrite && !destination.CanRead) + { + throw new ObjectDisposedException(nameof(destination), SR.ObjectDisposed_StreamClosed); + } + + if (!sourceCanRead) + { + throw new NotSupportedException(SR.NotSupported_UnreadableStream); + } + + if (!destinationCanWrite) + { + throw new NotSupportedException(SR.NotSupported_UnwritableStream); + } + } + } +} diff --git a/src/mscorlib/shared/System/IO/Win32Marshal.cs b/src/mscorlib/shared/System/IO/Win32Marshal.cs new file mode 100644 index 0000000000..ef76c27010 --- /dev/null +++ b/src/mscorlib/shared/System/IO/Win32Marshal.cs @@ -0,0 +1,109 @@ +// 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; +using System.Diagnostics; +using System.Runtime.InteropServices; + +namespace System.IO +{ + /// <summary> + /// Provides static methods for converting from Win32 errors codes to exceptions, HRESULTS and error messages. + /// </summary> + internal static class Win32Marshal + { + /// <summary> + /// Converts, resetting it, the last Win32 error into a corresponding <see cref="Exception"/> object. + /// </summary> + internal static Exception GetExceptionForLastWin32Error() + { + int errorCode = Marshal.GetLastWin32Error(); + return GetExceptionForWin32Error(errorCode, string.Empty); + } + + /// <summary> + /// Converts the specified Win32 error into a corresponding <see cref="Exception"/> object. + /// </summary> + internal static Exception GetExceptionForWin32Error(int errorCode) + { + return GetExceptionForWin32Error(errorCode, string.Empty); + } + + /// <summary> + /// Converts the specified Win32 error into a corresponding <see cref="Exception"/> object, optionally + /// including the specified path in the error message. + /// </summary> + internal static Exception GetExceptionForWin32Error(int errorCode, string path) + { + switch (errorCode) + { + case Interop.Errors.ERROR_FILE_NOT_FOUND: + if (path.Length == 0) + return new FileNotFoundException(SR.IO_FileNotFound); + else + return new FileNotFoundException(SR.Format(SR.IO_FileNotFound_FileName, path), path); + + case Interop.Errors.ERROR_PATH_NOT_FOUND: + if (path.Length == 0) + return new DirectoryNotFoundException(SR.IO_PathNotFound_NoPathName); + else + return new DirectoryNotFoundException(SR.Format(SR.IO_PathNotFound_Path, path)); + + case Interop.Errors.ERROR_ACCESS_DENIED: + if (path.Length == 0) + return new UnauthorizedAccessException(SR.UnauthorizedAccess_IODenied_NoPathName); + else + return new UnauthorizedAccessException(SR.Format(SR.UnauthorizedAccess_IODenied_Path, path)); + + case Interop.Errors.ERROR_ALREADY_EXISTS: + if (path.Length == 0) + goto default; + + return new IOException(SR.Format(SR.IO_AlreadyExists_Name, path), MakeHRFromErrorCode(errorCode)); + + case Interop.Errors.ERROR_FILENAME_EXCED_RANGE: + return new PathTooLongException(SR.IO_PathTooLong); + + case Interop.Errors.ERROR_INVALID_PARAMETER: + return new IOException(GetMessage(errorCode), MakeHRFromErrorCode(errorCode)); + + case Interop.Errors.ERROR_SHARING_VIOLATION: + if (path.Length == 0) + return new IOException(SR.IO_SharingViolation_NoFileName, MakeHRFromErrorCode(errorCode)); + else + return new IOException(SR.Format(SR.IO_SharingViolation_File, path), MakeHRFromErrorCode(errorCode)); + + case Interop.Errors.ERROR_FILE_EXISTS: + if (path.Length == 0) + goto default; + + return new IOException(SR.Format(SR.IO_FileExists_Name, path), MakeHRFromErrorCode(errorCode)); + + case Interop.Errors.ERROR_OPERATION_ABORTED: + return new OperationCanceledException(); + + default: + return new IOException(GetMessage(errorCode), MakeHRFromErrorCode(errorCode)); + } + } + + /// <summary> + /// Returns a HRESULT for the specified Win32 error code. + /// </summary> + internal static int MakeHRFromErrorCode(int errorCode) + { + Debug.Assert((0xFFFF0000 & errorCode) == 0, "This is an HRESULT, not an error code!"); + + return unchecked(((int)0x80070000) | errorCode); + } + + /// <summary> + /// Returns a string message for the specified Win32 error code. + /// </summary> + internal static string GetMessage(int errorCode) + { + return Interop.Kernel32.GetMessage(errorCode); + } + } +} |