diff --git a/src/Microsoft.Data.SqlClient/netcore/src/Common/System/IO/PathInternal.Windows.cs b/src/Microsoft.Data.SqlClient/netcore/src/Common/System/IO/PathInternal.Windows.cs deleted file mode 100644 index 5f9ee0e02d..0000000000 --- a/src/Microsoft.Data.SqlClient/netcore/src/Common/System/IO/PathInternal.Windows.cs +++ /dev/null @@ -1,428 +0,0 @@ -// 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.CompilerServices; -using System.Text; - -namespace System.IO -{ - /// Contains internal path helpers that are shared between many projects. - 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 DevicePrefixLength = 4; - // \\ - internal const int UncPrefixLength = 2; - // \\?\UNC\, \\.\UNC\ - internal const int UncExtendedPrefixLength = 8; - - /// - /// Returns true if the given character is a valid drive letter - /// - internal static bool IsValidDriveChar(char value) - { - return ((value >= 'A' && value <= 'Z') || (value >= 'a' && value <= 'z')); - } - - internal static bool EndsWithPeriodOrSpace(string path) - { - if (string.IsNullOrEmpty(path)) - return false; - - char c = path[path.Length - 1]; - return c == ' ' || c == '.'; - } - - /// - /// 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). This will also insert the extended - /// prefix if the path ends with a period or a space. Trailing periods and spaces are normally eaten - /// away from paths during normalization, but if we see such a path at this point it should be - /// normalized and has retained the final characters. (Typically from one of the *Info classes) - /// - internal static string EnsureExtendedPrefixIfNeeded(string path) - { - if (path != null && (path.Length >= MaxShortPath || EndsWithPeriodOrSpace(path))) - { - return EnsureExtendedPrefix(path); - } - else - { - return path; - } - } - - /// - /// DO NOT USE- Use EnsureExtendedPrefixIfNeeded. This will be removed shortly. - /// 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) - /// - internal static string EnsureExtendedPrefixOverMaxPath(string path) - { - if (path != null && path.Length >= MaxShortPath) - { - return EnsureExtendedPrefix(path); - } - else - { - return path; - } - } - - /// - /// Adds the extended path prefix (\\?\) if not relative or already a device path. - /// - 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.AsSpan()) || IsDevice(path.AsSpan())) - 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; - } - - /// - /// Returns true if the path uses any of the DOS device path syntaxes. ("\\.\", "\\?\", or "\??\") - /// - internal static bool IsDevice(ReadOnlySpan 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]) - ); - } - - /// - /// Returns true if the path is a device UNC (\\?\UNC\, \\.\UNC\) - /// - internal static bool IsDeviceUNC(ReadOnlySpan path) - { - return path.Length >= UncExtendedPrefixLength - && IsDevice(path) - && IsDirectorySeparator(path[7]) - && path[4] == 'U' - && path[5] == 'N' - && path[6] == 'C'; - } - - /// - /// 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. - /// - internal static bool IsExtended(ReadOnlySpan 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] == '\\'; - } - - /// - /// Check for known wildcard characters. '*' and '?' are the most common ones. - /// - internal static bool HasWildCardCharacters(ReadOnlySpan 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; - } - - /// - /// Gets the length of the root of the path (drive, share, etc.). - /// - internal static int GetRootLength(ReadOnlySpan path) - { - int pathLength = path.Length; - int i = 0; - - bool deviceSyntax = IsDevice(path); - bool deviceUnc = deviceSyntax && IsDeviceUNC(path); - - if ((!deviceSyntax || deviceUnc) && pathLength > 0 && IsDirectorySeparator(path[0])) - { - // UNC or simple rooted path (e.g. "\foo", NOT "\\?\C:\foo") - if (deviceUnc || (pathLength > 1 && IsDirectorySeparator(path[1]))) - { - // UNC (\\?\UNC\ or \\), scan past server\share - - // Start past the prefix ("\\" or "\\?\UNC\") - i = deviceUnc ? UncExtendedPrefixLength : UncPrefixLength; - - // Skip two separators at most - int n = 2; - while (i < pathLength && (!IsDirectorySeparator(path[i]) || --n > 0)) - i++; - } - else - { - // Current drive rooted (e.g. "\foo") - i = 1; - } - } - else if (deviceSyntax) - { - // Device path (e.g. "\\?\.", "\\.\") - // Skip any characters following the prefix that aren't a separator - i = DevicePrefixLength; - while (i < pathLength && !IsDirectorySeparator(path[i])) - i++; - - // If there is another separator take it, as long as we have had at least one - // non-separator after the prefix (e.g. don't take "\\?\\", but take "\\?\a\") - if (i < pathLength && i > DevicePrefixLength && IsDirectorySeparator(path[i])) - i++; - } - else if (pathLength >= 2 - && path[1] == VolumeSeparatorChar - && IsValidDriveChar(path[0])) - { - // Valid drive specified path ("C:", "D:", etc.) - i = 2; - - // If the colon is followed by a directory separator, move past it (e.g "C:\") - if (pathLength > 2 && IsDirectorySeparator(path[2])) - i++; - } - - return i; - } - - /// - /// 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). - /// - /// - /// 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). - /// - internal static bool IsPartiallyQualified(ReadOnlySpan 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])); - } - - /// - /// True if the given character is a directory separator. - /// - [MethodImpl(MethodImplOptions.AggressiveInlining)] - internal static bool IsDirectorySeparator(char c) - { - return c == DirectorySeparatorChar || c == AltDirectorySeparatorChar; - } - - /// - /// 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. - /// - /// - /// 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 - /// - internal static string NormalizeDirectorySeparators(string path) - { - if (string.IsNullOrEmpty(path)) - return path; - - char current; - - // 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); - - int start = 0; - 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(); - } - - /// - /// Returns true if the path is effectively empty for the current OS. - /// For unix, this is empty or null. For Windows, this is empty, null, or - /// just spaces ((char)32). - /// - internal static bool IsEffectivelyEmpty(ReadOnlySpan path) - { - if (path.IsEmpty) - return true; - - foreach (char c in path) - { - if (c != ' ') - return false; - } - return true; - } - } -} diff --git a/src/Microsoft.Data.SqlClient/netcore/src/Microsoft.Data.SqlClient.csproj b/src/Microsoft.Data.SqlClient/netcore/src/Microsoft.Data.SqlClient.csproj index a7778281b5..08e07b1989 100644 --- a/src/Microsoft.Data.SqlClient/netcore/src/Microsoft.Data.SqlClient.csproj +++ b/src/Microsoft.Data.SqlClient/netcore/src/Microsoft.Data.SqlClient.csproj @@ -701,6 +701,9 @@ Common\Interop\Windows\Kernel32\Interop.FileTypes.cs + + Common\Interop\Windows\Kernel32\Interop.GetFullPathName.cs + Common\Interop\Windows\Kernel32\Interop.IoControlCodeAccess.cs @@ -722,6 +725,9 @@ Common\Interop\Windows\NtDll\Interop.RtlNtStatusToDosError.cs + + Common\Interop\Windows\NtDll\Interop.SecurityQualityOfService.cs + Microsoft\Data\Common\AdapterUtil.Windows.cs @@ -749,6 +755,10 @@ Microsoft\Data\SqlClient\TdsParserSafeHandles.Windows.cs + + Microsoft\Data\SqlTypes\SqlFileStream.Windows.cs + + @@ -775,7 +785,6 @@ - @@ -792,7 +801,6 @@ - diff --git a/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlTypes/SqlFileStream.Windows.cs b/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlTypes/SqlFileStream.Windows.cs deleted file mode 100644 index 319e0d0169..0000000000 --- a/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlTypes/SqlFileStream.Windows.cs +++ /dev/null @@ -1,701 +0,0 @@ -// 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.Buffers; -using System.Diagnostics; -using System.Globalization; -using System.IO; -using System.Runtime.InteropServices; -using System.Security.Permissions; -using System.Threading; -using Microsoft.Data.Common; -using Microsoft.Data.SqlClient; -using Microsoft.Win32.SafeHandles; - -namespace Microsoft.Data.SqlTypes -{ - /// - public sealed partial class SqlFileStream : System.IO.Stream - { - // NOTE: if we ever unseal this class, be sure to specify the Name, SafeFileHandle, and - // TransactionContext accessors as virtual methods. Doing so now on a sealed class - // generates a compiler error (CS0549) - - private static int _objectTypeCount; // EventSource Counter - internal int ObjectID { get; } = Interlocked.Increment(ref _objectTypeCount); - - // from System.IO.FileStream implementation - // DefaultBufferSize = 4096; - // SQLBUVSTS# 193123 - disable lazy flushing of written data in order to prevent - // potential exceptions during Close/Finalization. Since System.IO.FileStream will - // not allow for a zero byte buffer, we'll create a one byte buffer which, in normal - // usage, will not be used and the user buffer will automatically flush directly to - // the disk cache. In pathological scenarios where the client is writing a single - // byte at a time, we'll explicitly call flush ourselves. - internal const int DefaultBufferSize = 1; - - private const ushort IoControlCodeFunctionCode = 2392; - private const int ERROR_MR_MID_NOT_FOUND = 317; - #region Definitions from devioctl.h - private const ushort FILE_DEVICE_FILE_SYSTEM = 0x0009; - #endregion - - private System.IO.FileStream _m_fs; - private string _m_path; - private byte[] _m_txn; - private bool _m_disposed; - private static byte[] s_eaNameString = new byte[] - { - (byte)'F', (byte)'i', (byte)'l', (byte)'e', (byte)'s', (byte)'t', (byte)'r', (byte)'e', (byte)'a', (byte)'m', (byte)'_', - (byte)'T', (byte)'r', (byte)'a', (byte)'n', (byte)'s', (byte)'a', (byte)'c', (byte)'t', (byte)'i', (byte)'o', (byte)'n', (byte)'_', - (byte)'T', (byte)'a', (byte)'g', (byte) '\0' - }; - - /// - public SqlFileStream(string path, byte[] transactionContext, FileAccess access) : - this(path, transactionContext, access, FileOptions.None, 0) - { } - - /// - public SqlFileStream(string path, byte[] transactionContext, FileAccess access, FileOptions options, long allocationSize) - { - using (TryEventScope.Create(SqlClientEventSource.Log.TryScopeEnterEvent("SqlFileStream.ctor | API | Object Id {0} | Access {1} | Options {2} | Path '{3}'", ObjectID, (int)access, (int)options, path))) - { - //----------------------------------------------------------------- - // precondition validation - - if (transactionContext == null) - throw ADP.ArgumentNull("transactionContext"); - - if (path == null) - throw ADP.ArgumentNull("path"); - - //----------------------------------------------------------------- - - _m_disposed = false; - _m_fs = null; - - OpenSqlFileStream(path, transactionContext, access, options, allocationSize); - - // only set internal state once the file has actually been successfully opened - Name = path; - TransactionContext = transactionContext; - } - } - - #region destructor/dispose code - - // NOTE: this destructor will only be called only if the Dispose - // method is not called by a client, giving the class a chance - // to finalize properly (i.e., free unmanaged resources) - /// - ~SqlFileStream() - { - Dispose(false); - } - - /// - protected override void Dispose(bool disposing) - { - try - { - if (!_m_disposed) - { - try - { - if (disposing) - { - if (_m_fs != null) - { - _m_fs.Close(); - _m_fs = null; - } - } - } - finally - { - _m_disposed = true; - } - } - } - finally - { - base.Dispose(disposing); - } - } - #endregion - - /// - public string Name - { - get - { - // assert that path has been properly processed via GetFullPathInternal - // (e.g. m_path hasn't been set directly) - AssertPathFormat(_m_path); - return _m_path; - } - private set - { - // should be validated by callers of this method - Debug.Assert(value != null); - Debug.Assert(!_m_disposed); - - _m_path = GetFullPathInternal(value); - } - } - - /// - public byte[] TransactionContext - { - get - { - if (_m_txn == null) - return null; - - return (byte[])_m_txn.Clone(); - } - private set - { - // should be validated by callers of this method - Debug.Assert(value != null); - Debug.Assert(!_m_disposed); - - _m_txn = (byte[])value.Clone(); - } - } - - #region System.IO.Stream methods - - /// - public override bool CanRead - { - get - { - if (_m_disposed) - throw ADP.ObjectDisposed(this); - - return _m_fs.CanRead; - } - } - - /// - // If CanSeek is false, Position, Seek, Length, and SetLength should throw. - public override bool CanSeek - { - get - { - if (_m_disposed) - throw ADP.ObjectDisposed(this); - - return _m_fs.CanSeek; - } - } - - /// - public override bool CanTimeout - { - get - { - if (_m_disposed) - throw ADP.ObjectDisposed(this); - - return _m_fs.CanTimeout; - } - } - - /// - public override bool CanWrite - { - get - { - if (_m_disposed) - throw ADP.ObjectDisposed(this); - - return _m_fs.CanWrite; - } - } - - /// - public override long Length - { - get - { - if (_m_disposed) - throw ADP.ObjectDisposed(this); - - return _m_fs.Length; - } - } - - /// - public override long Position - { - get - { - if (_m_disposed) - throw ADP.ObjectDisposed(this); - - return _m_fs.Position; - } - set - { - if (_m_disposed) - throw ADP.ObjectDisposed(this); - - _m_fs.Position = value; - } - } - - /// - public override int ReadTimeout - { - get - { - if (_m_disposed) - throw ADP.ObjectDisposed(this); - - return _m_fs.ReadTimeout; - } - set - { - if (_m_disposed) - throw ADP.ObjectDisposed(this); - - _m_fs.ReadTimeout = value; - } - } - - /// - public override int WriteTimeout - { - get - { - if (_m_disposed) - throw ADP.ObjectDisposed(this); - - return _m_fs.WriteTimeout; - } - set - { - if (_m_disposed) - throw ADP.ObjectDisposed(this); - - _m_fs.WriteTimeout = value; - } - } - - /// - public override void Flush() - { - if (_m_disposed) - throw ADP.ObjectDisposed(this); - - _m_fs.Flush(); - } - - /// -#if NETFRAMEWORK - [HostProtection(ExternalThreading = true)] -#endif - public override IAsyncResult BeginRead(byte[] buffer, int offset, int count, AsyncCallback callback, object state) - { - if (_m_disposed) - throw ADP.ObjectDisposed(this); - - return _m_fs.BeginRead(buffer, offset, count, callback, state); - } - - /// - public override int EndRead(IAsyncResult asyncResult) - { - if (_m_disposed) - throw ADP.ObjectDisposed(this); - - return _m_fs.EndRead(asyncResult); - } - - /// -#if NETFRAMEWORK - [HostProtection(ExternalThreading = true)] -#endif - public override IAsyncResult BeginWrite(byte[] buffer, int offset, int count, AsyncCallback callback, object state) - { - if (_m_disposed) - throw ADP.ObjectDisposed(this); - - IAsyncResult asyncResult = _m_fs.BeginWrite(buffer, offset, count, callback, state); - - // SQLBUVSTS# 193123 - disable lazy flushing of written data in order to prevent - // potential exceptions during Close/Finalization. Since System.IO.FileStream will - // not allow for a zero byte buffer, we'll create a one byte buffer which, in normal - // usage, will not be used and the user buffer will automatically flush directly to - // the disk cache. In pathological scenarios where the client is writing a single - // byte at a time, we'll explicitly call flush ourselves. - if (count == 1) - { - // calling flush here will mimic the internal control flow of System.IO.FileStream - _m_fs.Flush(); - } - - return asyncResult; - } - - /// - public override void EndWrite(IAsyncResult asyncResult) - { - if (_m_disposed) - throw ADP.ObjectDisposed(this); - - _m_fs.EndWrite(asyncResult); - } - - /// - public override long Seek(long offset, SeekOrigin origin) - { - if (_m_disposed) - throw ADP.ObjectDisposed(this); - - return _m_fs.Seek(offset, origin); - } - - /// - public override void SetLength(long value) - { - if (_m_disposed) - throw ADP.ObjectDisposed(this); - - _m_fs.SetLength(value); - } - - /// - public override int Read([In, Out] byte[] buffer, int offset, int count) - { - if (_m_disposed) - throw ADP.ObjectDisposed(this); - - return _m_fs.Read(buffer, offset, count); - } - - /// - public override int ReadByte() - { - if (_m_disposed) - throw ADP.ObjectDisposed(this); - - return _m_fs.ReadByte(); - } - - /// - public override void Write(byte[] buffer, int offset, int count) - { - if (_m_disposed) - throw ADP.ObjectDisposed(this); - - _m_fs.Write(buffer, offset, count); - - // SQLBUVSTS# 193123 - disable lazy flushing of written data in order to prevent - // potential exceptions during Close/Finalization. Since System.IO.FileStream will - // not allow for a zero byte buffer, we'll create a one byte buffer which, in normal - // usage, will cause System.IO.FileStream to utilize the user-supplied buffer and - // automatically flush the data directly to the disk cache. In pathological scenarios - // where the user is writing a single byte at a time, we'll explicitly call flush ourselves. - if (count == 1) - { - // calling flush here will mimic the internal control flow of System.IO.FileStream - _m_fs.Flush(); - } - } - - /// - public override void WriteByte(byte value) - { - if (_m_disposed) - throw ADP.ObjectDisposed(this); - - _m_fs.WriteByte(value); - - // SQLBUVSTS# 193123 - disable lazy flushing of written data in order to prevent - // potential exceptions during Close/Finalization. Since our internal buffer is - // only a single byte in length, the provided user data will always be cached. - // As a result, we need to be sure to flush the data to disk ourselves. - - // calling flush here will mimic the internal control flow of System.IO.FileStream - _m_fs.Flush(); - } - -#endregion - - [Conditional("DEBUG")] - static private void AssertPathFormat(string path) - { - Debug.Assert(path != null); - Debug.Assert(path == path.Trim()); - Debug.Assert(path.Length > 0); - Debug.Assert(path.StartsWith(@"\\", StringComparison.OrdinalIgnoreCase)); - } - - static private string GetFullPathInternal(string path) - { - //----------------------------------------------------------------- - // precondition validation should be validated by callers of this method - // NOTE: if this method moves elsewhere, this assert should become an actual runtime check - // as the implicit assumptions here cannot be relied upon in an inter-class context - Debug.Assert(path != null); - - // remove leading and trailing whitespace - path = path.Trim(); - if (path.Length == 0) - { - throw ADP.Argument(StringsHelper.GetString(Strings.SqlFileStream_InvalidPath), "path"); - } - - // make sure path is not DOS device path - if (!path.StartsWith(@"\\", StringComparison.Ordinal) && !System.IO.PathInternal.IsDevice(path.AsSpan())) - { - throw ADP.Argument(StringsHelper.GetString(Strings.SqlFileStream_InvalidPath), "path"); - } - - // normalize the path - path = System.IO.Path.GetFullPath(path); - - // make sure path is a UNC path - if (System.IO.PathInternal.IsDeviceUNC(path.AsSpan())) - { - throw ADP.Argument(StringsHelper.GetString(Strings.SqlFileStream_PathNotValidDiskResource), "path"); - } - - return path; - } - - private unsafe void OpenSqlFileStream - ( - string sPath, - byte[] transactionContext, - System.IO.FileAccess access, - System.IO.FileOptions options, - long allocationSize - ) - { - //----------------------------------------------------------------- - // precondition validation - // these should be checked by any caller of this method - // ensure we have validated and normalized the path before - Debug.Assert(sPath != null); - Debug.Assert(transactionContext != null); - - if (access != System.IO.FileAccess.Read && access != System.IO.FileAccess.Write && access != System.IO.FileAccess.ReadWrite) - throw ADP.ArgumentOutOfRange("access"); - - // FileOptions is a set of flags, so AND the given value against the set of values we do not support - if ((options & ~(System.IO.FileOptions.WriteThrough | System.IO.FileOptions.Asynchronous | System.IO.FileOptions.RandomAccess | System.IO.FileOptions.SequentialScan)) != 0) - throw ADP.ArgumentOutOfRange("options"); - - //----------------------------------------------------------------- - // normalize the provided path - // * compress path to remove any occurrences of '.' or '..' - // * trim whitespace from the beginning and end of the path - // * ensure that the path starts with '\\' - // * ensure that the path does not start with '\\.\' - sPath = GetFullPathInternal(sPath); - - Microsoft.Win32.SafeHandles.SafeFileHandle hFile = null; - Interop.NtDll.DesiredAccess nDesiredAccess = Interop.NtDll.DesiredAccess.FILE_READ_ATTRIBUTES | Interop.NtDll.DesiredAccess.SYNCHRONIZE; - Interop.NtDll.CreateOptions dwCreateOptions = 0; - Interop.NtDll.CreateDisposition dwCreateDisposition = 0; - System.IO.FileShare nShareAccess = System.IO.FileShare.None; - - switch (access) - { - case System.IO.FileAccess.Read: - - nDesiredAccess |= Interop.NtDll.DesiredAccess.FILE_READ_DATA; - nShareAccess = System.IO.FileShare.Delete | System.IO.FileShare.ReadWrite; - dwCreateDisposition = Interop.NtDll.CreateDisposition.FILE_OPEN; - break; - - case System.IO.FileAccess.Write: - nDesiredAccess |= Interop.NtDll.DesiredAccess.FILE_WRITE_DATA; - nShareAccess = System.IO.FileShare.Delete | System.IO.FileShare.Read; - dwCreateDisposition = Interop.NtDll.CreateDisposition.FILE_OVERWRITE; - break; - - case System.IO.FileAccess.ReadWrite: - default: - // we validate the value of 'access' parameter in the beginning of this method - Debug.Assert(access == System.IO.FileAccess.ReadWrite); - - nDesiredAccess |= Interop.NtDll.DesiredAccess.FILE_READ_DATA | Interop.NtDll.DesiredAccess.FILE_WRITE_DATA; - nShareAccess = System.IO.FileShare.Delete | System.IO.FileShare.Read; - dwCreateDisposition = Interop.NtDll.CreateDisposition.FILE_OVERWRITE; - break; - } - - if ((options & System.IO.FileOptions.WriteThrough) != 0) - { - dwCreateOptions |= Interop.NtDll.CreateOptions.FILE_WRITE_THROUGH; - } - - if ((options & System.IO.FileOptions.Asynchronous) == 0) - { - dwCreateOptions |= Interop.NtDll.CreateOptions.FILE_SYNCHRONOUS_IO_NONALERT; - } - - if ((options & System.IO.FileOptions.SequentialScan) != 0) - { - dwCreateOptions |= Interop.NtDll.CreateOptions.FILE_SEQUENTIAL_ONLY; - } - - if ((options & System.IO.FileOptions.RandomAccess) != 0) - { - dwCreateOptions |= Interop.NtDll.CreateOptions.FILE_RANDOM_ACCESS; - } - - try - { - // NOTE: the Name property is intended to reveal the publicly available moniker for the - // FILESTREAM attributed column data. We will not surface the internal processing that - // takes place to create the mappedPath. - string mappedPath = InitializeNtPath(sPath); - int retval = 0; - Interop.Kernel32.SetThreadErrorMode(Interop.Kernel32.SEM_FAILCRITICALERRORS, out uint oldMode); - - try - { - if (transactionContext.Length >= ushort.MaxValue) - throw ADP.ArgumentOutOfRange("transactionContext"); - - int headerSize = sizeof(Interop.NtDll.FILE_FULL_EA_INFORMATION); - int fullSize = headerSize + transactionContext.Length + s_eaNameString.Length; - - byte[] buffer = ArrayPool.Shared.Rent(fullSize); - - fixed (byte* b = buffer) - { - Interop.NtDll.FILE_FULL_EA_INFORMATION* ea = (Interop.NtDll.FILE_FULL_EA_INFORMATION*)b; - ea->NextEntryOffset = 0; - ea->Flags = 0; - ea->EaNameLength = (byte)(s_eaNameString.Length - 1); // Length does not include terminating null character. - ea->EaValueLength = (ushort)transactionContext.Length; - - // We could continue to do pointer math here, chose to use Span for convenience to - // make sure we get the other members in the right place. - Span data = buffer.AsSpan(headerSize); - s_eaNameString.AsSpan().CopyTo(data); - data = data.Slice(s_eaNameString.Length); - transactionContext.AsSpan().CopyTo(data); - - (int status, IntPtr handle) = Interop.NtDll.CreateFile(path: mappedPath.AsSpan(), - rootDirectory: IntPtr.Zero, - createDisposition: dwCreateDisposition, - desiredAccess: nDesiredAccess, - shareAccess: nShareAccess, - fileAttributes: 0, - createOptions: dwCreateOptions, - eaBuffer: b, - eaLength: (uint)fullSize); - - SqlClientEventSource.Log.TryAdvancedTraceEvent("SqlFileStream.OpenSqlFileStream | ADV | Object Id {0}, Desired Access 0x{1}, Allocation Size {2}, File Attributes 0, Share Access 0x{3}, Create Disposition 0x{4}, Create Options 0x{5}", ObjectID, (int)nDesiredAccess, allocationSize, (int)nShareAccess, dwCreateDisposition, dwCreateOptions); - - retval = status; - hFile = new SafeFileHandle(handle, true); - } - - ArrayPool.Shared.Return(buffer); - } - finally - { - Interop.Kernel32.SetThreadErrorMode(oldMode, out oldMode); - } - - switch (retval) - { - case 0: - break; - - case Interop.Errors.ERROR_SHARING_VIOLATION: - throw ADP.InvalidOperation(StringsHelper.GetString(Strings.SqlFileStream_FileAlreadyInTransaction)); - - case Interop.Errors.ERROR_INVALID_PARAMETER: - throw ADP.Argument(StringsHelper.GetString(Strings.SqlFileStream_InvalidParameter)); - - case Interop.Errors.ERROR_FILE_NOT_FOUND: - { - System.IO.DirectoryNotFoundException e = new System.IO.DirectoryNotFoundException(); - ADP.TraceExceptionAsReturnValue(e); - throw e; - } - default: - { - uint error = Interop.NtDll.RtlNtStatusToDosError(retval); - if (error == ERROR_MR_MID_NOT_FOUND) - { - // status code could not be mapped to a Win32 error code - error = (uint)retval; - } - - System.ComponentModel.Win32Exception e = new System.ComponentModel.Win32Exception(unchecked((int)error)); - ADP.TraceExceptionAsReturnValue(e); - throw e; - } - } - - if (hFile.IsInvalid) - { - System.ComponentModel.Win32Exception e = new System.ComponentModel.Win32Exception(Interop.Errors.ERROR_INVALID_HANDLE); - ADP.TraceExceptionAsReturnValue(e); - throw e; - } - - if (Interop.Kernel32.GetFileType(hFile) != Interop.Kernel32.FileTypes.FILE_TYPE_DISK) - { - hFile.Dispose(); - throw ADP.Argument(StringsHelper.GetString(Strings.SqlFileStream_PathNotValidDiskResource)); - } - - // if the user is opening the SQL FileStream in read/write mode, we assume that they want to scan - // through current data and then append new data to the end, so we need to tell SQL Server to preserve - // the existing file contents. - if (access == System.IO.FileAccess.ReadWrite) - { - uint ioControlCode = Interop.Kernel32.CTL_CODE(FILE_DEVICE_FILE_SYSTEM, - IoControlCodeFunctionCode, (byte)Interop.Kernel32.IoControlTransferType.METHOD_BUFFERED, - (byte)Interop.Kernel32.IoControlCodeAccess.FILE_ANY_ACCESS); - - if (!Interop.Kernel32.DeviceIoControl(hFile, ioControlCode, IntPtr.Zero, 0, IntPtr.Zero, 0, out uint cbBytesReturned, IntPtr.Zero)) - { - System.ComponentModel.Win32Exception e = new System.ComponentModel.Win32Exception(Marshal.GetLastWin32Error()); - ADP.TraceExceptionAsReturnValue(e); - throw e; - } - } - - // now that we've successfully opened a handle on the path and verified that it is a file, - // use the SafeFileHandle to initialize our internal System.IO.FileStream instance - System.Diagnostics.Debug.Assert(_m_fs == null); - _m_fs = new System.IO.FileStream(hFile, access, DefaultBufferSize, ((options & System.IO.FileOptions.Asynchronous) != 0)); - } - catch - { - if (hFile != null && !hFile.IsInvalid) - hFile.Dispose(); - - throw; - } - } - // This method exists to ensure that the requested path name is unique so that SMB/DNS is prevented - // from collapsing a file open request to a file handle opened previously. In the SQL FILESTREAM case, - // this would likely be a file open in another transaction, so this mechanism ensures isolation. - static private string InitializeNtPath(string path) - { - // Ensure we have validated and normalized the path before - AssertPathFormat(path); - string uniqueId = Guid.NewGuid().ToString("N"); - return System.IO.PathInternal.IsDeviceUNC(path) - ? string.Format(CultureInfo.InvariantCulture, @"{0}\{1}", path.Replace(@"\\.", @"\??"), uniqueId) - : string.Format(CultureInfo.InvariantCulture, @"\??\UNC\{0}\{1}", path.Trim('\\'), uniqueId); - } - } -} diff --git a/src/Microsoft.Data.SqlClient/netfx/src/Microsoft.Data.SqlClient.csproj b/src/Microsoft.Data.SqlClient/netfx/src/Microsoft.Data.SqlClient.csproj index ce918d2c93..c42fc5b18e 100644 --- a/src/Microsoft.Data.SqlClient/netfx/src/Microsoft.Data.SqlClient.csproj +++ b/src/Microsoft.Data.SqlClient/netfx/src/Microsoft.Data.SqlClient.csproj @@ -100,6 +100,54 @@ + + Interop\Interop.Errors.cs + + + Interop\Kernel32\Interop.CTL_CODE.cs + + + Interop\Kernel32\Interop.DeviceIoControl.cs + + + Interop\Kernel32\Interop.FileTypes.cs + + + Interop\Kernel32\Interop.GetFileType_SafeHandle.cs + + + Interop\Kernel32\Interop.GetFullPathName.cs + + + Interop\Kernel32\Interop.IoControlCodeAccess.cs + + + Interop\Kernel32\Interop.IoControlTransferType.cs + + + Interop\Interop.Libraries.cs + + + Interop\Kernel32\Interop.SetThreadErrorMode.cs + + + Interop\NtDll\Interop.FILE_FULL_EA_INFORMATION.cs + + + Interop\NtDll\Interop.IO_STATUS_BLOCK.cs + + + Interop\NtDll\Interop.NtCreateFile.cs + + + Interop\NtDll\Interop.RtlNtStatusToDosError.cs + + + Interop\NtDll\Interop.SecurityQualityOfService.cs + + + Interop\Interop.UNICODE_STRING.cs + Microsoft\Data\Common\ActivityCorrelator.cs @@ -670,6 +718,9 @@ Microsoft\Data\SqlClient\VirtualSecureModeEnclaveProviderBase.cs + + Microsoft\Data\SqlTypes\SqlFileStream.cs + Microsoft\Data\SqlTypes\SQLResource.cs @@ -739,7 +790,6 @@ - diff --git a/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlTypes/UnsafeNativeMethods.cs b/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlTypes/UnsafeNativeMethods.cs index 296c2dbcdc..4396137798 100644 --- a/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlTypes/UnsafeNativeMethods.cs +++ b/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlTypes/UnsafeNativeMethods.cs @@ -43,75 +43,6 @@ internal static extern FileType GetFileType Microsoft.Win32.SafeHandles.SafeFileHandle hFile ); - // do not use this PInvoke directly, use SafeGetFullPathName instead - [DllImport("Kernel32.dll", SetLastError = true, CharSet = CharSet.Unicode)] - [ResourceExposure(ResourceScope.Machine)] - private static extern int GetFullPathName - ( - string path, - int numBufferChars, - StringBuilder buffer, - IntPtr lpFilePartOrNull - ); - - /// - /// safe wrapper for GetFullPathName - /// check that the path length is less than Int16.MaxValue before calling this API! - /// - [ResourceExposure(ResourceScope.Machine)] - [ResourceConsumption(ResourceScope.Machine)] - internal static string SafeGetFullPathName(string path) - { - Debug.Assert(path != null, "path is null?"); - // make sure to test for Int16.MaxValue limit before calling this method - // see the below comment re GetLastWin32Error for the reason - Debug.Assert(path.Length < Int16.MaxValue); - - // since we expect network paths, the 'full path' is expected to be the same size - // as the provided one. we still need to allocate +1 for null termination - StringBuilder buffer = new StringBuilder(path.Length + 1); - - int cchRequiredSize = GetFullPathName(path, buffer.Capacity, buffer, IntPtr.Zero); - - // if our buffer was smaller than required, GetFullPathName will succeed and return us the required buffer size with null - if (cchRequiredSize > buffer.Capacity) - { - // we have to reallocate and retry - buffer.Capacity = cchRequiredSize; - cchRequiredSize = GetFullPathName(path, buffer.Capacity, buffer, IntPtr.Zero); - } - - if (cchRequiredSize == 0) - { - // GetFullPathName call failed - int lastError = Marshal.GetLastWin32Error(); - if (lastError == 0) - { - // we found that in some cases GetFullPathName fail but does not set the last error value - // for example, it happens when the path provided to it is longer than 32K: return value is 0 (failure) - // but GetLastError was zero too so we raised Win32Exception saying "The operation completed successfully". - // To raise proper "path too long" failure, check the length before calling this API. - // For other (yet unknown cases), we will throw InvalidPath message since we do not know what exactly happened - throw ADP.Argument(StringsHelper.GetString(StringsHelper.SqlFileStream_InvalidPath), "path"); - } - else - { - System.ComponentModel.Win32Exception e = new System.ComponentModel.Win32Exception(lastError); - ADP.TraceExceptionAsReturnValue(e); - throw e; - } - } - - // this should not happen since we already reallocate - Debug.Assert(cchRequiredSize <= buffer.Capacity, string.Format( - System.Globalization.CultureInfo.InvariantCulture, - "second call to GetFullPathName returned greater size: {0} > {1}", - cchRequiredSize, - buffer.Capacity)); - - return buffer.ToString(); - } - // RTM versions of Win7 and Windows Server 2008 R2 private static readonly Version ThreadErrorModeMinOsVersion = new Version(6, 1, 7600); diff --git a/src/Microsoft.Data.SqlClient/src/Interop/Windows/Interop.UNICODE_STRING.cs b/src/Microsoft.Data.SqlClient/src/Interop/Windows/Interop.UNICODE_STRING.cs index a0ca032d16..92d5933407 100644 --- a/src/Microsoft.Data.SqlClient/src/Interop/Windows/Interop.UNICODE_STRING.cs +++ b/src/Microsoft.Data.SqlClient/src/Interop/Windows/Interop.UNICODE_STRING.cs @@ -2,7 +2,7 @@ // 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.IO; using System.Runtime.InteropServices; internal static partial class Interop @@ -10,7 +10,7 @@ internal static partial class Interop // https://msdn.microsoft.com/en-us/library/windows/desktop/aa380518.aspx // https://msdn.microsoft.com/en-us/library/windows/hardware/ff564879.aspx [StructLayout(LayoutKind.Sequential)] - internal struct UNICODE_STRING + internal unsafe struct UNICODE_STRING { /// /// Length, in bytes, not including the the null, if any. @@ -21,6 +21,17 @@ internal struct UNICODE_STRING /// Max size of the buffer in bytes /// internal ushort MaximumLength; - internal IntPtr Buffer; + + /// + /// Pointer to the buffer used to contain the wide characters of the string. + /// + internal char* Buffer; + + public UNICODE_STRING(char* buffer, int length) + { + Length = checked((ushort)(length * sizeof(char))); + MaximumLength = checked((ushort)(length * sizeof(char))); + Buffer = buffer; + } } } diff --git a/src/Microsoft.Data.SqlClient/src/Interop/Windows/Kernel32/Interop.DeviceIoControl.cs b/src/Microsoft.Data.SqlClient/src/Interop/Windows/Kernel32/Interop.DeviceIoControl.cs index 7520040a50..4301c0851b 100644 --- a/src/Microsoft.Data.SqlClient/src/Interop/Windows/Kernel32/Interop.DeviceIoControl.cs +++ b/src/Microsoft.Data.SqlClient/src/Interop/Windows/Kernel32/Interop.DeviceIoControl.cs @@ -10,6 +10,8 @@ internal partial class Interop { internal partial class Kernel32 { + internal const ushort FILE_DEVICE_FILE_SYSTEM = 0x0009; + [DllImport(Libraries.Kernel32, CharSet = CharSet.Unicode, SetLastError = true)] internal static extern bool DeviceIoControl ( diff --git a/src/Microsoft.Data.SqlClient/src/Interop/Windows/Kernel32/Interop.GetFullPathName.cs b/src/Microsoft.Data.SqlClient/src/Interop/Windows/Kernel32/Interop.GetFullPathName.cs new file mode 100644 index 0000000000..941d03a8da --- /dev/null +++ b/src/Microsoft.Data.SqlClient/src/Interop/Windows/Kernel32/Interop.GetFullPathName.cs @@ -0,0 +1,22 @@ +// 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.Runtime.Versioning; +using System.Text; + +internal partial class Interop +{ + internal partial class Kernel32 + { + [DllImport("Kernel32.dll", SetLastError = true, CharSet = CharSet.Unicode)] + [ResourceExposure(ResourceScope.Machine)] + internal static extern int GetFullPathName( + string path, + int numBufferChars, + StringBuilder buffer, + IntPtr lpFilePartOrNull); + } +} diff --git a/src/Microsoft.Data.SqlClient/src/Interop/Windows/NtDll/Interop.NtCreateFile.cs b/src/Microsoft.Data.SqlClient/src/Interop/Windows/NtDll/Interop.NtCreateFile.cs index 967b7bf27f..5574fc036c 100644 --- a/src/Microsoft.Data.SqlClient/src/Interop/Windows/NtDll/Interop.NtCreateFile.cs +++ b/src/Microsoft.Data.SqlClient/src/Interop/Windows/NtDll/Interop.NtCreateFile.cs @@ -3,6 +3,7 @@ // See the LICENSE file in the project root for more information. using System; +using System.IO; using System.Runtime.InteropServices; internal partial class Interop @@ -25,45 +26,87 @@ private unsafe static extern int NtCreateFile( void* EaBuffer, uint EaLength); - internal unsafe static (int status, IntPtr handle) CreateFile( - ReadOnlySpan path, - IntPtr rootDirectory, + internal static unsafe (int status, IntPtr handle) CreateFile( + string path, + byte[] eaName, + byte[] eaValue, + + DesiredAccess desiredAccess, + FileAttributes fileAttributes, + FileShare shareAccess, CreateDisposition createDisposition, - DesiredAccess desiredAccess = DesiredAccess.FILE_GENERIC_READ | DesiredAccess.SYNCHRONIZE, - System.IO.FileShare shareAccess = System.IO.FileShare.ReadWrite | System.IO.FileShare.Delete, - System.IO.FileAttributes fileAttributes = 0, - CreateOptions createOptions = CreateOptions.FILE_SYNCHRONOUS_IO_NONALERT, - ObjectAttributes objectAttributes = ObjectAttributes.OBJ_CASE_INSENSITIVE, - void* eaBuffer = null, - uint eaLength = 0) + CreateOptions createOptions + + #if NETFRAMEWORK + ,ImpersonationLevel impersonationLevel, + bool isDynamicTracking, + bool isEffectiveOnly + #endif + ) { - fixed (char* c = &MemoryMarshal.GetReference(path)) + // Acquire space for the file extended attribute + int eaHeaderSize = sizeof(FILE_FULL_EA_INFORMATION); + int eaBufferSize = eaHeaderSize + eaName.Length + eaValue.Length; + Span eaBuffer = stackalloc byte[eaBufferSize]; + + // Fix the position of the path and the extended attribute buffer + fixed (char* pPath = path) + fixed (byte* pEaBuffer = eaBuffer) { - UNICODE_STRING name = new UNICODE_STRING - { - Length = checked((ushort)(path.Length * sizeof(char))), - MaximumLength = checked((ushort)(path.Length * sizeof(char))), - Buffer = (IntPtr)c - }; - + // Generate a unicode string object from the path + UNICODE_STRING ucPath = new UNICODE_STRING(pPath, path.Length); + + #if NETFRAMEWORK + // Generate a Security QOS object + SecurityQualityOfService qos = new SecurityQualityOfService( + impersonationLevel, + isDynamicTracking, + isEffectiveOnly); + SecurityQualityOfService* pQos = &qos; + #else + SecurityQualityOfService* pQos = null; + #endif + + // Generate the object attributes object that defines what we're opening OBJECT_ATTRIBUTES attributes = new OBJECT_ATTRIBUTES( - &name, - objectAttributes, - rootDirectory); - + objectName: &ucPath, + attributes: ObjectAttributes.OBJ_CASE_INSENSITIVE, + rootDirectory: IntPtr.Zero, + securityQos: pQos); + + // Set the contents of the extended information + // NOTE: This chunk of code treats a byte[] as FILE_FULL_EA_INFORMATION. Since we + // do not have a direct reference to a FILE_FULL_EA_INFORMATION, we have to use + // the -> operator to dereference the object before accessing its members. + // However, the byte[] is longer than the FILE_FULL_EA_INFORMATION struct in + // order to contain the name and value. Since byte[] are reference types, we + // cannot store the name/value directly in the struct (in memory it would be + // stored as a pointer). So in the second chunk, we copy the name/value to the + // byte[] after the FILE_FULL_EA_INFORMATION struct. + // Step 1) Write the header + FILE_FULL_EA_INFORMATION* pEaObj = (FILE_FULL_EA_INFORMATION*)pEaBuffer; + pEaObj->NextEntryOffset = 0; + pEaObj->Flags = 0; + pEaObj->EaNameLength = (byte)(eaName.Length - 1); // Null terminator is not included + pEaObj->EaValueLength = (ushort)eaValue.Length; + + // Step 2) Write the contents + eaName.AsSpan().CopyTo(eaBuffer.Slice(eaHeaderSize)); + eaValue.AsSpan().CopyTo(eaBuffer.Slice(eaHeaderSize + eaName.Length)); + + // Make the interop call int status = NtCreateFile( out IntPtr handle, desiredAccess, ref attributes, - out IO_STATUS_BLOCK statusBlock, + IoStatusBlock: out _, AllocationSize: null, fileAttributes, shareAccess, createDisposition, createOptions, - eaBuffer, - eaLength); - + pEaBuffer, + (uint) eaBufferSize); return (status, handle); } } @@ -100,19 +143,23 @@ internal unsafe struct OBJECT_ATTRIBUTES /// Optional quality of service to be applied to the object. Used to indicate /// security impersonation level and context tracking mode (dynamic or static). /// - public void* SecurityQualityOfService; + public SecurityQualityOfService* SecurityQoS; /// /// Equivalent of InitializeObjectAttributes macro with the exception that you can directly set SQOS. /// - public unsafe OBJECT_ATTRIBUTES(UNICODE_STRING* objectName, ObjectAttributes attributes, IntPtr rootDirectory) + public unsafe OBJECT_ATTRIBUTES( + UNICODE_STRING* objectName, + ObjectAttributes attributes, + IntPtr rootDirectory, + SecurityQualityOfService* securityQos) { Length = (uint)sizeof(OBJECT_ATTRIBUTES); RootDirectory = rootDirectory; ObjectName = objectName; Attributes = attributes; SecurityDescriptor = null; - SecurityQualityOfService = null; + SecurityQoS = securityQos; } } @@ -215,6 +262,7 @@ public enum CreateDisposition : uint /// /// Options for creating/opening files with NtCreateFile. /// + [Flags] public enum CreateOptions : uint { /// diff --git a/src/Microsoft.Data.SqlClient/src/Interop/Windows/NtDll/Interop.RtlNtStatusToDosError.cs b/src/Microsoft.Data.SqlClient/src/Interop/Windows/NtDll/Interop.RtlNtStatusToDosError.cs index 05e8ce4cf4..ef1034230d 100644 --- a/src/Microsoft.Data.SqlClient/src/Interop/Windows/NtDll/Interop.RtlNtStatusToDosError.cs +++ b/src/Microsoft.Data.SqlClient/src/Interop/Windows/NtDll/Interop.RtlNtStatusToDosError.cs @@ -9,6 +9,11 @@ internal partial class Interop { internal partial class NtDll { + /// + /// The system cannot find message text for the provided error number. + /// + public const int ERROR_MR_MID_NOT_FOUND = 317; + // https://msdn.microsoft.com/en-us/library/windows/desktop/ms680600(v=vs.85).aspx [DllImport(Libraries.NtDll, ExactSpelling = true)] public unsafe static extern uint RtlNtStatusToDosError( diff --git a/src/Microsoft.Data.SqlClient/src/Interop/Windows/NtDll/Interop.SecurityQualityOfService.cs b/src/Microsoft.Data.SqlClient/src/Interop/Windows/NtDll/Interop.SecurityQualityOfService.cs new file mode 100644 index 0000000000..d83a01e0b1 --- /dev/null +++ b/src/Microsoft.Data.SqlClient/src/Interop/Windows/NtDll/Interop.SecurityQualityOfService.cs @@ -0,0 +1,85 @@ +// 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.InteropServices; + +internal partial class Interop +{ + [StructLayout(LayoutKind.Sequential)] + internal unsafe struct SecurityQualityOfService + { + public SecurityQualityOfService( + ImpersonationLevel impersonationLevel, + bool isDynamicTracking, + bool isEffectiveOnly) + { + Length = (uint)sizeof(SecurityQualityOfService); + ImpersonationLevel = impersonationLevel; + IsDynamicTracking = isDynamicTracking; + IsEffectiveOnly = isEffectiveOnly; + } + + /// + /// Specifies the size, in bytes, of this structure. + /// + public uint Length { get; } + + /// + /// Specifies the information given to the server about the client, and how the server + /// may represent, or impersonate, the client. Security impersonation levels govern the + /// degree to which a server process can act on behalf of a client process. + /// + public ImpersonationLevel ImpersonationLevel { get; set; } + + /// + /// Specifies whether the server is to be given a snapshot of the client's security + /// context (called static tracking), or is to be continually updated to track changes + /// to the client's security context (called dynamic tracking). Not all communication + /// mechanisms support dynamic tracking; those that do not will default to static + /// tracking. + /// + public bool IsDynamicTracking { get; set; } + + /// + /// Specifies whether the server may enable or disable privileges and groups that the + /// client's security context may include. + /// + public bool IsEffectiveOnly { get; set; } + } + + /// + /// Values that specify security impersonation levels. Security impersonation levels govern + /// the degree to which a server process can act on behalf of a client process. + /// + internal enum ImpersonationLevel + { + /// + /// The server process cannot obtain identification information about the client, and + /// it cannot impersonate the client. It is defined with no value given, and this, by + /// ANSI C rules, defaults to a value of zero. + /// + SecurityAnonymous = 0, + + /// + /// The server process can obtain information about the client, such as security + /// identifiers and privileges, but it cannot impersonate the client. This is useful + /// for servers that export their own objects, for example, database products that + /// export tables and views. Using the retrieved client-security information, the + /// server can make access-validation decision without being able to use other + /// services that are using the client's security context. + /// + SecurityIdentification = 1, + + /// + /// The server process can impersonate the client's security context on its local + /// system. The server cannot impersonate the client on remote systems. + /// + SecurityImpersonation = 2, + + /// + /// The server process can impersonate the client's security context on remote systems. + /// + SecurityDelegation = 3, + } +} diff --git a/src/Microsoft.Data.SqlClient/src/Microsoft.Data.SqlClient.csproj b/src/Microsoft.Data.SqlClient/src/Microsoft.Data.SqlClient.csproj index 3ecd506e73..344f86ff89 100644 --- a/src/Microsoft.Data.SqlClient/src/Microsoft.Data.SqlClient.csproj +++ b/src/Microsoft.Data.SqlClient/src/Microsoft.Data.SqlClient.csproj @@ -1,10 +1,15 @@ net6.0;net8.0;net462 + true - + + + + + diff --git a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlTypes/SqlFileStream.Windows.cs b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlTypes/SqlFileStream.Windows.cs new file mode 100644 index 0000000000..eb37dabef3 --- /dev/null +++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlTypes/SqlFileStream.Windows.cs @@ -0,0 +1,959 @@ +// 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.ComponentModel; +using System.Diagnostics; +using System.Globalization; +using System.IO; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using System.Threading; +using Microsoft.Data.Common; +using Microsoft.Data.SqlClient; +using Microsoft.Win32.SafeHandles; + +#if NETFRAMEWORK +using System.Runtime.Versioning; +using System.Security; +using System.Security.Permissions; +using System.Text; +#endif + +namespace Microsoft.Data.SqlTypes +{ + /// + public sealed class SqlFileStream : Stream + { + // NOTE: if we ever unseal this class, be sure to specify the Name, SafeFileHandle, and + // TransactionContext accessors as virtual methods. Doing so now on a sealed class + // generates a compiler error (CS0549) + + #region Constants + + /// + /// From System.IO.FileStream implementation: DefaultBufferSize = 4096; + /// SQLBUVSTS# 193123 - disable lazy flushing of written data in order to prevent potential + /// exceptions during Close/Finalization. Since System.IO.FileStream will not allow for a + /// zero byte buffer, we'll create a one byte buffer which, in normal usage, will not be + /// used and the user buffer will automatically flush directly to the disk cache. In + /// pathological scenarios where the client is writing a single byte at a time, we'll + /// explicitly call flush ourselves. + /// + private const int DefaultBufferSize = 1; + + private const FileOptions AllowedOptions = FileOptions.WriteThrough | + FileOptions.Asynchronous | + FileOptions.RandomAccess | + FileOptions.SequentialScan; + + private const ushort IoControlCodeFunctionCode = 2392; + + /// + /// Used as the extended attribute name string. Preallocated as a byte array for ease of + /// copying into the EA struct. Value is "Filestream_Transaction_Tag" + /// + private static readonly byte[] EaNameString = new byte[] + { + (byte)'F', (byte)'i', (byte)'l', (byte)'e', (byte)'s', (byte)'t', (byte)'r', (byte)'e', (byte)'a', + (byte)'m', (byte)'_', (byte)'T', (byte)'r', (byte)'a', (byte)'n', (byte)'s', (byte)'a', (byte)'c', + (byte)'t', (byte)'i', (byte)'o', (byte)'n', (byte)'_', (byte)'T', (byte)'a', (byte)'g', (byte) '\0', + }; + + #if NETFRAMEWORK + private const short MaxWin32PathLengthChars = short.MaxValue - 1; + private static readonly char[] InvalidPathCharacters = Path.GetInvalidPathChars(); + #endif + + #endregion + + #region Member Variables + + /// + /// Counter for how many instances have been created, used in EventSource. + /// + private static int _objectTypeCount; + + private readonly string _path; + private readonly byte[] _transactionContext; + private readonly int _objectId = Interlocked.Increment(ref _objectTypeCount); + + private FileStream _fileStream; + private bool _isDisposed; + + #endregion + + #region Construction / Destruction + + /// + public SqlFileStream(string path, byte[] transactionContext, FileAccess access) + : this(path, transactionContext, access, FileOptions.None, 0) + { } + + /// + public SqlFileStream( + string path, + byte[] transactionContext, + FileAccess access, + FileOptions options, + long allocationSize) + { + #if NETFRAMEWORK + const string scopeFormat = " {0} access={1} options={2} path='{3}'"; + #else + const string scopeFormat = "SqlFileStream.ctor | API | Object Id {0} | Access {1} | Options {2} | Path '{3}'"; + #endif + + long scopeId = SqlClientEventSource.Log.TryScopeEnterEvent(scopeFormat, _objectId, (int)access, (int)options, path); + using (TryEventScope.Create(scopeId)) + { + //----------------------------------------------------------------- + // precondition validation + if (transactionContext == null) + { + throw ADP.ArgumentNull("transactionContext"); + } + + if (path == null) + { + throw ADP.ArgumentNull("path"); + } + //----------------------------------------------------------------- + + _isDisposed = false; + _fileStream = null; + + string normalizedPath = GetFullPathInternal(path); + OpenSqlFileStream(normalizedPath, transactionContext, access, options, allocationSize); + + // only set internal state once the file has actually been successfully opened + _path = normalizedPath; + _transactionContext = (byte[])transactionContext.Clone(); + } + } + + // NOTE: this destructor will only be called only if the Dispose + // method is not called by a client, giving the class a chance + // to finalize properly (i.e., free unmanaged resources) + /// + ~SqlFileStream() + { + Dispose(false); + } + + #endregion + + #region Properties + + /// + public override bool CanRead + { + get + { + ThrowIfDisposed(); + return _fileStream.CanRead; + } + } + + /// + public override bool CanSeek + { + get + { + ThrowIfDisposed(); + return _fileStream.CanSeek; + } + } + + /// + #if NETFRAMEWORK + [ComVisible(false)] + #endif + public override bool CanTimeout + { + get + { + ThrowIfDisposed(); + return _fileStream.CanTimeout; + } + } + + /// + public override bool CanWrite + { + get + { + ThrowIfDisposed(); + return _fileStream.CanWrite; + } + } + + /// + public override long Length + { + get + { + ThrowIfDisposed(); + return _fileStream.Length; + } + } + + /// + public string Name + { + get + { + // Assert that path has been properly processed via GetFullPathInternal + // (e.g. m_path hasn't been set directly) + AssertPathFormat(_path); + return _path; + } + } + + /// + public override long Position + { + get + { + ThrowIfDisposed(); + return _fileStream.Position; + } + set + { + ThrowIfDisposed(); + _fileStream.Position = value; + } + } + + /// + #if NETFRAMEWORK + [ComVisible(false)] + #endif + public override int ReadTimeout + { + get + { + ThrowIfDisposed(); + return _fileStream.ReadTimeout; + } + set + { + ThrowIfDisposed(); + _fileStream.ReadTimeout = value; + } + } + + /// + public byte[] TransactionContext => + (byte[]) _transactionContext?.Clone(); + + /// + #if NETFRAMEWORK + [ComVisible(false)] + #endif + public override int WriteTimeout + { + get + { + ThrowIfDisposed(); + return _fileStream.WriteTimeout; + } + set + { + ThrowIfDisposed(); + _fileStream.WriteTimeout = value; + } + } + + #endregion + + #region Public Methods + + /// + #if NETFRAMEWORK + [HostProtection(ExternalThreading = true)] + #endif + public override IAsyncResult BeginRead(byte[] buffer, int offset, int count, AsyncCallback callback, object state) + { + ThrowIfDisposed(); + return _fileStream.BeginRead(buffer, offset, count, callback, state); + } + + /// + #if NETFRAMEWORK + [HostProtection(ExternalThreading = true)] + #endif + public override IAsyncResult BeginWrite(byte[] buffer, int offset, int count, AsyncCallback callback, object state) + { + ThrowIfDisposed(); + + IAsyncResult asyncResult = _fileStream.BeginWrite(buffer, offset, count, callback, state); + + // SQLBUVSTS# 193123 - disable lazy flushing of written data in order to prevent + // potential exceptions during Close/Finalization. Since System.IO.FileStream will + // not allow for a zero byte buffer, we'll create a one byte buffer which, in normal + // usage, will not be used and the user buffer will automatically flush directly to + // the disk cache. In pathological scenarios where the client is writing a single + // byte at a time, we'll explicitly call flush ourselves. + if (count == 1) + { + // calling flush here will mimic the internal control flow of System.IO.FileStream + _fileStream.Flush(); + } + + return asyncResult; + } + + /// + public override int EndRead(IAsyncResult asyncResult) + { + ThrowIfDisposed(); + return _fileStream.EndRead(asyncResult); + } + + /// + public override void EndWrite(IAsyncResult asyncResult) + { + ThrowIfDisposed(); + _fileStream.EndWrite(asyncResult); + } + + /// + public override void Flush() + { + ThrowIfDisposed(); + _fileStream.Flush(); + } + + /// + public override int Read([In, Out] byte[] buffer, int offset, int count) + { + ThrowIfDisposed(); + return _fileStream.Read(buffer, offset, count); + } + + /// + public override int ReadByte() + { + ThrowIfDisposed(); + return _fileStream.ReadByte(); + } + + /// + public override long Seek(long offset, SeekOrigin origin) + { + ThrowIfDisposed(); + return _fileStream.Seek(offset, origin); + } + + /// + public override void SetLength(long value) + { + ThrowIfDisposed(); + _fileStream.SetLength(value); + } + + /// + public override void Write(byte[] buffer, int offset, int count) + { + ThrowIfDisposed(); + + _fileStream.Write(buffer, offset, count); + + // SQLBUVSTS# 193123 - disable lazy flushing of written data in order to prevent + // potential exceptions during Close/Finalization. Since System.IO.FileStream will + // not allow for a zero byte buffer, we'll create a one byte buffer which, in normal + // usage, will cause System.IO.FileStream to utilize the user-supplied buffer and + // automatically flush the data directly to the disk cache. In pathological scenarios + // where the user is writing a single byte at a time, we'll explicitly call flush ourselves. + if (count == 1) + { + // calling flush here will mimic the internal control flow of System.IO.FileStream + _fileStream.Flush(); + } + } + + /// + public override void WriteByte(byte value) + { + ThrowIfDisposed(); + + _fileStream.WriteByte(value); + + // SQLBUVSTS# 193123 - disable lazy flushing of written data in order to prevent + // potential exceptions during Close/Finalization. Since our internal buffer is + // only a single byte in length, the provided user data will always be cached. + // As a result, we need to be sure to flush the data to disk ourselves. + + // calling flush here will mimic the internal control flow of System.IO.FileStream + _fileStream.Flush(); + } + + #endregion + + /// + protected override void Dispose(bool disposing) + { + try + { + if (!_isDisposed) + { + try + { + if (disposing) + { + if (_fileStream != null) + { + _fileStream.Close(); + _fileStream = null; + } + } + } + finally + { + _isDisposed = true; + } + } + } + finally + { + base.Dispose(disposing); + } + } + + #region Private Helper Methods + + [Conditional("DEBUG")] + private static void AssertPathFormat(string path) + { + Debug.Assert(path != null); + Debug.Assert(path == path.Trim()); + Debug.Assert(path.Length > 0); + Debug.Assert(path.StartsWith(@"\\", StringComparison.OrdinalIgnoreCase)); + + #if NETFRAMEWORK + // * Path length storage (in bytes) in UNICODE_STRING is limited to ushort.MaxValue + // *bytes* (short.MaxValue *chars*) + // * GetFullPathName API of kernel32 dos not accept paths with length (in chars) + // greater than 32766 (which is short.MaxValue - 1, where -1 allows for NULL termination) + Debug.Assert(path.Length <= MaxWin32PathLengthChars); + Debug.Assert(path.IndexOfAny(InvalidPathCharacters) < 0); + #endif + } + + #if NETFRAMEWORK + private static void DemandAccessPermission (string path, FileAccess access) + { + // Ensure we demand for a valid path + AssertPathFormat(path); + + FileIOPermissionAccess demandPermissions; + switch (access) + { + case FileAccess.Read: + demandPermissions = FileIOPermissionAccess.Read; + break; + + case FileAccess.Write: + demandPermissions = FileIOPermissionAccess.Write; + break; + + case FileAccess.ReadWrite: + default: + // the caller have to validate the value of 'access' parameter + Debug.Assert(access is FileAccess.ReadWrite); + demandPermissions = FileIOPermissionAccess.Read | FileIOPermissionAccess.Write; + break; + } + + FileIOPermission filePerm; + bool pathTooLong = false; + + // Check for read and/or write permissions + try + { + filePerm = new FileIOPermission(demandPermissions, path); + filePerm.Demand(); + } + catch (PathTooLongException e) + { + pathTooLong = true; + ADP.TraceExceptionWithoutRethrow(e); + } + + if (pathTooLong) + { + // SQLBUVSTS bugs 192677 and 203422: currently, FileIOPermission does not support + // path longer than MAX_PATH (260) so we cannot demand permissions for long files. + // We are going to open bug for FileIOPermission to support this. + + // In the meanwhile, we agreed to have try-catch block on the permission demand + // instead of checking the path length. This way, if/when the 260-chars limitation + // is fixed in FileIOPermission, we will not need to change our code + + // since we do not want to relax security checks, we have to demand this permission + // for AllFiles in order to continue! + // Note: demand for AllFiles will fail in scenarios where the running code does not + // have this permission (such as ASP.Net) and the only workaround will be reducing + // the total path length, which means reducing the length of SqlFileStream path + // components, such as instance name, table name, etc. to fit into 260 characters. + filePerm = new FileIOPermission(PermissionState.Unrestricted) { AllFiles = demandPermissions }; + filePerm.Demand(); + } + } + #endif + + #if NETFRAMEWORK + [ResourceExposure(ResourceScope.Machine)] + [ResourceConsumption(ResourceScope.Machine)] + #endif + private static string GetFullPathInternal(string path) + { + //----------------------------------------------------------------- + // Precondition Validation + + // Remove leading and trailing whitespace + path = path.Trim(); + if (path.Length == 0) + { + throw ADP.Argument(StringsHelper.GetString(Strings.SqlFileStream_InvalidPath), "path"); + } + + // Make sure path is a UNC path and not a DOS device path + if (!path.StartsWith(@"\\", StringComparison.Ordinal) || IsDevicePath(path)) + { + throw ADP.Argument(StringsHelper.GetString(Strings.SqlFileStream_InvalidPath), "path"); + } + //----------------------------------------------------------------- + + // Normalize the path + #if NETFRAMEWORK + // In netfx, the System.IO.Path.GetFullPath requires PathDiscovery permission, which is + // not necessary since we are dealing with network paths. Thus, we are going directly + // to the GetFullPathName function in kernel32.dll (SQLBUVSTS01 192677, 193221) + path = GetFullPathNameNetfx(path); + #else + path = Path.GetFullPath(path); + #endif + + // Validate after normalization + // Make sure path is a UNC path (not a device or device UNC path) + if (IsDevicePath(path) || IsDeviceUncPath(path)) + { + throw ADP.Argument(StringsHelper.GetString(Strings.SqlFileStream_PathNotValidDiskResource), "path"); + } + + return path; + } + + #if NETFRAMEWORK + /// + /// Makes the call to GetFullPathName in kernel32.dll and handles special conditions that + /// may arise. + /// + /// + /// Do not use this in netcore - Path.GetFullPathName does not require additional + /// permissions like netfx does. + /// + [ResourceExposure(ResourceScope.Machine)] + [ResourceConsumption(ResourceScope.Machine)] + private static string GetFullPathNameNetfx(string path) + { + // In the most common case where (SqlFileStream),the 'full path' is expected to be the + // same size as the provided path. However, we still need to allocate one extra char + // for null termination. + // Note: StringBuilder has a magical capability to be compatible with char*, and also + // provides wide-character support w/o messing around with encodings. + StringBuilder buffer = new StringBuilder(path.Length + 1); + + // If everything goes correctly, we only need to call this once + int fullPathLength = Interop.Kernel32.GetFullPathName(path, buffer.Capacity, buffer, IntPtr.Zero); + + // If our buffer was smaller than required, the buffer will be empty, but the full + // path size will be the size we should reallocate to. + if (fullPathLength > buffer.Capacity) + { + // Reallocate the buffer and try again + buffer.Capacity = fullPathLength; + fullPathLength = Interop.Kernel32.GetFullPathName(path, buffer.Capacity, buffer, IntPtr.Zero); + } + + // If the method tells us the full path length is 0, then we have an error condition. + if (fullPathLength == 0) + { + // GetFullPathName call failed + int lastError = Marshal.GetLastWin32Error(); + if (lastError == 0) + { + // We've found that in some cases, GetFullPathName will fail but does not set the + // last error correctly. For example, when the path provided is longer than 32k, + // the return value will be 0 (indicating failure), but GetLastWin32Error is also + // zero (indicating success). + // In this case, we will throw an invalid path exception. + throw ADP.Argument(StringsHelper.GetString(Strings.SqlFileStream_InvalidPath), "path"); + } + + // In any other error condition, we will throw a Win32 exception. + Win32Exception e = new Win32Exception(lastError); + ADP.TraceExceptionAsReturnValue(e); + throw e; + } + + // this should not happen since we already reallocate + Debug.Assert(fullPathLength <= buffer.Capacity, + string.Format( + CultureInfo.InvariantCulture, + "second call to GetFullPathName returned greater size: {0} > {1}", + fullPathLength, + buffer.Capacity)); + + Debug.Assert(buffer.Length <= MaxWin32PathLengthChars, + "kernel32.dll GetFullPathName returned path longer than max"); + + return buffer.ToString(); + } + #endif + + /// + /// This method exists to ensure that the requested path name is unique so that SMB/DNS is + /// prevented from collapsing a file open request to a file handle opened previously. In + /// the SQL FILESTREAM case, this would likely be a file open in another transaction, so + /// this mechanism ensures isolation. + /// + private static string InitializeNtPath(string path) + { + // Ensure we have validated and normalized the path before + AssertPathFormat(path); + + string uniqueId = Guid.NewGuid().ToString("N"); + return IsDeviceUncPath(path) + ? string.Format(CultureInfo.InvariantCulture, @"{0}\{1}", path.Replace(@"\\.", @"\??"), uniqueId) + : string.Format(CultureInfo.InvariantCulture, @"\??\UNC\{0}\{1}", path.Trim('\\'), uniqueId); + } + + /// + /// Returns if the path uses any of the DOS device path syntaxes + /// + /// \\.\ + /// \\?\ + /// \??\ + /// + /// + /// + /// Implementation lifted from System.IO.PathInternal + /// https://github.com/dotnet/runtime/blob/main/src/libraries/Common/src/System/IO/PathInternal.Windows.cs + /// + private static bool IsDevicePath(string path) + { + return IsExtendedPath(path) + || + ( + path.Length >= 4 + && IsDirectorySeparator(path[0]) + && IsDirectorySeparator(path[1]) + && (path[2] == '.' || path[2] == '?') + && IsDirectorySeparator(path[3]) + ); + } + + /// + /// Returns true if the path is a device UNC path: + /// + /// \\.\UNC\ + /// \\?\UNC\ + /// \??\UNC\ + /// + /// + /// + /// Implementation lifted from System.IO.PathInternal + /// https://github.com/dotnet/runtime/blob/main/src/libraries/Common/src/System/IO/PathInternal.Windows.cs + /// + private static bool IsDeviceUncPath(string path) + { + return path.Length >= 8 + && IsDevicePath(path) + && IsDirectorySeparator(path[7]) + && path[4] == 'U' + && path[5] == 'N' + && path[6] == 'C'; + } + + /// + /// Returns if the given character is a directory separator. + /// + /// + /// Implementation lifted from System.IO.PathInternal. + /// https://github.com/dotnet/runtime/blob/main/src/libraries/Common/src/System/IO/PathInternal.Windows.cs + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static bool IsDirectorySeparator(char c) => + c == Path.DirectorySeparatorChar || c == Path.AltDirectorySeparatorChar; + + /// + /// Returns if the path uses the canonical form of extended syntax + /// (\\?\ or \??\). If the path matches exactly (cannot use alternative + /// directory separators) Windows will skip normalization and path length checks. + /// + /// + /// Implementation lifted from System.IO.PathInternal. + /// https://github.com/dotnet/runtime/blob/main/src/libraries/Common/src/System/IO/PathInternal.Windows.cs + /// + private static bool IsExtendedPath(string path) + { + return path.Length >= 4 + && path[0] == '\\' + && (path[1] == '\\' || path[1] == '?') + && path[2] == '?' + && path[3] == '\\'; + } + + private static FileStream OpenFileStream(SafeFileHandle fileHandle, FileAccess access, FileOptions options) + { + #if NETFRAMEWORK + // NOTE: We need to assert UnmanagedCode permissions for this constructor. This is + // relatively benign in that we've done much the same validation as in the + // FileStream(string path, ...) ctor case most notably, validating that the handle + // type corresponds to an on-disk file. + // This likely only applies in partially trusted environments and is not required in + // netcore since CAS was removed. + bool bRevertAssert = false; + try + { + SecurityPermission sp = new SecurityPermission(SecurityPermissionFlag.UnmanagedCode); + sp.Assert(); + bRevertAssert = true; + + return new FileStream(fileHandle, access, DefaultBufferSize,(options & FileOptions.Asynchronous) != 0); + } + finally + { + if (bRevertAssert) + { + CodeAccessPermission.RevertAssert(); + } + } + #else + return new FileStream(fileHandle, access, DefaultBufferSize, (options & FileOptions.Asynchronous) != 0); + #endif + } + + private void OpenSqlFileStream( + string path, + byte[] transactionContext, + FileAccess access, + FileOptions options, + long allocationSize) + { + if (access is not (FileAccess.Read or FileAccess.Write or FileAccess.ReadWrite)) + { + throw ADP.ArgumentOutOfRange("access"); + } + + if ((options & ~AllowedOptions) != 0) + { + throw ADP.ArgumentOutOfRange("options"); + } + + #if NETFRAMEWORK + // Ensure the running code has permission to read/write the file + DemandAccessPermission(path, access); + #endif + + Interop.NtDll.CreateOptions createOptions = 0; + Interop.NtDll.CreateDisposition createDisposition = 0; + Interop.NtDll.DesiredAccess desiredAccess = Interop.NtDll.DesiredAccess.FILE_READ_ATTRIBUTES | + Interop.NtDll.DesiredAccess.SYNCHRONIZE; + FileShare shareAccess = 0; + + switch (access) + { + case FileAccess.Read: + desiredAccess |= Interop.NtDll.DesiredAccess.FILE_READ_DATA; + shareAccess = FileShare.Delete | FileShare.ReadWrite; + createDisposition = Interop.NtDll.CreateDisposition.FILE_OPEN; + break; + + case FileAccess.Write: + desiredAccess |= Interop.NtDll.DesiredAccess.FILE_WRITE_DATA; + shareAccess = FileShare.Delete | FileShare.Read; + createDisposition = Interop.NtDll.CreateDisposition.FILE_OVERWRITE; + break; + + case FileAccess.ReadWrite: + desiredAccess |= Interop.NtDll.DesiredAccess.FILE_READ_DATA | + Interop.NtDll.DesiredAccess.FILE_WRITE_DATA; + shareAccess = FileShare.Delete | FileShare.Read; + createDisposition = Interop.NtDll.CreateDisposition.FILE_OVERWRITE; + break; + + // Note: default case is heuristically unreachable due to check above. + } + + if ((options & FileOptions.WriteThrough) != 0) + { + createOptions |= Interop.NtDll.CreateOptions.FILE_WRITE_THROUGH; + } + + if ((options & FileOptions.Asynchronous) == 0) + { + createOptions |= Interop.NtDll.CreateOptions.FILE_SYNCHRONOUS_IO_NONALERT; + } + + if ((options & FileOptions.SequentialScan) != 0) + { + createOptions |= Interop.NtDll.CreateOptions.FILE_SEQUENTIAL_ONLY; + } + + if ((options & FileOptions.RandomAccess) != 0) + { + createOptions |= Interop.NtDll.CreateOptions.FILE_RANDOM_ACCESS; + } + + SafeFileHandle fileHandle = null; + try + { + // NOTE: the Name property is intended to reveal the publicly available moniker for the + // FILESTREAM attributed column data. We will not surface the internal processing that + // takes place to create the mappedPath. + string mappedPath = InitializeNtPath(path); + + Interop.Kernel32.SetThreadErrorMode(Interop.Kernel32.SEM_FAILCRITICALERRORS, out uint oldMode); + + // Make the interop call to open the file + int retval; + try + { + if (transactionContext.Length >= ushort.MaxValue) + { + throw ADP.ArgumentOutOfRange("transactionContext"); + } + + IntPtr handle; + + #if NETFRAMEWORK + const string traceEventMessage = " {0}, desiredAccess=0x{1}, allocationSize={2}, fileAttributes=0x00, shareAccess=0x{3}, dwCreateDisposition=0x{4}, createOptions=0x{5}"; + (retval, handle) = Interop.NtDll.CreateFile( + path: mappedPath, + eaName: EaNameString, + eaValue: transactionContext, + desiredAccess: desiredAccess, + fileAttributes: 0, + shareAccess: shareAccess, + createDisposition: createDisposition, + createOptions: createOptions, + impersonationLevel: Interop.ImpersonationLevel.SecurityAnonymous, + isDynamicTracking: false, + isEffectiveOnly: false); + #else + const string traceEventMessage = "SqlFileStream.OpenSqlFileStream | ADV | Object Id {0}, Desired Access 0x{1}, Allocation Size {2}, File Attributes 0, Share Access 0x{3}, Create Disposition 0x{4}, Create Options 0x{5}"; + (retval, handle) = Interop.NtDll.CreateFile( + path: mappedPath, + eaName: EaNameString, + eaValue: transactionContext, + desiredAccess: desiredAccess, + fileAttributes: 0, + shareAccess: shareAccess, + createDisposition: createDisposition, + createOptions: createOptions); + #endif + + SqlClientEventSource.Log.TryAdvancedTraceEvent(traceEventMessage, _objectId, (int)desiredAccess, allocationSize, (int)shareAccess, createDisposition, createOptions); + + fileHandle = new SafeFileHandle(handle, true); + } + finally + { + Interop.Kernel32.SetThreadErrorMode(oldMode, out oldMode); + } + + // Handle error codes from the interop call + switch (retval) + { + case 0: + break; + + case Interop.Errors.ERROR_SHARING_VIOLATION: + throw ADP.InvalidOperation(StringsHelper.GetString(Strings.SqlFileStream_FileAlreadyInTransaction)); + + case Interop.Errors.ERROR_INVALID_PARAMETER: + throw ADP.Argument(StringsHelper.GetString(Strings.SqlFileStream_InvalidParameter)); + + case Interop.Errors.ERROR_FILE_NOT_FOUND: + DirectoryNotFoundException dirNotFoundException = new DirectoryNotFoundException(); + ADP.TraceExceptionAsReturnValue(dirNotFoundException); + throw dirNotFoundException; + + default: + uint error = Interop.NtDll.RtlNtStatusToDosError(retval); + if (error == Interop.NtDll.ERROR_MR_MID_NOT_FOUND) + { + // status code could not be mapped to a Win32 error code + error = (uint)retval; + } + + Win32Exception win32Exception = new Win32Exception(unchecked((int)error)); + ADP.TraceExceptionAsReturnValue(win32Exception); + throw win32Exception; + } + + // Make sure the file handle is usable for us + if (fileHandle.IsInvalid) + { + Win32Exception e = new Win32Exception(Interop.Errors.ERROR_INVALID_HANDLE); + ADP.TraceExceptionAsReturnValue(e); + throw e; + } + + if (Interop.Kernel32.GetFileType(fileHandle) != Interop.Kernel32.FileTypes.FILE_TYPE_DISK) + { + fileHandle.Dispose(); + throw ADP.Argument(StringsHelper.GetString(Strings.SqlFileStream_PathNotValidDiskResource)); + } + + // If the user is opening the SQL FileStream in read/write mode, we assume that + // they want to scan through current data and then append new data to the end, so + // we need to tell SQL Server to preserve the existing file contents. + if (access is FileAccess.ReadWrite) + { + uint ioControlCode = Interop.Kernel32.CTL_CODE( + Interop.Kernel32.FILE_DEVICE_FILE_SYSTEM, + IoControlCodeFunctionCode, + (byte)Interop.Kernel32.IoControlTransferType.METHOD_BUFFERED, + (byte)Interop.Kernel32.IoControlCodeAccess.FILE_ANY_ACCESS); + + if (!Interop.Kernel32.DeviceIoControl(fileHandle, ioControlCode, IntPtr.Zero, 0, IntPtr.Zero, 0, out _, IntPtr.Zero)) + { + Win32Exception e = new Win32Exception(Marshal.GetLastWin32Error()); + ADP.TraceExceptionAsReturnValue(e); + throw e; + } + } + + // Now that we've successfully opened a handle on the path and verified that it is + // a file, use the SafeFileHandle to initialize our internal FileStream instance. + Debug.Assert(_fileStream is null); + _fileStream = OpenFileStream(fileHandle, access, options); + } + catch + { + if (fileHandle is not null && !fileHandle.IsInvalid) + { + fileHandle.Dispose(); + } + + throw; + } + } + + private void ThrowIfDisposed() + { + if (_isDisposed) + { + throw ADP.ObjectDisposed(this); + } + } + + #endregion + } +} diff --git a/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/SqlFileStreamTest/SqlFileStreamTest.cs b/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/SqlFileStreamTest/SqlFileStreamTest.cs index c34b7e9ceb..7ac9809aa3 100644 --- a/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/SqlFileStreamTest/SqlFileStreamTest.cs +++ b/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/SqlFileStreamTest/SqlFileStreamTest.cs @@ -243,7 +243,7 @@ LOG ON catch (SqlException e) { Console.WriteLine("File Stream database could not be setup. " + e.Message); - fileStreamDir = null; + throw; } return s_fileStreamDBName; }