Merge pull request #4634 from dferreyra/fix_windows_relative_paths

Fix handling of relative paths when adding prefix to Windows paths.
This commit is contained in:
warwickmm
2021-11-11 18:41:49 -08:00
committed by GitHub
8 changed files with 561 additions and 12 deletions
+1 -1
View File
@@ -25,7 +25,7 @@ using System.Threading;
namespace Duplicati.CommandLine.BackendTool
{
class Program
public class Program
{
/// <summary>
/// The main entry point for the application.
@@ -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" />
+59
View File
@@ -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);
}
}
}
+133
View File
@@ -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;
}
}
}
+70 -8
View File
@@ -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)));
}
}
+74
View File
@@ -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);
}
}
}
}
@@ -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" />
+221 -3
View File
@@ -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}");
}
}
}
}