From 0e8280f6f34af110c18f0a612b015055e75f7f29 Mon Sep 17 00:00:00 2001 From: Dean Ferreyra Date: Thu, 4 Nov 2021 22:27:32 -0700 Subject: [PATCH 1/6] Fix SystemIOWindows handling of relative paths The handling of relative paths in SystemIOWindows was broken by changes for #4256. Change SystemIOWindows.PrefixWithUNC to only prefix paths with `\\?\` when they can be prefixed; i.e., only prefix paths that are fully qualified and that don't contain relative path components like `.` or `..`. This way, relative paths passed to SystemIOWindows methods work correctly again. Also support network paths specified using forward slashes; e.g., `//example.com/share/foo.txt`. Simplify SystemIOWindows.GetFullPath since Path.GetFullPath will honor `\\?\` prefixes and can replace forward slashes with backslashes, etc. Copy and adapt source for Path.IsPathFullyQualified from https://github.com/dotnet/runtime, which is MIT licensed. Add unit test for Duplicati.CommandLine.BackendTool to validate the fix. This fixes #4632. --- Duplicati/CommandLine/BackendTool/Program.cs | 2 +- .../Library/Common/IO/SystemIOWindows.cs | 147 ++++++++++++++++-- Duplicati/UnitTest/BackendToolTests.cs | 86 ++++++++++ Duplicati/UnitTest/Duplicati.UnitTest.csproj | 1 + 4 files changed, 224 insertions(+), 12 deletions(-) create mode 100755 Duplicati/UnitTest/BackendToolTests.cs diff --git a/Duplicati/CommandLine/BackendTool/Program.cs b/Duplicati/CommandLine/BackendTool/Program.cs index 0b30706cd..c72cee92d 100644 --- a/Duplicati/CommandLine/BackendTool/Program.cs +++ b/Duplicati/CommandLine/BackendTool/Program.cs @@ -25,7 +25,7 @@ using System.Threading; namespace Duplicati.CommandLine.BackendTool { - class Program + public class Program { /// /// The main entry point for the application. diff --git a/Duplicati/Library/Common/IO/SystemIOWindows.cs b/Duplicati/Library/Common/IO/SystemIOWindows.cs index b29a34331..f1486c154 100644 --- a/Duplicati/Library/Common/IO/SystemIOWindows.cs +++ b/Duplicati/Library/Common/IO/SystemIOWindows.cs @@ -32,17 +32,44 @@ namespace Duplicati.Library.Common.IO private const string UNCPREFIX = @"\\?\"; private const string UNCPREFIX_SERVER = @"\\?\UNC\"; private const string PATHPREFIX_SERVER = @"\\"; + private const string PATHPREFIX_SERVER_ALT = @"//"; private static readonly string DIRSEP = Util.DirectorySeparatorString; + /// + /// Prefix path with one of the UNC prefixes, but only if it's a fully + /// qualified path with no relative components (i.e., with no "." or + /// ".." as part of the path). + /// public static string PrefixWithUNC(string path) { if (IsPrefixedWithUNC(path)) { + // For example: \\?\C:\Temp\foo.txt or \\?\UNC\example.com\share\foo.txt return path; } - return path.StartsWith(PATHPREFIX_SERVER, StringComparison.Ordinal) - ? UNCPREFIX_SERVER + path.Substring(PATHPREFIX_SERVER.Length) - : UNCPREFIX + path; + else if (IsPrefixedWithBasicUNC(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)) + { + // For example: C:\Temp\foo.txt or C:/Temp/foo.txt + return UNCPREFIX + ConvertSlashes(path); + } + else + { + // A relative path or a fully qualified path with relative + // path components so the UNC prefix cannot be applied. + // For example: foo.txt or C:\Temp\..\foo.txt + return path; + } + } + + private static bool IsPrefixedWithBasicUNC(string path) + { + return path.StartsWith(PATHPREFIX_SERVER, StringComparison.Ordinal) || + path.StartsWith(PATHPREFIX_SERVER_ALT, StringComparison.Ordinal); } private static bool IsPrefixedWithUNC(string path) @@ -51,6 +78,32 @@ namespace Duplicati.Library.Common.IO path.StartsWith(UNCPREFIX, StringComparison.Ordinal); } + private static string[] relativePathComponents = new[] { ".", ".." }; + + /// + /// Returns true if contains relative path components; i.e., "." or "..". + /// + private static bool HasRelativePathComponents(string path) + { + return GetPathComponents(path).Any(pathComponent => relativePathComponents.Contains(pathComponent)); + } + + /// + /// Returns a sequence representing the files and directories in . + /// + private static IEnumerable GetPathComponents(string path) + { + while (!String.IsNullOrEmpty(path)) + { + var pathComponent = Path.GetFileName(path); + if (!String.IsNullOrEmpty(pathComponent)) + { + yield return pathComponent; + } + path = Path.GetDirectoryName(path); + } + } + public static string StripUNCPrefix(string path) { if (path.StartsWith(UNCPREFIX_SERVER, StringComparison.Ordinal)) @@ -177,6 +230,85 @@ 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) { @@ -407,14 +539,7 @@ namespace Duplicati.Library.Common.IO public string PathGetFullPath(string path) { - if (IsPrefixedWithUNC(path)) - { - return System.IO.Path.GetFullPath(ConvertSlashes(path)); - } - else - { - return StripUNCPrefix(System.IO.Path.GetFullPath(PrefixWithUNC(ConvertSlashes(path)))); - } + return PrefixWithUNC(Path.GetFullPath(path)); } public IFileEntry DirectoryEntry(string path) diff --git a/Duplicati/UnitTest/BackendToolTests.cs b/Duplicati/UnitTest/BackendToolTests.cs new file mode 100755 index 000000000..94616cccf --- /dev/null +++ b/Duplicati/UnitTest/BackendToolTests.cs @@ -0,0 +1,86 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using Duplicati.Library.Interface; +using Duplicati.Library.Main; +using NUnit.Framework; + +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() + { + // Files to create in MB. + int[] fileSizes = {10, 20, 30}; + foreach (int size in fileSizes) + { + var data = new byte[size * 1024 * 1024]; + var rng = new Random(); + rng.NextBytes(data); + File.WriteAllBytes(Path.Combine(DATAFOLDER, size + "MB"), data); + } + + // Run a backup. + var options = new Dictionary(TestOptions); + var backendURL = "file://" + this.TARGETFOLDER; + using (Controller c = new Controller(backendURL, options, null)) + { + var backupResults = c.Backup(new[] {DATAFOLDER}); + Assert.AreEqual(0, backupResults.Errors.Count()); + Assert.AreEqual(0, backupResults.Warnings.Count()); + } + + // Get the backend files using absolute paths + var absoluteDownloadFolder = Path.Combine(RESTOREFOLDER, "target-files-absolute"); + Directory.CreateDirectory(absoluteDownloadFolder); + foreach (var targetFile in Directory.GetFiles(TARGETFOLDER)) + { + // Absolute path + var downloadFileName = Path.Combine(absoluteDownloadFolder, Path.GetFileName(targetFile)); + var status = CommandLine.BackendTool.Program.RealMain(new[] { "GET", $"{backendURL}", $"{downloadFileName}" }); + Assert.AreEqual(0, status); + Assert.IsTrue(File.Exists(downloadFileName)); + TestUtils.AssertFilesAreEqual(targetFile, downloadFileName, false, downloadFileName); + } + + // Get the backend files using relative paths + var relativeDownloadFolder = Path.Combine(RESTOREFOLDER, "target-files-relative"); + Directory.CreateDirectory(relativeDownloadFolder); + var originalCurrentDirectory = Directory.GetCurrentDirectory(); + Directory.SetCurrentDirectory(relativeDownloadFolder); + try + { + foreach (var targetFile in Directory.GetFiles(TARGETFOLDER)) + { + // Relative path + var downloadFileName = Path.GetFileName(targetFile); + var status = CommandLine.BackendTool.Program.RealMain(new[] { "GET", $"{backendURL}", $"{downloadFileName}" }); + Assert.AreEqual(0, status); + Assert.IsTrue(File.Exists(downloadFileName)); + TestUtils.AssertFilesAreEqual(targetFile, downloadFileName, false, downloadFileName); + } + } + finally + { + Directory.SetCurrentDirectory(originalCurrentDirectory); + } + } + } +} \ No newline at end of file diff --git a/Duplicati/UnitTest/Duplicati.UnitTest.csproj b/Duplicati/UnitTest/Duplicati.UnitTest.csproj index 5851a78d2..e0459ce24 100644 --- a/Duplicati/UnitTest/Duplicati.UnitTest.csproj +++ b/Duplicati/UnitTest/Duplicati.UnitTest.csproj @@ -46,6 +46,7 @@ + From 2f5bb306bd3f4c2c05201ae76d0d72afa9b9a66b Mon Sep 17 00:00:00 2001 From: Dean Ferreyra Date: Sat, 6 Nov 2021 23:20:04 -0700 Subject: [PATCH 2/6] Change Duplicati.UnitTest project to get System.Net.Http from NuGet --- Duplicati/UnitTest/Duplicati.UnitTest.csproj | 35 ++++++++++++++++++++ Duplicati/UnitTest/app.config | 2 +- Duplicati/UnitTest/packages.config | 9 ++++- 3 files changed, 44 insertions(+), 2 deletions(-) mode change 100644 => 100755 Duplicati/UnitTest/packages.config diff --git a/Duplicati/UnitTest/Duplicati.UnitTest.csproj b/Duplicati/UnitTest/Duplicati.UnitTest.csproj index e0459ce24..536e173a8 100644 --- a/Duplicati/UnitTest/Duplicati.UnitTest.csproj +++ b/Duplicati/UnitTest/Duplicati.UnitTest.csproj @@ -40,9 +40,44 @@ ..\..\packages\NUnit.3.12.0\lib\net45\nunit.framework.dll + + + ..\..\packages\System.IO.4.3.0\lib\net462\System.IO.dll + True + True + + + ..\..\packages\System.Net.Http.4.3.4\lib\net46\System.Net.Http.dll + True + + + ..\..\packages\System.Runtime.4.3.0\lib\net462\System.Runtime.dll + True + True + + + ..\..\packages\System.Security.Cryptography.Algorithms.4.3.0\lib\net463\System.Security.Cryptography.Algorithms.dll + True + True + + + ..\..\packages\System.Security.Cryptography.Encoding.4.3.0\lib\net46\System.Security.Cryptography.Encoding.dll + True + True + + + ..\..\packages\System.Security.Cryptography.Primitives.4.3.0\lib\net46\System.Security.Cryptography.Primitives.dll + True + True + + + ..\..\packages\System.Security.Cryptography.X509Certificates.4.3.0\lib\net461\System.Security.Cryptography.X509Certificates.dll + True + True + diff --git a/Duplicati/UnitTest/app.config b/Duplicati/UnitTest/app.config index 1ca2093f4..4866b7b0b 100644 --- a/Duplicati/UnitTest/app.config +++ b/Duplicati/UnitTest/app.config @@ -13,7 +13,7 @@ - + diff --git a/Duplicati/UnitTest/packages.config b/Duplicati/UnitTest/packages.config old mode 100644 new mode 100755 index 344223e2a..814110de8 --- a/Duplicati/UnitTest/packages.config +++ b/Duplicati/UnitTest/packages.config @@ -1,4 +1,11 @@ - + + + + + + + + From 459d1f27f619e5f3c7ac4e012efa30f2f48b5aba Mon Sep 17 00:00:00 2001 From: Dean Ferreyra Date: Sun, 7 Nov 2021 14:05:39 -0800 Subject: [PATCH 3/6] 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. --- .../Common/Duplicati.Library.Common.csproj | 2 + .../DotNetRuntime.System.IO.Path.Windows.cs | 59 +++ ...tRuntime.System.IO.PathInternal.Windows.cs | 133 ++++++ .../Library/Common/IO/SystemIOWindows.cs | 103 +--- Duplicati/UnitTest/BackendToolTests.cs | 12 - Duplicati/UnitTest/IOTests.cs | 452 ++++++++++++++++++ 6 files changed, 664 insertions(+), 97 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 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 "))); + } } } From 423921d1c2529be9f6f8770767a7ab5f93710c23 Mon Sep 17 00:00:00 2001 From: Dean Ferreyra Date: Mon, 8 Nov 2021 20:19:22 -0800 Subject: [PATCH 4/6] Revert "Change Duplicati.UnitTest project to get System.Net.Http from NuGet" This reverts commit 2f5bb306bd3f4c2c05201ae76d0d72afa9b9a66b. This was an experiment that was accidentally committed. --- Duplicati/UnitTest/Duplicati.UnitTest.csproj | 35 -------------------- Duplicati/UnitTest/app.config | 2 +- Duplicati/UnitTest/packages.config | 9 +---- 3 files changed, 2 insertions(+), 44 deletions(-) mode change 100755 => 100644 Duplicati/UnitTest/packages.config diff --git a/Duplicati/UnitTest/Duplicati.UnitTest.csproj b/Duplicati/UnitTest/Duplicati.UnitTest.csproj index 536e173a8..e0459ce24 100644 --- a/Duplicati/UnitTest/Duplicati.UnitTest.csproj +++ b/Duplicati/UnitTest/Duplicati.UnitTest.csproj @@ -40,44 +40,9 @@ ..\..\packages\NUnit.3.12.0\lib\net45\nunit.framework.dll - - - ..\..\packages\System.IO.4.3.0\lib\net462\System.IO.dll - True - True - - - ..\..\packages\System.Net.Http.4.3.4\lib\net46\System.Net.Http.dll - True - - - ..\..\packages\System.Runtime.4.3.0\lib\net462\System.Runtime.dll - True - True - - - ..\..\packages\System.Security.Cryptography.Algorithms.4.3.0\lib\net463\System.Security.Cryptography.Algorithms.dll - True - True - - - ..\..\packages\System.Security.Cryptography.Encoding.4.3.0\lib\net46\System.Security.Cryptography.Encoding.dll - True - True - - - ..\..\packages\System.Security.Cryptography.Primitives.4.3.0\lib\net46\System.Security.Cryptography.Primitives.dll - True - True - - - ..\..\packages\System.Security.Cryptography.X509Certificates.4.3.0\lib\net461\System.Security.Cryptography.X509Certificates.dll - True - True - diff --git a/Duplicati/UnitTest/app.config b/Duplicati/UnitTest/app.config index 4866b7b0b..1ca2093f4 100644 --- a/Duplicati/UnitTest/app.config +++ b/Duplicati/UnitTest/app.config @@ -13,7 +13,7 @@ - + diff --git a/Duplicati/UnitTest/packages.config b/Duplicati/UnitTest/packages.config old mode 100755 new mode 100644 index 814110de8..344223e2a --- a/Duplicati/UnitTest/packages.config +++ b/Duplicati/UnitTest/packages.config @@ -1,11 +1,4 @@ - + - - - - - - - From 158614d59a7a6cf69ea964ac1b7a4e9c91b66fc2 Mon Sep 17 00:00:00 2001 From: Dean Ferreyra Date: Tue, 9 Nov 2021 22:34:35 -0800 Subject: [PATCH 5/6] Refactor \\?\ prefix tests, rename source files, plus other review issues --- .../Common/Duplicati.Library.Common.csproj | 4 +- ...time.System.IO.Path.Windows.cs => Path.cs} | 0 ...nal.Windows.cs => PathInternal.Windows.cs} | 0 .../Library/Common/IO/SystemIOWindows.cs | 32 +- Duplicati/UnitTest/IOTests.cs | 631 ++++++------------ 5 files changed, 220 insertions(+), 447 deletions(-) rename Duplicati/Library/Common/IO/{DotNetRuntime.System.IO.Path.Windows.cs => Path.cs} (100%) rename Duplicati/Library/Common/IO/{DotNetRuntime.System.IO.PathInternal.Windows.cs => PathInternal.Windows.cs} (100%) diff --git a/Duplicati/Library/Common/Duplicati.Library.Common.csproj b/Duplicati/Library/Common/Duplicati.Library.Common.csproj index e85ee6a2d..abd423d19 100644 --- a/Duplicati/Library/Common/Duplicati.Library.Common.csproj +++ b/Duplicati/Library/Common/Duplicati.Library.Common.csproj @@ -62,8 +62,8 @@ - - + + diff --git a/Duplicati/Library/Common/IO/DotNetRuntime.System.IO.Path.Windows.cs b/Duplicati/Library/Common/IO/Path.cs similarity index 100% rename from Duplicati/Library/Common/IO/DotNetRuntime.System.IO.Path.Windows.cs rename to Duplicati/Library/Common/IO/Path.cs diff --git a/Duplicati/Library/Common/IO/DotNetRuntime.System.IO.PathInternal.Windows.cs b/Duplicati/Library/Common/IO/PathInternal.Windows.cs similarity index 100% rename from Duplicati/Library/Common/IO/DotNetRuntime.System.IO.PathInternal.Windows.cs rename to Duplicati/Library/Common/IO/PathInternal.Windows.cs diff --git a/Duplicati/Library/Common/IO/SystemIOWindows.cs b/Duplicati/Library/Common/IO/SystemIOWindows.cs index 53a0ceb33..52f316576 100644 --- a/Duplicati/Library/Common/IO/SystemIOWindows.cs +++ b/Duplicati/Library/Common/IO/SystemIOWindows.cs @@ -47,22 +47,26 @@ namespace Duplicati.Library.Common.IO // For example: \\?\C:\Temp\foo.txt or \\?\UNC\example.com\share\foo.txt return 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 (DotNetRuntimePathWindows.IsPathFullyQualified(path) && !HasRelativePathComponents(path)) - { - // For example: C:\Temp\foo.txt or C:/Temp/foo.txt - return UNCPREFIX + ConvertSlashes(path); - } else { - // A relative path or a fully qualified path with relative - // path components so the UNC prefix cannot be applied. - // For example: foo.txt or C:\Temp\..\foo.txt - return path; + var hasRelativePathComponents = HasRelativePathComponents(path); + if (IsPrefixedWithBasicUNC(path) && !hasRelativePathComponents) + { + // For example: \\example.com\share\foo.txt or //example.com/share/foo.txt + return UNCPREFIX_SERVER + ConvertSlashes(path.Substring(PATHPREFIX_SERVER.Length)); + } + else if (DotNetRuntimePathWindows.IsPathFullyQualified(path) && !hasRelativePathComponents) + { + // For example: C:\Temp\foo.txt or C:/Temp/foo.txt + return UNCPREFIX + ConvertSlashes(path); + } + else + { + // A relative path or a fully qualified path with relative + // path components so the UNC prefix cannot be applied. + // For example: foo.txt or C:\Temp\..\foo.txt + return path; + } } } diff --git a/Duplicati/UnitTest/IOTests.cs b/Duplicati/UnitTest/IOTests.cs index 84940d2b1..72d3c9413 100644 --- a/Duplicati/UnitTest/IOTests.cs +++ b/Duplicati/UnitTest/IOTests.cs @@ -21,7 +21,8 @@ using NUnit.Framework; using Duplicati.Library.Common.IO; using Duplicati.Library.Utility; using Duplicati.Library.Common; - +using System.Collections.Generic; + namespace Duplicati.UnitTest { [Category("IO")] @@ -104,222 +105,101 @@ namespace Duplicati.UnitTest 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")); + var testCasesLeavingPathUnchanged = + new[] + { + // Normalization of basic relative paths, with both kinds of slashes + @".", + @"temp", + @"temp\file.txt", + @"\", + @"/", + @"\temp", + @"/temp", + @"/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")); + // Normalization of full qualified paths, but with relative components, with both kinds of slashes + @"C:\temp\.", + @"C:/temp/.", + @"C:\temp\..", + @"C:/temp/..", + @"C:\temp\..\folder", + @"C:/temp/../folder", + @"C:\temp\.\file.txt", + @"C:/temp/./file.txt", + @"\\example.com\share\.", + @"//example.com/share/.", + @"\\example.com\share\..", + @"//example.com/share/..", + @"\\example.com\share\..\folder", + @"//example.com/share/../folder", + @"\\example.com\share\.\file.txt", + @"//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")); + // Normalization disabled for paths with @"\\?\" prefix + @"\\?\C:\", + @"\\?\C:\temp", + @"\\?\C:\temp\file.txt", + @"\\?\C:\temp.", + @"\\?\C:\temp.\file.txt", + @"\\?\C:\temp.\file.txt.", + @"\\?\C:\temp ", + @"\\?\C:\temp\file.txt ", + @"\\?\C:\", + @"\\?\UNC\example.com\share", + @"\\?\UNC\example.com\share\file.txt", + @"\\?\UNC\example.com\share.", + @"\\?\UNC\example.com\share.\file.txt", + @"\\?\UNC\example.com\share.\file.txt.", + @"\\?\UNC\example.com\share ", + @"\\?\UNC\example.com\share\file.txt " + }; + foreach (var path in testCasesLeavingPathUnchanged) + { + Assert.AreEqual(path, SystemIOWindows.PrefixWithUNC(path)); + } - // 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 ")); + var testCasesWherePrefixIsApplied = + new Dictionary() + { + // Fully qualified paths with no relative components, with both kinds of slashes + { @"C:\", @"\\?\C:\" }, + { @"C:/", @"\\?\C:\" }, + { @"C:\temp", @"\\?\C:\temp" }, + { @"C:/temp", @"\\?\C:\temp" }, + { @"C:\temp\file.txt", @"\\?\C:\temp\file.txt" }, + { @"C:/temp/file.txt", @"\\?\C:\temp\file.txt" }, + { @"\\example.com\share", @"\\?\UNC\example.com\share" }, + { @"//example.com/share", @"\\?\UNC\example.com\share" }, + { @"\\example.com\share\file.txt", @"\\?\UNC\example.com\share\file.txt" }, + { @"//example.com/share/file.txt", @"\\?\UNC\example.com\share\file.txt" }, + + // Fully qualified paths with no relative components, but with problematic names, with both kinds of slashes + { @"C:\temp.", @"\\?\C:\temp." }, + { @"C:/temp.", @"\\?\C:\temp." }, + { @"C:\temp.\file.txt", @"\\?\C:\temp.\file.txt" }, + { @"C:/temp./file.txt", @"\\?\C:\temp.\file.txt" }, + { @"C:\temp.\file.txt.", @"\\?\C:\temp.\file.txt." }, + { @"C:/temp./file.txt.", @"\\?\C:\temp.\file.txt." }, + { @"C:\temp ", @"\\?\C:\temp " }, + { @"C:/temp ", @"\\?\C:\temp " }, + { @"C:\temp\file.txt ", @"\\?\C:\temp\file.txt " }, + { @"C:/temp/file.txt ", @"\\?\C:\temp\file.txt " }, + { @"\\example.com\share.", @"\\?\UNC\example.com\share." }, + { @"//example.com/share.", @"\\?\UNC\example.com\share." }, + { @"\\example.com\share\file.txt.", @"\\?\UNC\example.com\share\file.txt." }, + { @"//example.com/share./file.txt.", @"\\?\UNC\example.com\share.\file.txt." }, + { @"\\example.com\share ", @"\\?\UNC\example.com\share " }, + { @"//example.com/share ", @"\\?\UNC\example.com\share " }, + { @"\\example.com\share\file.txt ", @"\\?\UNC\example.com\share\file.txt " }, + { @"//example.com/share/file.txt ", @"\\?\UNC\example.com\share\file.txt " } + }; + foreach (var keyValuePair in testCasesWherePrefixIsApplied) + { + var actual = SystemIOWindows.PrefixWithUNC(keyValuePair.Key); + var expected = keyValuePair.Value; + Assert.AreEqual(expected, actual); + } } [Test] @@ -330,222 +210,111 @@ namespace Duplicati.UnitTest 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"))); + var testCasesWherePathGetFullGivesSameResultsAsDotNet = + new[] + { + // Normalization of basic relative paths, with both kinds of slashes + @".", + @"temp", + @"temp\file.txt", + @"\", + @"/", + @"\temp", + @"/temp", + @"/temp/file.txt", + @"/temp/file.txt", + + // Normalization of full qualified paths, but with relative components, with both kinds of slashes + @"C:\temp\.", + @"C:/temp/.", + @"C:\temp\..", + @"C:/temp/..", + @"C:\temp\..\folder", + @"C:/temp/../folder", + @"C:\temp\.\file.txt", + @"C:/temp/./file.txt", + @"\\example.com\share\.", + @"//example.com/share/.", + @"\\example.com\share\..", + @"//example.com/share/..", + @"\\example.com\share\..\folder", + @"//example.com/share/../folder", + @"\\example.com\share\.\file.txt", + @"//example.com/share/./file.txt", + + // Fully qualified paths with no relative components, with both kinds of slashes + @"C:\", + @"C:/", + @"C:\temp", + @"C:/temp", + @"C:\temp\file.txt", + @"C:/temp/file.txt", + @"\\example.com\share", + @"//example.com/share", + @"\\example.com\share\file.txt", + @"//example.com/share/file.txt", + + // Normalization disabled for paths with @"\\?\" prefix + @"\\?\C:\", + @"\\?\C:\temp", + @"\\?\C:\temp\file.txt", + @"\\?\C:\temp.", + @"\\?\C:\temp.\file.txt", + @"\\?\C:\temp.\file.txt.", + @"\\?\C:\temp ", + @"\\?\C:\temp\file.txt ", + @"\\?\C:\", + @"\\?\UNC\example.com\share", + @"\\?\UNC\example.com\share\file.txt", + @"\\?\UNC\example.com\share.", + @"\\?\UNC\example.com\share.\file.txt", + @"\\?\UNC\example.com\share.\file.txt.", + @"\\?\UNC\example.com\share ", + @"\\?\UNC\example.com\share\file.txt ", + }; + foreach (var path in testCasesWherePathGetFullGivesSameResultsAsDotNet) + { + Assert.AreEqual( + System.IO.Path.GetFullPath(path), + SystemIO.IO_WIN.PathGetFullPath(path)); + } - // 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 "))); + var testCasesWherePathGetFullGivesDifferentResultsThanDotNet = + new Dictionary() + { + // Fully qualified paths with no relative components, but with problematic names, with both kinds of slashes + { @"C:\temp.", @"C:\temp." }, + { @"C:/temp.", @"C:\temp." }, + { @"C:\temp.\file.txt", @"C:\temp.\file.txt" }, + { @"C:/temp./file.txt", @"C:\temp.\file.txt" }, + { @"C:\temp.\file.txt.", @"C:\temp.\file.txt." }, + { @"C:/temp./file.txt.", @"C:\temp.\file.txt." }, + { @"C:\temp ", @"C:\temp " }, + { @"C:/temp ", @"C:\temp " }, + { @"C:\temp\file.txt ", @"C:\temp\file.txt " }, + { @"C:/temp/file.txt ", @"C:\temp\file.txt " }, + { @"\\example.com\share.", @"\\example.com\share." }, + { @"//example.com/share.", @"\\example.com\share." }, + { @"\\example.com\share\file.txt.", @"\\example.com\share\file.txt." }, + { @"//example.com/share./file.txt.", @"\\example.com\share.\file.txt." }, + { @"\\example.com\share ", @"\\example.com\share " }, + { @"//example.com/share ", @"\\example.com\share " }, + { @"\\example.com\share\file.txt ", @"\\example.com\share\file.txt " }, + { @"//example.com/share/file.txt ", @"\\example.com\share\file.txt " }, + }; + foreach (var keyValuePair in testCasesWherePathGetFullGivesDifferentResultsThanDotNet) + { + var path = keyValuePair.Key; + var expected = keyValuePair.Value; + // PathGetFullPath should give the expected result + Assert.AreEqual( + expected, + SystemIO.IO_WIN.PathGetFullPath(path)); + // Path.GetFullPath should not give the expected result + Assert.AreNotEqual( + expected, + System.IO.Path.GetFullPath(path)); + } } - } -} + } +} \ No newline at end of file From 44673ef759a32326cda21d752839e5b036a1a467 Mon Sep 17 00:00:00 2001 From: Dean Ferreyra Date: Wed, 10 Nov 2021 20:33:17 -0800 Subject: [PATCH 6/6] Changes to IOTests per review, plus other improvements Add path to assertion messages to pinpoint test case on failures. --- Duplicati/UnitTest/IOTests.cs | 25 +++++++++++-------------- 1 file changed, 11 insertions(+), 14 deletions(-) diff --git a/Duplicati/UnitTest/IOTests.cs b/Duplicati/UnitTest/IOTests.cs index 72d3c9413..6c18f27f6 100644 --- a/Duplicati/UnitTest/IOTests.cs +++ b/Duplicati/UnitTest/IOTests.cs @@ -156,7 +156,9 @@ namespace Duplicati.UnitTest }; foreach (var path in testCasesLeavingPathUnchanged) { - Assert.AreEqual(path, SystemIOWindows.PrefixWithUNC(path)); + var actual = SystemIOWindows.PrefixWithUNC(path); + var expected = path; + Assert.AreEqual(expected, actual, $"Path: {path}"); } var testCasesWherePrefixIsApplied = @@ -196,9 +198,10 @@ namespace Duplicati.UnitTest }; foreach (var keyValuePair in testCasesWherePrefixIsApplied) { - var actual = SystemIOWindows.PrefixWithUNC(keyValuePair.Key); + var path = keyValuePair.Key; + var actual = SystemIOWindows.PrefixWithUNC(path); var expected = keyValuePair.Value; - Assert.AreEqual(expected, actual); + Assert.AreEqual(expected, actual, $"Path: {path}"); } } @@ -274,9 +277,9 @@ namespace Duplicati.UnitTest }; foreach (var path in testCasesWherePathGetFullGivesSameResultsAsDotNet) { - Assert.AreEqual( - System.IO.Path.GetFullPath(path), - SystemIO.IO_WIN.PathGetFullPath(path)); + var actual = SystemIO.IO_WIN.PathGetFullPath(path); + var expected = System.IO.Path.GetFullPath(path); + Assert.AreEqual(expected, actual, $"Path: {path}"); } var testCasesWherePathGetFullGivesDifferentResultsThanDotNet = @@ -305,15 +308,9 @@ namespace Duplicati.UnitTest foreach (var keyValuePair in testCasesWherePathGetFullGivesDifferentResultsThanDotNet) { var path = keyValuePair.Key; + var actual = SystemIO.IO_WIN.PathGetFullPath(path); var expected = keyValuePair.Value; - // PathGetFullPath should give the expected result - Assert.AreEqual( - expected, - SystemIO.IO_WIN.PathGetFullPath(path)); - // Path.GetFullPath should not give the expected result - Assert.AreNotEqual( - expected, - System.IO.Path.GetFullPath(path)); + Assert.AreEqual(expected, actual, $"Path: {path}"); } } }