From 459d1f27f619e5f3c7ac4e012efa30f2f48b5aba Mon Sep 17 00:00:00 2001 From: Dean Ferreyra Date: Sun, 7 Nov 2021 14:05:39 -0800 Subject: Formalize inclusion of code from https://github.com/dotnet Fix SystemIOWindows.PathGetFullPath per review comments. Add unit tests for SystemIOWindows.PathGetFullPath and SystemIOWindows.PrefixWithUNC. Also, fix a bug in the SystemIOWindows.PrefixWithUNC change that was exposed by these new unit tests. In BackendToolTests, remove unused [Setup] and [TearDown] methods. --- .../IO/DotNetRuntime.System.IO.Path.Windows.cs | 59 +++++++++ ...DotNetRuntime.System.IO.PathInternal.Windows.cs | 133 +++++++++++++++++++++ Duplicati/Library/Common/IO/SystemIOWindows.cs | 103 +++------------- 3 files changed, 210 insertions(+), 85 deletions(-) create mode 100755 Duplicati/Library/Common/IO/DotNetRuntime.System.IO.Path.Windows.cs create mode 100755 Duplicati/Library/Common/IO/DotNetRuntime.System.IO.PathInternal.Windows.cs (limited to 'Duplicati/Library/Common/IO') diff --git a/Duplicati/Library/Common/IO/DotNetRuntime.System.IO.Path.Windows.cs b/Duplicati/Library/Common/IO/DotNetRuntime.System.IO.Path.Windows.cs new file mode 100755 index 000000000..6a830a955 --- /dev/null +++ b/Duplicati/Library/Common/IO/DotNetRuntime.System.IO.Path.Windows.cs @@ -0,0 +1,59 @@ +// Adapted from https://raw.githubusercontent.com/dotnet/runtime/v5.0.12/src/libraries/System.Private.CoreLib/src/System/IO/Path.cs + +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +// The MIT License (MIT) +// +// Copyright(c).NET Foundation and Contributors +// +// All rights reserved. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + +using System; + +namespace Duplicati.Library.Common.IO +{ + public static class DotNetRuntimePathWindows + { + /// + /// Returns true 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). + /// Returns false if the path specified is relative to the current drive or working directory. + /// + /// + /// Handles paths that use the alternate directory separator. It is a frequent mistake to + /// assume that rooted paths 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). + /// + /// + /// Thrown if is null. + /// + public static bool IsPathFullyQualified(string path) + { + if (path == null) + throw new ArgumentNullException(nameof(path)); + + return !PathInternalWindows.IsPartiallyQualified(path); + } + } +} diff --git a/Duplicati/Library/Common/IO/DotNetRuntime.System.IO.PathInternal.Windows.cs b/Duplicati/Library/Common/IO/DotNetRuntime.System.IO.PathInternal.Windows.cs new file mode 100755 index 000000000..75a117392 --- /dev/null +++ b/Duplicati/Library/Common/IO/DotNetRuntime.System.IO.PathInternal.Windows.cs @@ -0,0 +1,133 @@ +// Adapted from https://github.com/dotnet/runtime/blob/v5.0.12/src/libraries/Common/src/System/IO/PathInternal.Windows.cs + +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +// The MIT License (MIT) +// +// Copyright(c).NET Foundation and Contributors +// +// All rights reserved. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + +using System.Runtime.CompilerServices; +using System.IO; + +namespace Duplicati.Library.Common.IO +{ + /// Contains internal path helpers that are shared between many projects. + internal static class PathInternalWindows + { + // 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 string ExtendedDevicePathPrefix = @"\\?\"; + internal const string UncPathPrefix = @"\\"; + internal const string UncDevicePrefixToInsert = @"?\UNC\"; + internal const string UncExtendedPathPrefix = @"\\?\UNC\"; + internal const string DevicePathPrefix = @"\\.\"; + + internal const int MaxShortPath = 260; + + // \\?\, \\.\, \??\ + internal const int DevicePrefixLength = 4; + + /// + /// 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')); + } + + /// + /// 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(string path) + { + if (path.Length < 2) + { + // It isn't fixed, it must be relative. There is no way to specify a fixed + // path with one character (or less). + return true; + } + + if (IsDirectorySeparator(path[0])) + { + // There is no valid way to specify a relative path with two initial slashes or + // \? as ? isn't valid for drive relative paths and \??\ is equivalent to \\?\ + return !(path[1] == '?' || IsDirectorySeparator(path[1])); + } + + // The only way to specify a fixed path that doesn't begin with two slashes + // is the drive, colon, slash format- i.e. C:\ + return !((path.Length >= 3) + && (path[1] == Path.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 == Path.DirectorySeparatorChar || c == Path.AltDirectorySeparatorChar; + } + } +} diff --git a/Duplicati/Library/Common/IO/SystemIOWindows.cs b/Duplicati/Library/Common/IO/SystemIOWindows.cs index f1486c154..53a0ceb33 100644 --- a/Duplicati/Library/Common/IO/SystemIOWindows.cs +++ b/Duplicati/Library/Common/IO/SystemIOWindows.cs @@ -29,9 +29,9 @@ namespace Duplicati.Library.Common.IO { public struct SystemIOWindows : ISystemIO { - private const string UNCPREFIX = @"\\?\"; - private const string UNCPREFIX_SERVER = @"\\?\UNC\"; - private const string PATHPREFIX_SERVER = @"\\"; + private const string UNCPREFIX = PathInternalWindows.ExtendedDevicePathPrefix; + private const string UNCPREFIX_SERVER = PathInternalWindows.UncExtendedPathPrefix; + private const string PATHPREFIX_SERVER = PathInternalWindows.UncPathPrefix; private const string PATHPREFIX_SERVER_ALT = @"//"; private static readonly string DIRSEP = Util.DirectorySeparatorString; @@ -47,12 +47,12 @@ namespace Duplicati.Library.Common.IO // For example: \\?\C:\Temp\foo.txt or \\?\UNC\example.com\share\foo.txt return path; } - else if (IsPrefixedWithBasicUNC(path)) + else if (IsPrefixedWithBasicUNC(path) && !HasRelativePathComponents(path)) { // For example: \\example.com\share\foo.txt or //example.com/share/foo.txt return UNCPREFIX_SERVER + ConvertSlashes(path.Substring(PATHPREFIX_SERVER.Length)); } - else if (IsPathFullyQualified(path) && !HasRelativePathComponents(path)) + else if (DotNetRuntimePathWindows.IsPathFullyQualified(path) && !HasRelativePathComponents(path)) { // For example: C:\Temp\foo.txt or C:/Temp/foo.txt return UNCPREFIX + ConvertSlashes(path); @@ -230,85 +230,6 @@ namespace Duplicati.Library.Common.IO System.IO.Directory.SetAccessControl(PrefixWithUNC(path), rules); } - #region Adapted from https://github.com/dotnet/runtime (MIT license) - /// - /// Returns true 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). - /// Returns false if the path specified is relative to the current drive or working directory. - /// - /// - /// Handles paths that use the alternate directory separator. It is a frequent mistake to - /// assume that rooted paths 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). - /// - /// - /// Thrown if is null. - /// - public static bool IsPathFullyQualified(string path) - { - if (path == null) - throw new ArgumentNullException(nameof(path)); - - return !IsPartiallyQualified(path); - } - - /// - /// 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(string path) - { - if (path.Length < 2) - { - // It isn't fixed, it must be relative. There is no way to specify a fixed - // path with one character (or less). - return true; - } - - if (IsDirectorySeparator(path[0])) - { - // There is no valid way to specify a relative path with two initial slashes or - // \? as ? isn't valid for drive relative paths and \??\ is equivalent to \\?\ - return !(path[1] == '?' || IsDirectorySeparator(path[1])); - } - - // The only way to specify a fixed path that doesn't begin with two slashes - // is the drive, colon, slash format- i.e. C:\ - return !((path.Length >= 3) - && (path[1] == Path.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. - /// - internal static bool IsDirectorySeparator(char c) - { - return c == Path.DirectorySeparatorChar || c == Path.AltDirectorySeparatorChar; - } - - /// - /// 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'); - } - #endregion - #region ISystemIO implementation public void DirectoryCreate(string path) { @@ -539,7 +460,19 @@ namespace Duplicati.Library.Common.IO public string PathGetFullPath(string path) { - return PrefixWithUNC(Path.GetFullPath(path)); + // Desired behavior: + // 1. If path is already prefixed with \\?\, it should be left untouched + // 2. If path is not already prefixed with \\?\, the return value should also not be prefixed + // 3. If path is relative or has relative components, that should be resolved by calling Path.GetFullPath() + // 4. If path is not relative and has no relative components, prefix with \\?\ to prevent normalization from munging "problematic Windows paths" + if (IsPrefixedWithUNC(path)) + { + return path; + } + else + { + return StripUNCPrefix(Path.GetFullPath(PrefixWithUNC(path))); + } } public IFileEntry DirectoryEntry(string path) -- cgit v1.2.3