diff --git a/Duplicati/Library/Utility/Uri.cs b/Duplicati/Library/Utility/Uri.cs index 39c666369..6a26248fd 100644 --- a/Duplicati/Library/Utility/Uri.cs +++ b/Duplicati/Library/Utility/Uri.cs @@ -24,6 +24,8 @@ using System.Collections.Specialized; using System.Linq; using System.Text; +#nullable enable + namespace Duplicati.Library.Utility { // TODO: This class should be deleted. @@ -48,6 +50,11 @@ namespace Duplicati.Library.Utility /// private static readonly System.Text.RegularExpressions.Regex URL_PARSER = new System.Text.RegularExpressions.Regex(@"(?[^:]+)://(((?[^\:\?/]+)(\:(?[^@\:\?/]*))?\@))?((?(?:[^\[/\?\:][^/\?\:]*)|(?:\[[^\]]+\]))(\:(?\d+))?)?((?[^\?]*))?(\?(?.+))?"); + /// + /// Detects a Windows path + /// + private static readonly System.Text.RegularExpressions.Regex WINDOWS_PATH = new System.Text.RegularExpressions.Regex(@"^/?[a-zA-Z]:[\\/]"); + /// /// The URL scheme, e.g. http /// @@ -55,12 +62,12 @@ namespace Duplicati.Library.Utility /// /// The server name, e.g. www.example.com /// - public readonly string Host; + public readonly string? Host; /// /// The server path, e.g. index.html. /// Note that the path does NOT have a leading /. /// - public readonly string Path; + public readonly string? Path; /// /// The server port, e.g. 80, is -1 if using the default port /// @@ -68,15 +75,15 @@ namespace Duplicati.Library.Utility /// /// The querystring, e.g. ?id=1 /// - public readonly string Query; + public readonly string? Query; /// /// The username, if any /// - public readonly string Username; + public readonly string? Username; /// /// The password, if any /// - public readonly string Password; + public readonly string? Password; /// /// The original URI. @@ -86,7 +93,7 @@ namespace Duplicati.Library.Utility /// /// Cache for the query parameters. /// - private NameValueCollection m_queryParams; + private NameValueCollection? m_queryParams; /// /// Gets the parameters in the query string @@ -117,7 +124,7 @@ namespace Duplicati.Library.Utility get { if (string.IsNullOrEmpty(Path)) - return Host; + return Host ?? ""; else if (string.IsNullOrEmpty(Host)) return Path; else @@ -194,6 +201,10 @@ namespace Duplicati.Library.Utility h = null; } + // Support correctly encoded file:///C:\test + if ((h == null || h.Length == 0) && Scheme == "file" && WINDOWS_PATH.IsMatch(p) && p.StartsWith("/", StringComparison.Ordinal)) + p = p.Substring(1); + this.Host = h; this.Path = p; @@ -216,7 +227,7 @@ namespace Duplicati.Library.Utility /// The username /// The password /// The port - public Uri(string scheme, string host, string path = null, string query = null, string username = null, string password = null, int port = -1) + public Uri(string scheme, string? host, string? path = null, string? query = null, string? username = null, string? password = null, int port = -1) { m_queryParams = null; Scheme = scheme; @@ -258,7 +269,7 @@ namespace Duplicati.Library.Utility /// The username /// The password /// The port - private static string AsString(string scheme, string host, string path, string query, string username, string password, int port) + private static string AsString(string scheme, string? host, string? path, string? query, string? username, string? password, int port) { var s = scheme + "://"; if (!string.IsNullOrEmpty(username) || !string.IsNullOrEmpty(password)) @@ -279,8 +290,10 @@ namespace Duplicati.Library.Utility if (!string.IsNullOrEmpty(path)) { - if (!string.IsNullOrEmpty(host)) + // Append the leading `/` to the Windows path for file:///c:\test + if (!string.IsNullOrEmpty(host) || (WINDOWS_PATH.IsMatch(path) && !path.StartsWith("/"))) s += "/"; + s += string.Join('/', path.Split('/').Select(x => UrlPathEncode(x))); } if (!string.IsNullOrEmpty(query)) @@ -353,7 +366,7 @@ namespace Duplicati.Library.Utility /// /// The regular expression that matches %20 type values in a querystring /// - private static readonly System.Text.RegularExpressions.Regex RE_ESCAPECHAR = new System.Text.RegularExpressions.Regex(@"[^0-9a-zA-Z\-_]", System.Text.RegularExpressions.RegexOptions.Compiled); + private static readonly System.Text.RegularExpressions.Regex RE_ESCAPECHAR = new System.Text.RegularExpressions.Regex(@"[^0-9a-zA-Z\-_.]", System.Text.RegularExpressions.RegexOptions.Compiled); /// /// Encodes a URL, like System.Web.HttpUtility.UrlEncode @@ -361,7 +374,7 @@ namespace Duplicati.Library.Utility /// The encoded URL /// The URL fragment to encode /// The encoding to use - public static string UrlPathEncode(string value, System.Text.Encoding encoding = null) + public static string UrlPathEncode(string value, System.Text.Encoding? encoding = null) { if (value == null) throw new ArgumentNullException(nameof(value)); @@ -375,7 +388,7 @@ namespace Duplicati.Library.Utility /// The encoded URL /// The URL fragment to encode /// The encoding to use - public static string UrlEncode(string value, System.Text.Encoding encoding = null, string spacevalue = "+") + public static string UrlEncode(string value, System.Text.Encoding? encoding = null, string spacevalue = "+") { if (value == null) throw new ArgumentNullException(nameof(value)); @@ -419,7 +432,7 @@ namespace Duplicati.Library.Utility /// The decoded URL /// The URL fragment to decode /// The encoding to use - public static string UrlDecode(string value, System.Text.Encoding encoding = null) + public static string UrlDecode(string value, System.Text.Encoding? encoding = null) { if (value == null) throw new ArgumentNullException(nameof(value)); @@ -542,7 +555,7 @@ namespace Duplicati.Library.Utility /// Base URL, containing schema, host, port. /// Base path. /// A collection of name value pairs to be translated into a query string. - public static string UriBuilder(string url, string path, NameValueCollection query) + public static string UriBuilder(string url, string path, NameValueCollection? query) { var builder = new UriBuilder(url) { @@ -558,7 +571,7 @@ namespace Duplicati.Library.Utility /// /// The path. /// URL. - public static string ExtractPath(string url) + public static string? ExtractPath(string url) { return new Uri(url).Path; }