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:
authorDean Ferreyra <dean@octw.com>2021-11-05 08:27:32 +0300
committerDean Ferreyra <dean@octw.com>2021-11-07 03:14:18 +0300
commit0e8280f6f34af110c18f0a612b015055e75f7f29 (patch)
tree471b69728b8b022728139dcf74b3a4b5fe73bcb9
parent0a1b32e1887c98c6034c9fafdfddcb8f8f31e207 (diff)
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.
-rw-r--r--Duplicati/CommandLine/BackendTool/Program.cs2
-rw-r--r--Duplicati/Library/Common/IO/SystemIOWindows.cs147
-rwxr-xr-xDuplicati/UnitTest/BackendToolTests.cs86
-rw-r--r--Duplicati/UnitTest/Duplicati.UnitTest.csproj1
4 files changed, 224 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/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;
+ /// <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;
+ }
+ 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;
}
- return path.StartsWith(PATHPREFIX_SERVER, StringComparison.Ordinal)
- ? UNCPREFIX_SERVER + path.Substring(PATHPREFIX_SERVER.Length)
- : UNCPREFIX + 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[] { ".", ".." };
+
+ /// <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))
@@ -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)
+ /// <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 !IsPartiallyQualified(path);
+ }
+
+ /// <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>
+ internal static bool IsDirectorySeparator(char c)
+ {
+ return c == Path.DirectorySeparatorChar || c == Path.AltDirectorySeparatorChar;
+ }
+
+ /// <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');
+ }
+ #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<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" />