Welcome to mirror list, hosted at ThFree Co, Russian Federation.

github.com/duplicati/duplicati.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--Duplicati/CommandLine/BackendTool/Program.cs2
-rw-r--r--Duplicati/Library/Common/Duplicati.Library.Common.csproj2
-rwxr-xr-xDuplicati/Library/Common/IO/Path.cs59
-rwxr-xr-xDuplicati/Library/Common/IO/PathInternal.Windows.cs133
-rw-r--r--Duplicati/Library/Common/IO/SystemIOWindows.cs78
-rwxr-xr-xDuplicati/UnitTest/BackendToolTests.cs74
-rw-r--r--Duplicati/UnitTest/Duplicati.UnitTest.csproj1
-rw-r--r--Duplicati/UnitTest/IOTests.cs224
8 files changed, 561 insertions, 12 deletions
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
{
/// <summary>
/// The main entry point for the application.
diff --git a/Duplicati/Library/Common/Duplicati.Library.Common.csproj b/Duplicati/Library/Common/Duplicati.Library.Common.csproj
index 6fc16df6a..abd423d19 100644
--- a/Duplicati/Library/Common/Duplicati.Library.Common.csproj
+++ b/Duplicati/Library/Common/Duplicati.Library.Common.csproj
@@ -62,6 +62,8 @@
<Reference Include="System.Transactions" />
</ItemGroup>
<ItemGroup>
+ <Compile Include="IO\Path.cs" />
+ <Compile Include="IO\PathInternal.Windows.cs" />
<Compile Include="Properties\AssemblyInfo.cs" />
<Compile Include="Platform\Platform.cs" />
<Compile Include="IO\SystemIO.cs" />
diff --git a/Duplicati/Library/Common/IO/Path.cs b/Duplicati/Library/Common/IO/Path.cs
new file mode 100755
index 000000000..6a830a955
--- /dev/null
+++ b/Duplicati/Library/Common/IO/Path.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
+ {
+ /// <summary>
+ /// 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.
+ /// </summary>
+ /// <remarks>
+ /// Handles paths that use the alternate directory separator. It is a frequent mistake to
+ /// assume that rooted paths <see cref="Path.IsPathRooted(string)"/> 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).
+ /// </remarks>
+ /// <exception cref="ArgumentNullException">
+ /// Thrown if <paramref name="path"/> is null.
+ /// </exception>
+ 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/PathInternal.Windows.cs b/Duplicati/Library/Common/IO/PathInternal.Windows.cs
new file mode 100755
index 000000000..75a117392
--- /dev/null
+++ b/Duplicati/Library/Common/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
+{
+ /// <summary>Contains internal path helpers that are shared between many projects.</summary>
+ 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;
+
+ /// <summary>
+ /// Returns true if the given character is a valid drive letter
+ /// </summary>
+ internal static bool IsValidDriveChar(char value)
+ {
+ return ((value >= 'A' && value <= 'Z') || (value >= 'a' && value <= 'z'));
+ }
+
+ /// <summary>
+ /// 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).
+ /// </summary>
+ /// <remarks>
+ /// 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).
+ /// </remarks>
+ 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]));
+ }
+
+ /// <summary>
+ /// True if the given character is a directory separator.
+ /// </summary>
+ [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 b29a34331..52f316576 100644
--- a/Duplicati/Library/Common/IO/SystemIOWindows.cs
+++ b/Duplicati/Library/Common/IO/SystemIOWindows.cs
@@ -29,20 +29,51 @@ 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;
+ /// <summary>
+ /// 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).
+ /// </summary>
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
+ {
+ 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;
+ }
+ }
+ }
+
+ 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 +82,32 @@ namespace Duplicati.Library.Common.IO
path.StartsWith(UNCPREFIX, StringComparison.Ordinal);
}
+ private static string[] relativePathComponents = new[] { ".", ".." };
+
+ /// <summary>
+ /// Returns true if <paramref name="path"/> contains relative path components; i.e., "." or "..".
+ /// </summary>
+ private static bool HasRelativePathComponents(string path)
+ {
+ return GetPathComponents(path).Any(pathComponent => relativePathComponents.Contains(pathComponent));
+ }
+
+ /// <summary>
+ /// Returns a sequence representing the files and directories in <paramref name="path"/>.
+ /// </summary>
+ private static IEnumerable<string> 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))
@@ -407,13 +464,18 @@ namespace Duplicati.Library.Common.IO
public string PathGetFullPath(string 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 System.IO.Path.GetFullPath(ConvertSlashes(path));
+ return path;
}
else
{
- return StripUNCPrefix(System.IO.Path.GetFullPath(PrefixWithUNC(ConvertSlashes(path))));
+ return StripUNCPrefix(Path.GetFullPath(PrefixWithUNC(path)));
}
}
diff --git a/Duplicati/UnitTest/BackendToolTests.cs b/Duplicati/UnitTest/BackendToolTests.cs
new file mode 100755
index 000000000..6c4b17122
--- /dev/null
+++ b/Duplicati/UnitTest/BackendToolTests.cs
@@ -0,0 +1,74 @@
+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
+ {
+ [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<string, string>(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 @@
<Reference Include="System.Web" />
</ItemGroup>
<ItemGroup>
+ <Compile Include="BackendToolTests.cs" />
<Compile Include="SymLinkTests.cs" />
<Compile Include="ControllerTests.cs" />
<Compile Include="DeleteHandlerTests.cs" />
diff --git a/Duplicati/UnitTest/IOTests.cs b/Duplicati/UnitTest/IOTests.cs
index 48b4ef5ca..6c18f27f6 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")]
@@ -95,5 +96,222 @@ namespace Duplicati.UnitTest
//In particular don't throw PathTooLongException
Assert.Throws<System.IO.DirectoryNotFoundException>(() => SystemIO.IO_OS.GetDirectories(longPath));
}
- }
-}
+
+ [Test]
+ public void TestPrefixWithUNCInWindowsClient()
+ {
+ if (!Platform.IsClientWindows)
+ {
+ return;
+ }
+
+ 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
+ @"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",
+
+ // 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)
+ {
+ var actual = SystemIOWindows.PrefixWithUNC(path);
+ var expected = path;
+ Assert.AreEqual(expected, actual, $"Path: {path}");
+ }
+
+ var testCasesWherePrefixIsApplied =
+ new Dictionary<string, string>()
+ {
+ // 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 path = keyValuePair.Key;
+ var actual = SystemIOWindows.PrefixWithUNC(path);
+ var expected = keyValuePair.Value;
+ Assert.AreEqual(expected, actual, $"Path: {path}");
+ }
+ }
+
+ [Test]
+ public void TestPathGetFullPathInWindowsClient()
+ {
+ if (!Platform.IsClientWindows)
+ {
+ return;
+ }
+
+ 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)
+ {
+ var actual = SystemIO.IO_WIN.PathGetFullPath(path);
+ var expected = System.IO.Path.GetFullPath(path);
+ Assert.AreEqual(expected, actual, $"Path: {path}");
+ }
+
+ var testCasesWherePathGetFullGivesDifferentResultsThanDotNet =
+ new Dictionary<string, string>()
+ {
+ // 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 actual = SystemIO.IO_WIN.PathGetFullPath(path);
+ var expected = keyValuePair.Value;
+ Assert.AreEqual(expected, actual, $"Path: {path}");
+ }
+ }
+ }
+} \ No newline at end of file