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:
authorwarwickmm <warwickmm@users.noreply.github.com>2021-11-12 05:41:49 +0300
committerGitHub <noreply@github.com>2021-11-12 05:41:49 +0300
commitea98145f8b992ff53adf704ff2d399e7760fbb39 (patch)
tree8ef3cb28c469d1588d2406b857809c55c71f3d35 /Duplicati/Library
parent0a1b32e1887c98c6034c9fafdfddcb8f8f31e207 (diff)
parent44673ef759a32326cda21d752839e5b036a1a467 (diff)
Merge pull request #4634 from dferreyra/fix_windows_relative_paths
Fix handling of relative paths when adding prefix to Windows paths.
Diffstat (limited to 'Duplicati/Library')
-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
4 files changed, 264 insertions, 8 deletions
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)));
}
}