diff --git a/Duplicati/Library/Common/Duplicati.Library.Common.csproj b/Duplicati/Library/Common/Duplicati.Library.Common.csproj index 6fc16df6a..e85ee6a2d 100644 --- a/Duplicati/Library/Common/Duplicati.Library.Common.csproj +++ b/Duplicati/Library/Common/Duplicati.Library.Common.csproj @@ -62,6 +62,8 @@ + + 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) diff --git a/Duplicati/UnitTest/BackendToolTests.cs b/Duplicati/UnitTest/BackendToolTests.cs index 94616cccf..6c4b17122 100755 --- a/Duplicati/UnitTest/BackendToolTests.cs +++ b/Duplicati/UnitTest/BackendToolTests.cs @@ -11,18 +11,6 @@ namespace Duplicati.UnitTest [TestFixture] public class BackendToolTests : BasicSetupHelper { - [SetUp] - public override void SetUp() - { - base.SetUp(); - } - - [TearDown] - public override void TearDown() - { - base.TearDown(); - } - [Test] [Category("BackendTool")] public void Get() diff --git a/Duplicati/UnitTest/IOTests.cs b/Duplicati/UnitTest/IOTests.cs index 48b4ef5ca..84940d2b1 100644 --- a/Duplicati/UnitTest/IOTests.cs +++ b/Duplicati/UnitTest/IOTests.cs @@ -95,5 +95,457 @@ namespace Duplicati.UnitTest //In particular don't throw PathTooLongException Assert.Throws(() => SystemIO.IO_OS.GetDirectories(longPath)); } + + [Test] + public void TestPrefixWithUNCInWindowsClient() + { + if (!Platform.IsClientWindows) + { + return; + } + + // Normalization of basic relative paths, with both kinds of slashes + Assert.That( + SystemIOWindows.PrefixWithUNC(@"."), + Is.EqualTo(@".")); + Assert.That( + SystemIOWindows.PrefixWithUNC(@"temp"), + Is.EqualTo(@"temp")); + Assert.That( + SystemIOWindows.PrefixWithUNC(@"temp\file.txt"), + Is.EqualTo(@"temp\file.txt")); + Assert.That( + SystemIOWindows.PrefixWithUNC(@"\"), + Is.EqualTo(@"\")); + Assert.That( + SystemIOWindows.PrefixWithUNC(@"/"), + Is.EqualTo(@"/")); + Assert.That( + SystemIOWindows.PrefixWithUNC(@"\temp"), + Is.EqualTo(@"\temp")); + Assert.That( + SystemIOWindows.PrefixWithUNC(@"/temp"), + Is.EqualTo(@"/temp")); + Assert.That( + SystemIOWindows.PrefixWithUNC(@"/temp/file.txt"), + Is.EqualTo(@"/temp/file.txt")); + Assert.That( + SystemIOWindows.PrefixWithUNC(@"/temp/file.txt"), + Is.EqualTo(@"/temp/file.txt")); + + // Normalization of full qualified paths, but with relative components, with both kinds of slashes + Assert.That( + SystemIOWindows.PrefixWithUNC(@"C:\temp\."), + Is.EqualTo(@"C:\temp\.")); + Assert.That( + SystemIOWindows.PrefixWithUNC(@"C:/temp/."), + Is.EqualTo(@"C:/temp/.")); + Assert.That( + SystemIOWindows.PrefixWithUNC(@"C:\temp\.."), + Is.EqualTo(@"C:\temp\..")); + Assert.That( + SystemIOWindows.PrefixWithUNC(@"C:/temp/.."), + Is.EqualTo(@"C:/temp/..")); + Assert.That( + SystemIOWindows.PrefixWithUNC(@"C:\temp\..\folder"), + Is.EqualTo(@"C:\temp\..\folder")); + Assert.That( + SystemIOWindows.PrefixWithUNC(@"C:/temp/../folder"), + Is.EqualTo(@"C:/temp/../folder")); + Assert.That( + SystemIOWindows.PrefixWithUNC(@"C:\temp\.\file.txt"), + Is.EqualTo(@"C:\temp\.\file.txt")); + Assert.That( + SystemIOWindows.PrefixWithUNC(@"C:/temp/./file.txt"), + Is.EqualTo(@"C:/temp/./file.txt")); + Assert.That( + SystemIOWindows.PrefixWithUNC(@"\\example.com\share\."), + Is.EqualTo(@"\\example.com\share\.")); + Assert.That( + SystemIOWindows.PrefixWithUNC(@"//example.com/share/."), + Is.EqualTo(@"//example.com/share/.")); + Assert.That( + SystemIOWindows.PrefixWithUNC(@"\\example.com\share\.."), + Is.EqualTo(@"\\example.com\share\..")); + Assert.That( + SystemIOWindows.PrefixWithUNC(@"//example.com/share/.."), + Is.EqualTo(@"//example.com/share/..")); + Assert.That( + SystemIOWindows.PrefixWithUNC(@"\\example.com\share\..\folder"), + Is.EqualTo(@"\\example.com\share\..\folder")); + Assert.That( + SystemIOWindows.PrefixWithUNC(@"//example.com/share/../folder"), + Is.EqualTo(@"//example.com/share/../folder")); + Assert.That( + SystemIOWindows.PrefixWithUNC(@"\\example.com\share\.\file.txt"), + Is.EqualTo(@"\\example.com\share\.\file.txt")); + Assert.That( + SystemIOWindows.PrefixWithUNC(@"//example.com/share/./file.txt"), + Is.EqualTo(@"//example.com/share/./file.txt")); + + // Fully qualified paths with no relative components, with both kinds of slashes + Assert.That( + SystemIOWindows.PrefixWithUNC(@"C:\"), + Is.EqualTo(@"\\?\C:\")); + Assert.That( + SystemIOWindows.PrefixWithUNC(@"C:/"), + Is.EqualTo(@"\\?\C:\")); + Assert.That( + SystemIOWindows.PrefixWithUNC(@"C:\temp"), + Is.EqualTo(@"\\?\C:\temp")); + Assert.That( + SystemIOWindows.PrefixWithUNC(@"C:/temp"), + Is.EqualTo(@"\\?\C:\temp")); + Assert.That( + SystemIOWindows.PrefixWithUNC(@"C:\temp\file.txt"), + Is.EqualTo(@"\\?\C:\temp\file.txt")); + Assert.That( + SystemIOWindows.PrefixWithUNC(@"C:/temp/file.txt"), + Is.EqualTo(@"\\?\C:\temp\file.txt")); + Assert.That( + SystemIOWindows.PrefixWithUNC(@"\\example.com\share"), + Is.EqualTo(@"\\?\UNC\example.com\share")); + Assert.That( + SystemIOWindows.PrefixWithUNC(@"//example.com/share"), + Is.EqualTo(@"\\?\UNC\example.com\share")); + Assert.That( + SystemIOWindows.PrefixWithUNC(@"\\example.com\share\file.txt"), + Is.EqualTo(@"\\?\UNC\example.com\share\file.txt")); + Assert.That( + SystemIOWindows.PrefixWithUNC(@"//example.com/share/file.txt"), + Is.EqualTo(@"\\?\UNC\example.com\share\file.txt")); + + // Fully qualified paths with no relative components, but with problematic names, with both kinds of slashes + Assert.That( + SystemIOWindows.PrefixWithUNC(@"C:\temp."), + Is.EqualTo(@"\\?\C:\temp.")); + Assert.That( + SystemIOWindows.PrefixWithUNC(@"C:/temp."), + Is.EqualTo(@"\\?\C:\temp.")); + Assert.That( + SystemIOWindows.PrefixWithUNC(@"C:\temp.\file.txt"), + Is.EqualTo(@"\\?\C:\temp.\file.txt")); + Assert.That( + SystemIOWindows.PrefixWithUNC(@"C:/temp./file.txt"), + Is.EqualTo(@"\\?\C:\temp.\file.txt")); + Assert.That( + SystemIOWindows.PrefixWithUNC(@"C:\temp.\file.txt."), + Is.EqualTo(@"\\?\C:\temp.\file.txt.")); + Assert.That( + SystemIOWindows.PrefixWithUNC(@"C:/temp./file.txt."), + Is.EqualTo(@"\\?\C:\temp.\file.txt.")); + Assert.That( + SystemIOWindows.PrefixWithUNC(@"C:\temp "), + Is.EqualTo(@"\\?\C:\temp ")); + Assert.That( + SystemIOWindows.PrefixWithUNC(@"C:/temp "), + Is.EqualTo(@"\\?\C:\temp ")); + Assert.That( + SystemIOWindows.PrefixWithUNC(@"C:\temp\file.txt "), + Is.EqualTo(@"\\?\C:\temp\file.txt ")); + Assert.That( + SystemIOWindows.PrefixWithUNC(@"C:/temp/file.txt "), + Is.EqualTo(@"\\?\C:\temp\file.txt ")); + Assert.That( + SystemIOWindows.PrefixWithUNC(@"\\example.com\share."), + Is.EqualTo(@"\\?\UNC\example.com\share.")); + Assert.That( + SystemIOWindows.PrefixWithUNC(@"//example.com/share."), + Is.EqualTo(@"\\?\UNC\example.com\share.")); + Assert.That( + SystemIOWindows.PrefixWithUNC(@"\\example.com\share\file.txt."), + Is.EqualTo(@"\\?\UNC\example.com\share\file.txt.")); + Assert.That( + SystemIOWindows.PrefixWithUNC(@"//example.com/share./file.txt."), + Is.EqualTo(@"\\?\UNC\example.com\share.\file.txt.")); + Assert.That( + SystemIOWindows.PrefixWithUNC(@"\\example.com\share "), + Is.EqualTo(@"\\?\UNC\example.com\share ")); + Assert.That( + SystemIOWindows.PrefixWithUNC(@"//example.com/share "), + Is.EqualTo(@"\\?\UNC\example.com\share ")); + Assert.That( + SystemIOWindows.PrefixWithUNC(@"\\example.com\share\file.txt "), + Is.EqualTo(@"\\?\UNC\example.com\share\file.txt ")); + Assert.That( + SystemIOWindows.PrefixWithUNC(@"//example.com/share/file.txt "), + Is.EqualTo(@"\\?\UNC\example.com\share\file.txt ")); + + // Normalization disabled for paths with @"\\?\" prefix + Assert.That( + SystemIOWindows.PrefixWithUNC(@"\\?\C:\"), + Is.EqualTo(@"\\?\C:\")); + Assert.That( + SystemIOWindows.PrefixWithUNC(@"\\?\C:\temp"), + Is.EqualTo(@"\\?\C:\temp")); + Assert.That( + SystemIOWindows.PrefixWithUNC(@"\\?\C:\temp\file.txt"), + Is.EqualTo(@"\\?\C:\temp\file.txt")); + Assert.That( + SystemIOWindows.PrefixWithUNC(@"\\?\C:\temp."), + Is.EqualTo(@"\\?\C:\temp.")); + Assert.That( + SystemIOWindows.PrefixWithUNC(@"\\?\C:\temp.\file.txt"), + Is.EqualTo(@"\\?\C:\temp.\file.txt")); + Assert.That( + SystemIOWindows.PrefixWithUNC(@"\\?\C:\temp.\file.txt."), + Is.EqualTo(@"\\?\C:\temp.\file.txt.")); + Assert.That( + SystemIOWindows.PrefixWithUNC(@"\\?\C:\temp "), + Is.EqualTo(@"\\?\C:\temp ")); + Assert.That( + SystemIOWindows.PrefixWithUNC(@"\\?\C:\temp\file.txt "), + Is.EqualTo(@"\\?\C:\temp\file.txt ")); + Assert.That( + SystemIOWindows.PrefixWithUNC(@"\\?\C:\"), + Is.EqualTo(@"\\?\C:\")); + Assert.That( + SystemIOWindows.PrefixWithUNC(@"\\?\UNC\example.com\share"), + Is.EqualTo(@"\\?\UNC\example.com\share")); + Assert.That( + SystemIOWindows.PrefixWithUNC(@"\\?\UNC\example.com\share\file.txt"), + Is.EqualTo(@"\\?\UNC\example.com\share\file.txt")); + Assert.That( + SystemIOWindows.PrefixWithUNC(@"\\?\UNC\example.com\share."), + Is.EqualTo(@"\\?\UNC\example.com\share.")); + Assert.That( + SystemIOWindows.PrefixWithUNC(@"\\?\UNC\example.com\share.\file.txt"), + Is.EqualTo(@"\\?\UNC\example.com\share.\file.txt")); + Assert.That( + SystemIOWindows.PrefixWithUNC(@"\\?\UNC\example.com\share.\file.txt."), + Is.EqualTo(@"\\?\UNC\example.com\share.\file.txt.")); + Assert.That( + SystemIOWindows.PrefixWithUNC(@"\\?\UNC\example.com\share "), + Is.EqualTo(@"\\?\UNC\example.com\share ")); + Assert.That( + SystemIOWindows.PrefixWithUNC(@"\\?\UNC\example.com\share\file.txt "), + Is.EqualTo(@"\\?\UNC\example.com\share\file.txt ")); + } + + [Test] + public void TestPathGetFullPathInWindowsClient() + { + if (!Platform.IsClientWindows) + { + return; + } + + // Normalization of basic relative paths, with both kinds of slashes + Assert.That( + SystemIO.IO_WIN.PathGetFullPath(@"."), + Is.EqualTo(System.IO.Path.GetFullPath(@"."))); + Assert.That( + SystemIO.IO_WIN.PathGetFullPath(@"temp"), + Is.EqualTo(System.IO.Path.GetFullPath(@"temp"))); + Assert.That( + SystemIO.IO_WIN.PathGetFullPath(@"temp\file.txt"), + Is.EqualTo(System.IO.Path.GetFullPath(@"temp\file.txt"))); + Assert.That( + SystemIO.IO_WIN.PathGetFullPath(@"\"), + Is.EqualTo(System.IO.Path.GetFullPath(@"\"))); + Assert.That( + SystemIO.IO_WIN.PathGetFullPath(@"/"), + Is.EqualTo(System.IO.Path.GetFullPath(@"/"))); + Assert.That( + SystemIO.IO_WIN.PathGetFullPath(@"\temp"), + Is.EqualTo(System.IO.Path.GetFullPath(@"\temp"))); + Assert.That( + SystemIO.IO_WIN.PathGetFullPath(@"/temp"), + Is.EqualTo(System.IO.Path.GetFullPath(@"/temp"))); + Assert.That( + SystemIO.IO_WIN.PathGetFullPath(@"/temp/file.txt"), + Is.EqualTo(System.IO.Path.GetFullPath(@"/temp/file.txt"))); + Assert.That( + SystemIO.IO_WIN.PathGetFullPath(@"/temp/file.txt"), + Is.EqualTo(System.IO.Path.GetFullPath(@"/temp/file.txt"))); + + // Normalization of full qualified paths, but with relative components, with both kinds of slashes + Assert.That( + SystemIO.IO_WIN.PathGetFullPath(@"C:\temp\."), + Is.EqualTo(System.IO.Path.GetFullPath(@"C:\temp\."))); + Assert.That( + SystemIO.IO_WIN.PathGetFullPath(@"C:/temp/."), + Is.EqualTo(System.IO.Path.GetFullPath(@"C:/temp/."))); + Assert.That( + SystemIO.IO_WIN.PathGetFullPath(@"C:\temp\.."), + Is.EqualTo(System.IO.Path.GetFullPath(@"C:\temp\.."))); + Assert.That( + SystemIO.IO_WIN.PathGetFullPath(@"C:/temp/.."), + Is.EqualTo(System.IO.Path.GetFullPath(@"C:/temp/.."))); + Assert.That( + SystemIO.IO_WIN.PathGetFullPath(@"C:\temp\..\folder"), + Is.EqualTo(System.IO.Path.GetFullPath(@"C:\temp\..\folder"))); + Assert.That( + SystemIO.IO_WIN.PathGetFullPath(@"C:/temp/../folder"), + Is.EqualTo(System.IO.Path.GetFullPath(@"C:/temp/../folder"))); + Assert.That( + SystemIO.IO_WIN.PathGetFullPath(@"C:\temp\.\file.txt"), + Is.EqualTo(System.IO.Path.GetFullPath(@"C:\temp\.\file.txt"))); + Assert.That( + SystemIO.IO_WIN.PathGetFullPath(@"C:/temp/./file.txt"), + Is.EqualTo(System.IO.Path.GetFullPath(@"C:/temp/./file.txt"))); + Assert.That( + SystemIO.IO_WIN.PathGetFullPath(@"\\example.com\share\."), + Is.EqualTo(System.IO.Path.GetFullPath(@"\\example.com\share\."))); + Assert.That( + SystemIO.IO_WIN.PathGetFullPath(@"//example.com/share/."), + Is.EqualTo(System.IO.Path.GetFullPath(@"//example.com/share/."))); + Assert.That( + SystemIO.IO_WIN.PathGetFullPath(@"\\example.com\share\.."), + Is.EqualTo(System.IO.Path.GetFullPath(@"\\example.com\share\.."))); + Assert.That( + SystemIO.IO_WIN.PathGetFullPath(@"//example.com/share/.."), + Is.EqualTo(System.IO.Path.GetFullPath(@"//example.com/share/.."))); + Assert.That( + SystemIO.IO_WIN.PathGetFullPath(@"\\example.com\share\..\folder"), + Is.EqualTo(System.IO.Path.GetFullPath(@"\\example.com\share\..\folder"))); + Assert.That( + SystemIO.IO_WIN.PathGetFullPath(@"//example.com/share/../folder"), + Is.EqualTo(System.IO.Path.GetFullPath(@"//example.com/share/../folder"))); + Assert.That( + SystemIO.IO_WIN.PathGetFullPath(@"\\example.com\share\.\file.txt"), + Is.EqualTo(System.IO.Path.GetFullPath(@"\\example.com\share\.\file.txt"))); + Assert.That( + SystemIO.IO_WIN.PathGetFullPath(@"//example.com/share/./file.txt"), + Is.EqualTo(System.IO.Path.GetFullPath(@"//example.com/share/./file.txt"))); + + // Fully qualified paths with no relative components, with both kinds of slashes + Assert.That( + SystemIO.IO_WIN.PathGetFullPath(@"C:\"), + Is.EqualTo(System.IO.Path.GetFullPath(@"C:\"))); + Assert.That( + SystemIO.IO_WIN.PathGetFullPath(@"C:/"), + Is.EqualTo(System.IO.Path.GetFullPath(@"C:/"))); + Assert.That( + SystemIO.IO_WIN.PathGetFullPath(@"C:\temp"), + Is.EqualTo(System.IO.Path.GetFullPath(@"C:\temp"))); + Assert.That( + SystemIO.IO_WIN.PathGetFullPath(@"C:/temp"), + Is.EqualTo(System.IO.Path.GetFullPath(@"C:/temp"))); + Assert.That( + SystemIO.IO_WIN.PathGetFullPath(@"C:\temp\file.txt"), + Is.EqualTo(System.IO.Path.GetFullPath(@"C:\temp\file.txt"))); + Assert.That( + SystemIO.IO_WIN.PathGetFullPath(@"C:/temp/file.txt"), + Is.EqualTo(System.IO.Path.GetFullPath(@"C:/temp/file.txt"))); + Assert.That( + SystemIO.IO_WIN.PathGetFullPath(@"\\example.com\share"), + Is.EqualTo(System.IO.Path.GetFullPath(@"\\example.com\share"))); + Assert.That( + SystemIO.IO_WIN.PathGetFullPath(@"//example.com/share"), + Is.EqualTo(System.IO.Path.GetFullPath(@"//example.com/share"))); + Assert.That( + SystemIO.IO_WIN.PathGetFullPath(@"\\example.com\share\file.txt"), + Is.EqualTo(System.IO.Path.GetFullPath(@"\\example.com\share\file.txt"))); + Assert.That( + SystemIO.IO_WIN.PathGetFullPath(@"//example.com/share/file.txt"), + Is.EqualTo(System.IO.Path.GetFullPath(@"//example.com/share/file.txt"))); + + // Fully qualified paths with no relative components, but with problematic names, with both kinds of slashes + Assert.That( + SystemIO.IO_WIN.PathGetFullPath(@"C:\temp."), + Is.Not.EqualTo(System.IO.Path.GetFullPath(@"C:\temp.")).And.EqualTo(@"C:\temp.")); + Assert.That( + SystemIO.IO_WIN.PathGetFullPath(@"C:/temp."), + Is.Not.EqualTo(System.IO.Path.GetFullPath(@"C:/temp.")).And.EqualTo(@"C:\temp.")); + Assert.That( + SystemIO.IO_WIN.PathGetFullPath(@"C:\temp.\file.txt"), + Is.Not.EqualTo(System.IO.Path.GetFullPath(@"C:\temp.\file.txt")).And.EqualTo(@"C:\temp.\file.txt")); + Assert.That( + SystemIO.IO_WIN.PathGetFullPath(@"C:/temp./file.txt"), + Is.Not.EqualTo(System.IO.Path.GetFullPath(@"C:/temp./file.txt")).And.EqualTo(@"C:\temp.\file.txt")); + Assert.That( + SystemIO.IO_WIN.PathGetFullPath(@"C:\temp.\file.txt."), + Is.Not.EqualTo(System.IO.Path.GetFullPath(@"C:\temp.\file.txt.")).And.EqualTo(@"C:\temp.\file.txt.")); + Assert.That( + SystemIO.IO_WIN.PathGetFullPath(@"C:/temp./file.txt."), + Is.Not.EqualTo(System.IO.Path.GetFullPath(@"C:/temp./file.txt.")).And.EqualTo(@"C:\temp.\file.txt.")); + Assert.That( + SystemIO.IO_WIN.PathGetFullPath(@"C:\temp "), + Is.Not.EqualTo(System.IO.Path.GetFullPath(@"C:\temp ")).And.EqualTo(@"C:\temp ")); + Assert.That( + SystemIO.IO_WIN.PathGetFullPath(@"C:/temp "), + Is.Not.EqualTo(System.IO.Path.GetFullPath(@"C:/temp ")).And.EqualTo(@"C:\temp ")); + Assert.That( + SystemIO.IO_WIN.PathGetFullPath(@"C:\temp\file.txt "), + Is.Not.EqualTo(System.IO.Path.GetFullPath(@"C:\temp\file.txt ")).And.EqualTo(@"C:\temp\file.txt ")); + Assert.That( + SystemIO.IO_WIN.PathGetFullPath(@"C:/temp/file.txt "), + Is.Not.EqualTo(System.IO.Path.GetFullPath(@"C:/temp/file.txt ")).And.EqualTo(@"C:\temp\file.txt ")); + Assert.That( + SystemIO.IO_WIN.PathGetFullPath(@"\\example.com\share."), + Is.Not.EqualTo(System.IO.Path.GetFullPath(@"\\example.com\share.")).And.EqualTo(@"\\example.com\share.")); + Assert.That( + SystemIO.IO_WIN.PathGetFullPath(@"//example.com/share."), + Is.Not.EqualTo(System.IO.Path.GetFullPath(@"//example.com/share.")).And.EqualTo(@"\\example.com\share.")); + Assert.That( + SystemIO.IO_WIN.PathGetFullPath(@"\\example.com\share\file.txt."), + Is.Not.EqualTo(System.IO.Path.GetFullPath(@"\\example.com\share\file.txt.")).And.EqualTo(@"\\example.com\share\file.txt.")); + Assert.That( + SystemIO.IO_WIN.PathGetFullPath(@"//example.com/share./file.txt."), + Is.Not.EqualTo(System.IO.Path.GetFullPath(@"//example.com/share./file.txt.")).And.EqualTo(@"\\example.com\share.\file.txt.")); + Assert.That( + SystemIO.IO_WIN.PathGetFullPath(@"\\example.com\share "), + Is.Not.EqualTo(System.IO.Path.GetFullPath(@"\\example.com\share ")).And.EqualTo(@"\\example.com\share ")); + Assert.That( + SystemIO.IO_WIN.PathGetFullPath(@"//example.com/share "), + Is.Not.EqualTo(System.IO.Path.GetFullPath(@"//example.com/share ")).And.EqualTo(@"\\example.com\share ")); + Assert.That( + SystemIO.IO_WIN.PathGetFullPath(@"\\example.com\share\file.txt "), + Is.Not.EqualTo(System.IO.Path.GetFullPath(@"\\example.com\share\file.txt ")).And.EqualTo(@"\\example.com\share\file.txt ")); + Assert.That( + SystemIO.IO_WIN.PathGetFullPath(@"//example.com/share/file.txt "), + Is.Not.EqualTo(System.IO.Path.GetFullPath(@"//example.com/share/file.txt ")).And.EqualTo(@"\\example.com\share\file.txt ")); + + // Normalization disabled for paths with @"\\?\" prefix + Assert.That( + SystemIO.IO_WIN.PathGetFullPath(@"\\?\C:\"), + Is.EqualTo(System.IO.Path.GetFullPath(@"\\?\C:\"))); + Assert.That( + SystemIO.IO_WIN.PathGetFullPath(@"\\?\C:\temp"), + Is.EqualTo(System.IO.Path.GetFullPath(@"\\?\C:\temp"))); + Assert.That( + SystemIO.IO_WIN.PathGetFullPath(@"\\?\C:\temp\file.txt"), + Is.EqualTo(System.IO.Path.GetFullPath(@"\\?\C:\temp\file.txt"))); + Assert.That( + SystemIO.IO_WIN.PathGetFullPath(@"\\?\C:\temp."), + Is.EqualTo(System.IO.Path.GetFullPath(@"\\?\C:\temp."))); + Assert.That( + SystemIO.IO_WIN.PathGetFullPath(@"\\?\C:\temp.\file.txt"), + Is.EqualTo(System.IO.Path.GetFullPath(@"\\?\C:\temp.\file.txt"))); + Assert.That( + SystemIO.IO_WIN.PathGetFullPath(@"\\?\C:\temp.\file.txt."), + Is.EqualTo(System.IO.Path.GetFullPath(@"\\?\C:\temp.\file.txt."))); + Assert.That( + SystemIO.IO_WIN.PathGetFullPath(@"\\?\C:\temp "), + Is.EqualTo(System.IO.Path.GetFullPath(@"\\?\C:\temp "))); + Assert.That( + SystemIO.IO_WIN.PathGetFullPath(@"\\?\C:\temp\file.txt "), + Is.EqualTo(System.IO.Path.GetFullPath(@"\\?\C:\temp\file.txt "))); + Assert.That( + SystemIO.IO_WIN.PathGetFullPath(@"\\?\C:\"), + Is.EqualTo(System.IO.Path.GetFullPath(@"\\?\C:\"))); + Assert.That( + SystemIO.IO_WIN.PathGetFullPath(@"\\?\UNC\example.com\share"), + Is.EqualTo(System.IO.Path.GetFullPath(@"\\?\UNC\example.com\share"))); + Assert.That( + SystemIO.IO_WIN.PathGetFullPath(@"\\?\UNC\example.com\share\file.txt"), + Is.EqualTo(System.IO.Path.GetFullPath(@"\\?\UNC\example.com\share\file.txt"))); + Assert.That( + SystemIO.IO_WIN.PathGetFullPath(@"\\?\UNC\example.com\share."), + Is.EqualTo(System.IO.Path.GetFullPath(@"\\?\UNC\example.com\share."))); + Assert.That( + SystemIO.IO_WIN.PathGetFullPath(@"\\?\UNC\example.com\share.\file.txt"), + Is.EqualTo(System.IO.Path.GetFullPath(@"\\?\UNC\example.com\share.\file.txt"))); + Assert.That( + SystemIO.IO_WIN.PathGetFullPath(@"\\?\UNC\example.com\share.\file.txt."), + Is.EqualTo(System.IO.Path.GetFullPath(@"\\?\UNC\example.com\share.\file.txt."))); + Assert.That( + SystemIO.IO_WIN.PathGetFullPath(@"\\?\UNC\example.com\share "), + Is.EqualTo(System.IO.Path.GetFullPath(@"\\?\UNC\example.com\share "))); + Assert.That( + SystemIO.IO_WIN.PathGetFullPath(@"\\?\UNC\example.com\share\file.txt "), + Is.EqualTo(System.IO.Path.GetFullPath(@"\\?\UNC\example.com\share\file.txt "))); + } } }