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;
}