diff --git a/THIRD-PARTY-NOTICES.txt b/THIRD-PARTY-NOTICES.txt
new file mode 100644
index 0000000000..21f25610a0
--- /dev/null
+++ b/THIRD-PARTY-NOTICES.txt
@@ -0,0 +1,18 @@
+.NET System Web Adapters uses third-party libraries or other resources that may be
+distributed under licenses different than the .NET System Web Adapters software.
+
+In the event that we accidentally failed to list a required notice, please
+bring it to our attention. Post an issue or email us:
+
+ dotnet@microsoft.com
+
+The attached notices are provided for information only.
+
+License notice for .NET Framework
+-------------------------------
+
+Copyright (c) Microsoft Corporation
+Licenses under the MIT License
+
+Available at
+https://github.com/microsoft/referencesource/blob/master/LICENSE.txt
\ No newline at end of file
diff --git a/src/Microsoft.AspNetCore.SystemWebAdapters/Generated/ExcludedAttributes.txt b/src/Microsoft.AspNetCore.SystemWebAdapters/Generated/ExcludedAttributes.txt
new file mode 100644
index 0000000000..4437f6a625
--- /dev/null
+++ b/src/Microsoft.AspNetCore.SystemWebAdapters/Generated/ExcludedAttributes.txt
@@ -0,0 +1 @@
+T:System.Diagnostics.CodeAnalysis.NotNullIfNotNullAttribute
diff --git a/src/Microsoft.AspNetCore.SystemWebAdapters/Generated/GenerateApis.targets b/src/Microsoft.AspNetCore.SystemWebAdapters/Generated/GenerateApis.targets
index 1d9eb01a8d..8a80ea65e8 100644
--- a/src/Microsoft.AspNetCore.SystemWebAdapters/Generated/GenerateApis.targets
+++ b/src/Microsoft.AspNetCore.SystemWebAdapters/Generated/GenerateApis.targets
@@ -8,9 +8,10 @@
<__Generate Condition="$(GenerateTypeForwards) OR $(GenerateStandard)">true
$(MSBuildThisFileDirectory)\Header.txt
+ $(MSBuildThisFileDirectory)\ExcludedAttributes.txt
$(MSBuildThisFileDirectory)\TypeForwards.Framework.cs
$(MSBuildThisFileDirectory)\Ref.Standard.cs
- -excludeApiList $(MSBuildThisFileDirectory)ExcludedApis.txt -throw "$(PlatformNotSupportedMessage)" -HeaderFile "$(GenAPIHeaderFile)"
+ -excludeApiList $(MSBuildThisFileDirectory)ExcludedApis.txt -throw "$(PlatformNotSupportedMessage)" -HeaderFile "$(GenAPIHeaderFile)" -excludeAttributesList "$(GenAPIExcludedAttributes)"
$(GenAPIAdditionalParameters) -writer TypeForwards
diff --git a/src/Microsoft.AspNetCore.SystemWebAdapters/Generated/Ref.Standard.cs b/src/Microsoft.AspNetCore.SystemWebAdapters/Generated/Ref.Standard.cs
index 08dd233207..92dc75546b 100644
--- a/src/Microsoft.AspNetCore.SystemWebAdapters/Generated/Ref.Standard.cs
+++ b/src/Microsoft.AspNetCore.SystemWebAdapters/Generated/Ref.Standard.cs
@@ -442,6 +442,22 @@ public enum ReadEntityBodyMode
Classic = 1,
None = 0,
}
+ public static partial class VirtualPathUtility
+ {
+ public static string AppendTrailingSlash(string virtualPath) { throw new System.PlatformNotSupportedException("Only supported when running on ASP.NET Core or System.Web");}
+ public static string Combine(string basePath, string relativePath) { throw new System.PlatformNotSupportedException("Only supported when running on ASP.NET Core or System.Web");}
+ public static string GetDirectory(string virtualPath) { throw new System.PlatformNotSupportedException("Only supported when running on ASP.NET Core or System.Web");}
+ public static string GetExtension(string virtualPath) { throw new System.PlatformNotSupportedException("Only supported when running on ASP.NET Core or System.Web");}
+ public static string GetFileName(string virtualPath) { throw new System.PlatformNotSupportedException("Only supported when running on ASP.NET Core or System.Web");}
+ public static bool IsAbsolute(string virtualPath) { throw new System.PlatformNotSupportedException("Only supported when running on ASP.NET Core or System.Web");}
+ public static bool IsAppRelative(string virtualPath) { throw new System.PlatformNotSupportedException("Only supported when running on ASP.NET Core or System.Web");}
+ public static string MakeRelative(string fromPath, string toPath) { throw new System.PlatformNotSupportedException("Only supported when running on ASP.NET Core or System.Web");}
+ public static string RemoveTrailingSlash(string virtualPath) { throw new System.PlatformNotSupportedException("Only supported when running on ASP.NET Core or System.Web");}
+ public static string ToAbsolute(string virtualPath) { throw new System.PlatformNotSupportedException("Only supported when running on ASP.NET Core or System.Web");}
+ public static string ToAbsolute(string virtualPath, string applicationPath) { throw new System.PlatformNotSupportedException("Only supported when running on ASP.NET Core or System.Web");}
+ public static string ToAppRelative(string virtualPath) { throw new System.PlatformNotSupportedException("Only supported when running on ASP.NET Core or System.Web");}
+ public static string ToAppRelative(string virtualPath, string applicationPath) { throw new System.PlatformNotSupportedException("Only supported when running on ASP.NET Core or System.Web");}
+ }
}
namespace System.Web.Caching
{
diff --git a/src/Microsoft.AspNetCore.SystemWebAdapters/Generated/TypeForwards.Framework.cs b/src/Microsoft.AspNetCore.SystemWebAdapters/Generated/TypeForwards.Framework.cs
index f0e0cf2c6d..92d6700e46 100644
--- a/src/Microsoft.AspNetCore.SystemWebAdapters/Generated/TypeForwards.Framework.cs
+++ b/src/Microsoft.AspNetCore.SystemWebAdapters/Generated/TypeForwards.Framework.cs
@@ -42,6 +42,7 @@
[assembly:System.Runtime.CompilerServices.TypeForwardedTo(typeof(System.Web.HttpUnhandledException))]
[assembly:System.Runtime.CompilerServices.TypeForwardedTo(typeof(System.Web.ISubscriptionToken))]
[assembly:System.Runtime.CompilerServices.TypeForwardedTo(typeof(System.Web.ReadEntityBodyMode))]
+[assembly:System.Runtime.CompilerServices.TypeForwardedTo(typeof(System.Web.VirtualPathUtility))]
[assembly:System.Runtime.CompilerServices.TypeForwardedTo(typeof(System.Web.Caching.Cache))]
[assembly:System.Runtime.CompilerServices.TypeForwardedTo(typeof(System.Web.Caching.CacheDependency))]
[assembly:System.Runtime.CompilerServices.TypeForwardedTo(typeof(System.Web.Caching.CacheItemPriority))]
diff --git a/src/Microsoft.AspNetCore.SystemWebAdapters/Microsoft.AspNetCore.SystemWebAdapters.csproj b/src/Microsoft.AspNetCore.SystemWebAdapters/Microsoft.AspNetCore.SystemWebAdapters.csproj
index 15b817bdee..0f16c1b999 100644
--- a/src/Microsoft.AspNetCore.SystemWebAdapters/Microsoft.AspNetCore.SystemWebAdapters.csproj
+++ b/src/Microsoft.AspNetCore.SystemWebAdapters/Microsoft.AspNetCore.SystemWebAdapters.csproj
@@ -33,7 +33,6 @@
-
diff --git a/src/Microsoft.AspNetCore.SystemWebAdapters/Utilities/StringUtil.cs b/src/Microsoft.AspNetCore.SystemWebAdapters/Utilities/StringUtil.cs
new file mode 100644
index 0000000000..e2b14f68d7
--- /dev/null
+++ b/src/Microsoft.AspNetCore.SystemWebAdapters/Utilities/StringUtil.cs
@@ -0,0 +1,26 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+namespace System.Web.Util;
+
+///
+/// Provides utility methods for string operations.
+///
+///
+/// Portions of this code are based on code originally Copyright (c) 1999 Microsoft Corporation
+/// System.Web.Util.StringUtil at https://github.com/microsoft/referencesource/blob/master/System.Web/Util/StringUtil.cs
+/// These files are released under an MIT licence according to https://github.com/microsoft/referencesource#license
+///
+internal static class StringUtil
+{
+ /*
+ * Determines if the first string starts with the second string, ignoring case.
+ * Fast, non-culture aware.
+ */
+ internal static bool StringStartsWithIgnoreCase(string s1, string s2)
+ {
+ if (string.IsNullOrEmpty(s1) || string.IsNullOrEmpty(s2)) return false;
+ if (s2.Length > s1.Length) return false;
+ return s1.StartsWith(s2, StringComparison.OrdinalIgnoreCase);
+ }
+}
diff --git a/src/Microsoft.AspNetCore.SystemWebAdapters/Utilities/UrlPath.cs b/src/Microsoft.AspNetCore.SystemWebAdapters/Utilities/UrlPath.cs
new file mode 100644
index 0000000000..73a34401ab
--- /dev/null
+++ b/src/Microsoft.AspNetCore.SystemWebAdapters/Utilities/UrlPath.cs
@@ -0,0 +1,511 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System.Collections.Generic;
+using System.Diagnostics;
+using System.Diagnostics.CodeAnalysis;
+using System.Text;
+
+namespace System.Web.Util;
+
+///
+/// Provides utility methods for handling url and virtual paths.
+///
+///
+/// Portions of this code are based on code originally Copyright (c) 1999 Microsoft Corporation
+/// System.Web.Util.UrlPath code at https://github.com/microsoft/referencesource/blob/master/System.Web/Util/UrlPath.cs
+/// These files are released under an MIT licence according to https://github.com/microsoft/referencesource#license
+///
+internal static class UrlPath
+{
+ internal const char AppRelativeCharacter = '~';
+ internal const string AppRelativeCharacterString = "~/";
+ private const string Invalid_vpath = "'{0}' is not a valid virtual path.";
+ private const string Physical_path_not_allowed = "'{0}' is a physical path, but a virtual path was expected.";
+ private const string Cannot_exit_up_top_directory = "Cannot use a leading .. to exit above the top directory.";
+ internal const string Path_must_be_rooted = "The virtual path '{0}' is not rooted.";
+
+ internal static bool HasTrailingSlash(string virtualPath) => virtualPath[^1] == '/';
+
+ internal static bool IsRooted(string basepath) => string.IsNullOrEmpty(basepath) || basepath[0] == '/' || basepath[0] == '\\';
+
+ // Checks if virtual path contains a protocol, which is referred to as a scheme in the
+ // URI spec.
+ private static bool HasScheme(string virtualPath)
+ {
+ // URIs have the format :, e.g. mailto:user@ms.com,
+ // http://server/, nettcp://server/, etc. The cannot contain slashes.
+ // The virtualPath passed to this method may be absolute or relative. Although
+ // ':' is only allowed in the if it is encoded, the
+ // virtual path that we're receiving here may be decoded, so it is impossible
+ // for us to determine if virtualPath has a scheme. We will be conservative
+ // and err on the side of assuming it has a scheme when we cannot tell for certain.
+ // To do this, we first check for ':'. If not found, then it doesn't have a scheme.
+ // If ':' is found, then as long as we find a '/' before the ':', it cannot be
+ // a scheme because schemes don't contain '/'. Otherwise, we will assume it has a
+ // scheme.
+ var indexOfColon = virtualPath.IndexOf(':');
+ if (indexOfColon == -1)
+ return false;
+ var indexOfSlash = virtualPath.IndexOf('/');
+ return indexOfSlash == -1 || indexOfColon < indexOfSlash;
+ }
+
+ private static bool IsDirectorySeparatorChar(char ch) => ch == '\\' || ch == '/';
+
+ private static bool IsUncSharePath(string path)
+ {
+ // e.g \\server\share\foo or //server/share/foo
+ if (path.Length > 2 && IsDirectorySeparatorChar(path[0]) && IsDirectorySeparatorChar(path[1]))
+ return true;
+ return false;
+ }
+
+ private static bool IsAbsolutePhysicalPath(string path)
+ {
+ if (path == null || path.Length < 3) return false;
+
+ // e.g c:\foo
+ if (path[1] == ':' && IsDirectorySeparatorChar(path[2])) return true;
+
+ // e.g \\server\share\foo or //server/share/foo
+ return IsUncSharePath(path);
+ }
+
+ internal static void CheckValidVirtualPath(string path)
+ {
+
+ // Check if it looks like a physical path (UNC shares and C:)
+ if (IsAbsolutePhysicalPath(path))
+ {
+ throw new HttpException(string.Format(Physical_path_not_allowed, path));
+ }
+
+ // Virtual path can't have colons.
+ var iqs = path.IndexOf('?');
+ if (iqs >= 0)
+ {
+ path = path[..iqs];
+ }
+ if (HasScheme(path))
+ {
+ throw new HttpException(string.Format(Invalid_vpath, path));
+ }
+ }
+
+ internal static string Combine(string appPath, string basepath, string relative)
+ {
+ string path;
+
+ if (string.IsNullOrEmpty(relative))
+ {
+ throw new ArgumentNullException(nameof(relative));
+ }
+ if (string.IsNullOrEmpty(basepath))
+ {
+ throw new ArgumentNullException(nameof(basepath));
+ }
+
+ if (basepath[0] == AppRelativeCharacter && basepath.Length == 1)
+ {
+ // If it's "~", change it to "~/"
+ basepath = AppRelativeCharacterString;
+ }
+ else
+ {
+ // If the base path includes a file name, get rid of it before combining
+ var lastSlashIndex = basepath.LastIndexOf('/');
+ Debug.Assert(lastSlashIndex >= 0);
+ if (lastSlashIndex < basepath.Length - 1)
+ {
+ basepath = basepath[..(lastSlashIndex + 1)];
+ }
+ }
+
+ // Make sure it's a virtual path
+ Util.UrlPath.CheckValidVirtualPath(relative);
+
+ if (Util.UrlPath.IsRooted(relative))
+ {
+ path = relative;
+ }
+ else
+ {
+ // If the path is exactly "~", just return the app root path
+ if (relative.Length == 1 && relative[0] == AppRelativeCharacter)
+ return appPath;
+
+ // If the relative path starts with "~/" or "~\", treat it as app root
+ // relative
+ if (IsAppRelativePath(relative))
+ {
+ if (appPath.Length > 1)
+ path = string.Concat(appPath, "/", relative.AsSpan(2));
+ else
+ path = string.Concat("/", relative.AsSpan(2));
+ }
+ else
+ {
+ path = SimpleCombine(basepath, relative);
+ }
+ }
+
+ return Reduce(path);
+ }
+
+ // This simple version of combine should only be used when the relative
+ // path is known to be relative. It's more efficient, but doesn't do any
+ // sanity checks.
+ internal static string SimpleCombine(string basepath, string relative)
+ {
+ Debug.Assert(!string.IsNullOrEmpty(basepath));
+ Debug.Assert(!string.IsNullOrEmpty(relative));
+ Debug.Assert(relative[0] != '/');
+
+ if (HasTrailingSlash(basepath))
+ return basepath + relative;
+ else
+ return basepath + "/" + relative;
+ }
+
+ internal static string Reduce(string path)
+ {
+ // ignore query string
+ string? queryString = null;
+ if (!string.IsNullOrEmpty(path))
+ {
+ var iqs = path.IndexOf('?');
+ if (iqs >= 0)
+ {
+ queryString = path[iqs..];
+ path = path[..iqs];
+ }
+ }
+
+ // Take care of backslashes and duplicate slashes
+ path = FixVirtualPathSlashes(path);
+
+ path = ReduceVirtualPath(path);
+
+ return queryString != null ? path + queryString : path;
+ }
+
+ // Change backslashes to forward slashes, and remove duplicate slashes
+ internal static string FixVirtualPathSlashes(string virtualPath)
+ {
+ // Make sure we don't have any back slashes
+ virtualPath = virtualPath.Replace('\\', '/');
+
+ // Replace any double forward slashes
+ for (; ; )
+ {
+ var newPath = virtualPath.Replace("//", "/");
+
+ // If it didn't do anything, we're done
+ if (newPath == (object)virtualPath)
+ break;
+
+ // We need to loop again to take care of triple (or more) slashes
+ virtualPath = newPath;
+ }
+
+ return virtualPath;
+ }
+
+ internal static string MakeVirtualPathAppAbsolute(string virtualPath)
+ {
+ return MakeVirtualPathAppAbsolute(virtualPath, HttpRuntime.AppDomainAppVirtualPath);
+ }
+
+ // If a virtual path is app relative (i.e. starts with ~/), change it to
+ // start with the actuall app path.
+ // E.g. ~/Sub/foo.aspx --> /MyApp/Sub/foo.aspx
+ internal static string MakeVirtualPathAppAbsolute(string virtualPath, string applicationPath)
+ {
+ // If the path is exactly "~", just return the app root path
+ if (virtualPath.Length == 1 && virtualPath[0] == AppRelativeCharacter)
+ return applicationPath;
+
+ // If the virtual path starts with "~/" or "~\", replace with the app path
+ // relative
+ if (virtualPath.Length >= 2 && virtualPath[0] == AppRelativeCharacter &&
+ (virtualPath[1] == '/' || virtualPath[1] == '\\'))
+ {
+
+ if (applicationPath.Length > 1)
+ {
+ Debug.Assert(HasTrailingSlash(applicationPath));
+ return string.Concat(applicationPath, virtualPath.AsSpan(2));
+ }
+ else
+ return string.Concat("/", virtualPath.AsSpan(2));
+ }
+
+ // Don't allow relative paths, since they cannot be made App Absolute
+ if (!Util.UrlPath.IsRooted(virtualPath))
+ throw new ArgumentOutOfRangeException(nameof(virtualPath));
+
+ // Return it unchanged
+ return virtualPath;
+ }
+
+ internal static bool VirtualPathStartsWithVirtualPath(string virtualPath1, string virtualPath2)
+ {
+ if (virtualPath1 == null)
+ {
+ throw new ArgumentNullException(nameof(virtualPath1));
+ }
+
+ if (virtualPath2 == null)
+ {
+ throw new ArgumentNullException(nameof(virtualPath2));
+ }
+
+ // if virtualPath1 as a string doesn't start with virtualPath2 as s string, then no for sure
+ if (!StringUtil.StringStartsWithIgnoreCase(virtualPath1, virtualPath2))
+ {
+ return false;
+ }
+
+ var virtualPath2Length = virtualPath2.Length;
+
+ // same length - same path
+ if (virtualPath1.Length == virtualPath2Length)
+ {
+ return true;
+ }
+
+ // Special case for apps rooted at the root.
+ if (virtualPath2Length == 1)
+ {
+ Debug.Assert(virtualPath2[0] == '/');
+ return true;
+ }
+
+ // If virtualPath2 ends with a '/', it's definitely a child
+ if (virtualPath2[virtualPath2Length - 1] == '/')
+ return true;
+
+ // If it doesn't, make sure the next char in virtualPath1 is a '/'.
+ // e.g. /app1 vs /app11
+ if (virtualPath1[virtualPath2Length] != '/')
+ {
+ return false;
+ }
+
+ // passed all checks
+ return true;
+ }
+
+ internal static bool IsAppRelativePath(string? virtualPath)
+ {
+ if (virtualPath is null) return false;
+ var len = virtualPath.Length;
+
+ // Empty string case
+ if (len == 0) return false;
+
+ // It must start with ~
+ if (virtualPath[0] != AppRelativeCharacter) return false;
+
+ // Single character case: "~"
+ if (len == 1) return true;
+
+ // If it's longer, checks if it starts with "~/" or "~\"
+ return virtualPath[1] == '\\' || virtualPath[1] == '/';
+ }
+
+ // Same as Reduce, but for a virtual path that is known to be well formed
+ internal static string ReduceVirtualPath(string path)
+ {
+
+ var length = path.Length;
+ int examine;
+
+ // quickly rule out situations in which there are no . or ..
+
+ for (examine = 0; ; examine++)
+ {
+ examine = path.IndexOf('.', examine);
+ if (examine < 0)
+ return path;
+
+ if ((examine == 0 || path[examine - 1] == '/')
+ && (examine + 1 == length || path[examine + 1] == '/' ||
+ path[examine + 1] == '.' && (examine + 2 == length || path[examine + 2] == '/')))
+ break;
+ }
+
+ // OK, we found a . or .. so process it:
+
+ List list = new();
+ var sb = new StringBuilder();
+ int start;
+ examine = 0;
+
+ for (; ; )
+ {
+ start = examine;
+ examine = path.IndexOf('/', start + 1);
+
+ if (examine < 0)
+ examine = length;
+
+ if (examine - start <= 3 &&
+ (examine < 1 || path[examine - 1] == '.') &&
+ (start + 1 >= length || path[start + 1] == '.'))
+ {
+ if (examine - start == 3)
+ {
+ if (list.Count == 0)
+ throw new HttpException(Cannot_exit_up_top_directory);
+
+ // We're about to backtrack onto a starting '~', which would yield
+ // incorrect results. Instead, make the path App Absolute, and call
+ // Reduce on that.
+ if (list.Count == 1 && IsAppRelativePath(path))
+ {
+ Debug.Assert(sb.Length == 1);
+ return ReduceVirtualPath(Util.UrlPath.MakeVirtualPathAppAbsolute(path));
+ }
+
+ sb.Length = list[^1];
+ list.RemoveRange(list.Count - 1, 1);
+ }
+ }
+ else
+ {
+ list.Add(sb.Length);
+
+ sb.Append(path, start, examine - start);
+ }
+
+ if (examine == length)
+ break;
+ }
+
+ var result = sb.ToString();
+
+ // If we end up with en empty string, turn it into either "/" or "."
+ if (result.Length == 0)
+ {
+ if (length > 0 && path[0] == '/')
+ result = @"/";
+ else
+ result = ".";
+ }
+
+ return result;
+ }
+
+ // We use file: protocol instead of http:, so that Uri.MakeRelative behaves
+ // in a case insensitive way
+ private const string dummyProtocolAndServer = "file://foo";
+ private static readonly char[] s_slashChars = new char[] { '\\', '/' };
+
+ internal static string MakeRelative(string fromPath, string toPath)
+ {
+ // If either path is app relative (~/...), make it absolute, since the Uri
+ // class wouldn't know how to deal with it.
+ fromPath = Util.UrlPath.MakeVirtualPathAppAbsolute(fromPath);
+ toPath = Util.UrlPath.MakeVirtualPathAppAbsolute(toPath);
+
+ // Make sure both virtual paths are rooted
+ if (!Util.UrlPath.IsRooted(fromPath))
+ throw new ArgumentException(string.Format(Path_must_be_rooted, fromPath));
+ if (!Util.UrlPath.IsRooted(toPath))
+ throw new ArgumentException(string.Format(Path_must_be_rooted, toPath));
+
+ // Remove the query string, so that System.Uri doesn't corrupt it
+ string? queryString = null;
+ if (!string.IsNullOrEmpty(toPath))
+ {
+ var iqs = toPath.IndexOf('?');
+ if (iqs >= 0)
+ {
+ queryString = toPath[iqs..];
+ toPath = toPath[..iqs];
+ }
+ }
+
+ // Uri's need full url's so, we use a dummy root
+ Uri fromUri = new(dummyProtocolAndServer + fromPath);
+ Uri toUri = new(dummyProtocolAndServer + toPath);
+
+ string relativePath;
+
+ // If to and from points to identical path (excluding query and fragment), just use them instead
+ // of returning an empty string.
+ if (fromUri.Equals(toUri))
+ {
+ var iPos = toPath.LastIndexOfAny(s_slashChars);
+
+ if (iPos >= 0)
+ {
+ // If it's the same directory, simply return "./"
+ // Browsers should interpret "./" as the original directory.
+ if (iPos == toPath.Length - 1)
+ {
+ relativePath = "./";
+ }
+ else
+ {
+ relativePath = toPath[(iPos + 1)..];
+ }
+ }
+ else
+ {
+ relativePath = toPath;
+ }
+ }
+ else
+ {
+ // To avoid deprecation warning. It says we should use MakeRelativeUri instead (which returns a Uri),
+ // but we wouldn't gain anything from it. The way we use MakeRelative is hacky anyway (fake protocol, ...),
+ // and I don't want to take the chance of breaking something with this change.
+#pragma warning disable 0618
+ relativePath = fromUri.MakeRelative(toUri);
+#pragma warning restore 0618
+ }
+
+ // Note that we need to re-append the query string and fragment (e.g. #anchor)
+ return relativePath + queryString + toUri.Fragment;
+ }
+
+ internal static string? GetFileName(string virtualPath)
+ {
+ if (virtualPath is not null)
+ {
+ var length = virtualPath.Length;
+ for (var i = length; --i >= 0;)
+ {
+ var ch = virtualPath[i];
+ if (ch == '/')
+ return virtualPath.Substring(i + 1, length - i - 1);
+ }
+ }
+ return virtualPath;
+ }
+
+ [return: NotNullIfNotNull("virtualPath")]
+ internal static string? GetExtension(string? virtualPath)
+ {
+ if (virtualPath is null) return null;
+
+ var length = virtualPath.Length;
+ for (var i = length; --i >= 0;)
+ {
+ var ch = virtualPath[i];
+ if (ch == '.')
+ {
+ if (i != length - 1)
+ return virtualPath[i..length];
+ else
+ return string.Empty;
+ }
+ if (ch == '/')
+ break;
+ }
+ return string.Empty;
+ }
+}
diff --git a/src/Microsoft.AspNetCore.SystemWebAdapters/VirtualPathUtility.cs b/src/Microsoft.AspNetCore.SystemWebAdapters/VirtualPathUtility.cs
new file mode 100644
index 0000000000..fe9169fb0c
--- /dev/null
+++ b/src/Microsoft.AspNetCore.SystemWebAdapters/VirtualPathUtility.cs
@@ -0,0 +1,218 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System.Collections.Generic;
+using System.Diagnostics;
+using System.Diagnostics.CodeAnalysis;
+using System.Text;
+
+namespace System.Web;
+
+///
+/// Provides utility methods for common virtual path operations.
+///
+///
+/// Portions of this code are based on code originally Copyright (c) 1999 Microsoft Corporation
+/// System.Web.Util.UrlPath code at https://github.com/microsoft/referencesource/blob/master/System.Web/Util/UrlPath.cs
+/// System.Web.Util.StringUtil at https://github.com/microsoft/referencesource/blob/master/System.Web/Util/StringUtil.cs
+/// These files are released under an MIT licence according to https://github.com/microsoft/referencesource#license
+///
+public static class VirtualPathUtility
+{
+
+ #region "Error strings"
+ private const string Empty_path_has_no_directory = "Empty path has no directory.";
+ #endregion
+
+ /// Appends the literal slash mark (/) to the end of the virtual path, if one does not already exist.
+ /// The modified virtual path.
+ /// The virtual path to append the slash mark to.
+ [return: NotNullIfNotNull("virtualPath")]
+ public static string? AppendTrailingSlash(string? virtualPath)
+ {
+ if (virtualPath == null) return null;
+
+ var l = virtualPath.Length;
+ if (l == 0) return virtualPath;
+
+ if (virtualPath[l - 1] != '/')
+ virtualPath += '/';
+
+ return virtualPath;
+ }
+
+ /// Combines a base path and a relative path.
+ /// The combined and .
+ /// The base path.
+ /// The relative path.
+ ///
+ /// is a physical path.-or- includes one or more colons.
+ ///
+ /// is null or an empty string.-or- is null or an empty string.
+ public static string Combine(string basePath, string relativePath)
+ {
+ return Util.UrlPath.Combine(HttpRuntime.AppDomainAppVirtualPath, basePath, relativePath);
+ }
+
+ /// Returns the directory portion of a virtual path.
+ /// The directory referenced in the virtual path.
+ /// The virtual path.
+ ///
+ /// is not rooted. - or - is null or an empty string.
+ public static string? GetDirectory(string virtualPath)
+ {
+ if (string.IsNullOrEmpty(virtualPath))
+ throw new ArgumentNullException(nameof(virtualPath), Empty_path_has_no_directory);
+
+ if (virtualPath[0] != '/' && virtualPath[0] != Util.UrlPath.AppRelativeCharacter)
+ throw new ArgumentException(string.Format(Util.UrlPath.Path_must_be_rooted, virtualPath), nameof(virtualPath));
+
+ if ((virtualPath[0] == Util.UrlPath.AppRelativeCharacter && virtualPath.Length == 1) || virtualPath == Util.UrlPath.AppRelativeCharacterString) return "/";
+ if (virtualPath.Length == 1) return null;
+
+ var slashIndex = virtualPath.LastIndexOf('/');
+
+ // This could happen if the input looks like "~abc"
+ if (slashIndex < 0)
+ throw new ArgumentException(string.Format(Util.UrlPath.Path_must_be_rooted, virtualPath), nameof(virtualPath));
+
+ return virtualPath[..(slashIndex + 1)];
+ }
+
+ /// Retrieves the extension of the file that is referenced in the virtual path.
+ /// The file name extension string literal, including the period (.), null, or an empty string ("").
+ /// The virtual path.
+ ///
+ /// contains one or more characters that are not valid, as defined in .
+ public static string? GetExtension(string virtualPath)
+ {
+ if (string.IsNullOrEmpty(virtualPath)) throw new ArgumentNullException(nameof(virtualPath));
+
+ return Util.UrlPath.GetExtension(virtualPath);
+ }
+
+ /// Retrieves the file name of the file that is referenced in the virtual path.
+ /// The file name literal after the last directory character in ; otherwise, the last directory name, if the last character of is a directory or volume separator character.
+ /// The virtual path.
+ ///
+ /// contains one or more characters that are not valid, as defined in .
+ public static string? GetFileName(string virtualPath)
+ {
+ if (string.IsNullOrEmpty(virtualPath)) throw new ArgumentNullException(nameof(virtualPath));
+ if (!IsAppRelative(virtualPath) && !Util.UrlPath.IsRooted(virtualPath)) throw new ArgumentException($"The relative virtual path '{virtualPath}' is not allowed here.", nameof(virtualPath));
+ return Util.UrlPath.GetFileName(virtualPath);
+ }
+
+ /// Returns a Boolean value indicating whether the specified virtual path is absolute; that is, it starts with a literal slash mark (/).
+ /// true if is an absolute path and is not null or an empty string (""); otherwise, false.
+ /// The virtual path to check.
+ ///
+ /// is null.
+ public static bool IsAbsolute(string virtualPath)
+ {
+ if (string.IsNullOrEmpty(virtualPath)) throw new ArgumentNullException(nameof(virtualPath));
+ return Util.UrlPath.IsRooted(virtualPath);
+ }
+
+ /// Returns a Boolean value indicating whether the specified virtual path is relative to the application.
+ /// true if is relative to the application; otherwise, false.
+ /// The virtual path to check.
+ ///
+ /// is null.
+ public static bool IsAppRelative(string virtualPath)
+ {
+ if (string.IsNullOrEmpty(virtualPath)) throw new ArgumentNullException(nameof(virtualPath));
+ return Util.UrlPath.IsAppRelativePath(virtualPath);
+ }
+
+ /// Returns the relative virtual path from one virtual path containing the root operator (the tilde [~]) to another.
+ /// The relative virtual path from to .
+ /// The starting virtual path to return the relative virtual path from.
+ /// The ending virtual path to return the relative virtual path to.
+ ///
+ /// is not rooted.- or - is not rooted.
+ public static string MakeRelative(string fromPath, string toPath) => Util.UrlPath.MakeRelative(fromPath, toPath);
+
+ /// Removes a trailing slash mark (/) from a virtual path.
+ /// A virtual path without a trailing slash mark, if the virtual path is not already the root directory ("/"); otherwise, null.
+ /// The virtual path to remove any trailing slash mark from.
+ public static string? RemoveTrailingSlash(string virtualPath)
+ {
+ if (string.IsNullOrEmpty(virtualPath)) return null;
+ var l = virtualPath.Length;
+ if (l <= 1 || virtualPath[l - 1] != '/') return virtualPath;
+ return virtualPath[..(l - 1)];
+ }
+
+ /// Converts a virtual path to an application absolute path.
+ /// The absolute path representation of the specified virtual path.
+ /// The virtual path to convert to an application-relative path.
+ ///
+ /// is not rooted.
+ /// A leading double period (..) is used to exit above the top directory.
+ public static string ToAbsolute(string virtualPath)
+ {
+ if (Util.UrlPath.IsRooted(virtualPath)) return virtualPath;
+ if (IsAppRelative(virtualPath)) return Util.UrlPath.ReduceVirtualPath(Util.UrlPath.MakeVirtualPathAppAbsolute(virtualPath));
+ throw new ArgumentException($"The relative virtual path '{virtualPath}' is not allowed here.", nameof(virtualPath));
+ }
+
+ /// Converts a virtual path to an application absolute path using the specified application path.
+ /// The absolute virtual path representation of .
+ /// The virtual path to convert to an application-relative path.
+ /// The application path to use to convert to a relative path.
+ ///
+ /// is not rooted.
+ /// A leading double period (..) is used in the application path to exit above the top directory.
+ public static string ToAbsolute(string virtualPath, string applicationPath)
+ {
+ if (string.IsNullOrEmpty(applicationPath)) throw new ArgumentNullException(nameof(applicationPath));
+ if (!Util.UrlPath.IsRooted(applicationPath)) throw new ArgumentException($"The relative virtual path '{virtualPath}' is not allowed here.", nameof(applicationPath));
+ if (Util.UrlPath.IsRooted(virtualPath)) return virtualPath;
+ var appPath = AppendTrailingSlash(applicationPath);
+ if (IsAppRelative(virtualPath)) return Util.UrlPath.ReduceVirtualPath(Util.UrlPath.MakeVirtualPathAppAbsolute(virtualPath, appPath));
+ throw new ArgumentException($"The relative virtual path '{virtualPath}' is not allowed here.", nameof(virtualPath));
+ }
+
+ /// Converts a virtual path to an application-relative path using the application virtual path that is in the property.
+ /// The application-relative path representation of .
+ /// The virtual path to convert to an application-relative path.
+ ///
+ /// is null.
+ public static string ToAppRelative(string virtualPath) => ToAppRelative(virtualPath, HttpRuntime.AppDomainAppVirtualPath);
+
+ /// Converts a virtual path to an application-relative path using a specified application path.
+ /// The application-relative path representation of .
+ /// The virtual path to convert to an application-relative path.
+ /// The application path to use to convert to a relative path.
+ public static string ToAppRelative(string virtualPath, string applicationPath)
+ {
+ var appPath = AppendTrailingSlash(applicationPath);
+ if (virtualPath == null)
+ throw new ArgumentNullException(nameof(virtualPath));
+
+ var appPathLength = appPath.Length;
+ var virtualPathLength = virtualPath.Length;
+
+ // If virtualPath is the same as the app path, but without the ending slash,
+ // treat it as if it were truly the app path (VSWhidbey 495949)
+ if (virtualPathLength == appPathLength - 1)
+ {
+ if (Util.StringUtil.StringStartsWithIgnoreCase(appPath, virtualPath))
+ return Util.UrlPath.AppRelativeCharacterString;
+ }
+
+ if (!Util.UrlPath.VirtualPathStartsWithVirtualPath(virtualPath, appPath))
+ return virtualPath;
+
+ // If they are the same, just return "~/"
+ if (virtualPathLength == appPathLength)
+ return Util.UrlPath.AppRelativeCharacterString;
+
+ // Special case for apps rooted at the root:
+ if (appPathLength == 1)
+ return Util.UrlPath.AppRelativeCharacter + virtualPath;
+
+ return Util.UrlPath.AppRelativeCharacter + virtualPath[(appPathLength - 1)..];
+ }
+}
diff --git a/test/Microsoft.AspNetCore.SystemWebAdapters.Tests/VirtualPathUtilityTests.cs b/test/Microsoft.AspNetCore.SystemWebAdapters.Tests/VirtualPathUtilityTests.cs
new file mode 100644
index 0000000000..3a9fd8d196
--- /dev/null
+++ b/test/Microsoft.AspNetCore.SystemWebAdapters.Tests/VirtualPathUtilityTests.cs
@@ -0,0 +1,226 @@
+using System;
+using System.Web;
+using Xunit;
+
+namespace Microsoft.AspNetCore.SystemWebAdapters.Tests
+{
+ public class VirtualPathUtilityTests
+ {
+ public VirtualPathUtilityTests() => HttpRuntime.Current = new DefaultHttpRuntime();
+
+ internal class DefaultHttpRuntime : IHttpRuntime
+ {
+ public string AppDomainAppVirtualPath => "/";
+ }
+
+ [InlineData("/", "/")]
+ [InlineData("~/", "/")]
+ [InlineData("~/test/../other", "/other")]
+ [Theory]
+ public void ToAbsolute(string virtualPath, string expected)
+ {
+ Assert.Equal(expected, (string?)VirtualPathUtility.ToAbsolute(virtualPath, "/"));
+ }
+
+ [Fact]
+ public void ToAbsoluteError()
+ {
+ // This does not match the documentation
+ // https://docs.microsoft.com/en-us/dotnet/api/system.web.virtualpathutility.toabsolute?view=netframework-4.8
+ // but it does match the actual framework
+ Assert.Throws(() => VirtualPathUtility.ToAbsolute("hello", "/"));
+ Assert.Throws(() => VirtualPathUtility.ToAbsolute("../../test", "/"));
+ Assert.Throws(() => VirtualPathUtility.ToAbsolute("~hello", "/"));
+ Assert.Throws(() => VirtualPathUtility.ToAbsolute("~/../../test", "/"));
+ Assert.Throws(() => VirtualPathUtility.ToAbsolute("~/hello", null!));
+ Assert.Throws(() => VirtualPathUtility.ToAbsolute("~/hello", ""));
+ Assert.Throws(() => VirtualPathUtility.ToAbsolute("~/hello", "world"));
+ }
+
+ [InlineData("/", "~/")]
+ [InlineData("~/", "~/")]
+ [Theory]
+ public void ToAppRelative(string virtualPath, string expected)
+ {
+ Assert.Equal(expected, (string?)VirtualPathUtility.ToAppRelative(virtualPath, "/"));
+ }
+
+
+ [InlineData("~/test", "hello", "~/hello")]
+ [InlineData("~/test/world", "hello", "~/test/hello")]
+ [InlineData("~/test/world", "../hello", "~/hello")]
+ [Theory]
+ public void Combine(string basePath, string relativePath, string expected)
+ {
+ Assert.Equal(expected, (string?)VirtualPathUtility.Combine(basePath, relativePath));
+ }
+
+ [Fact]
+ public void CombineError()
+ {
+ Assert.Throws(() => VirtualPathUtility.Combine("~/", "../../"));
+ }
+
+ [InlineData("", "")] // This isn't mentioned in the docs but matches the behaviour of ASP.NET 4.x
+ [InlineData(null, null)] // This isn't mentioned in the docs but matches the behaviour of ASP.NET 4.x
+ [InlineData("/", "/")]
+ [InlineData("/test", "/test/")]
+ [InlineData("test", "test/")]
+ [Theory]
+ public void AppendTrailingSlash(string virtualPath, string expected)
+ {
+ Assert.Equal(expected, (string?)VirtualPathUtility.AppendTrailingSlash(virtualPath));
+ }
+
+ [InlineData(null, null)]
+ [InlineData("", null)]
+ [InlineData("/", "/")]
+ [InlineData("/test/", "/test")]
+ [Theory]
+ public void RemoveTrailingSlash(string virtualPath, string expected)
+ {
+ Assert.Equal(expected, (string?)VirtualPathUtility.RemoveTrailingSlash(virtualPath));
+ }
+
+ // These are conditional so that these tests can be run against net48.
+ // The GetDirectory function doesn't work unless for app relative paths unless running as a web application.
+#if NET6_0_OR_GREATER
+ [InlineData("~", "/")]
+ [InlineData("~/", "/")]
+ [InlineData("~/test", "~/")]
+ [InlineData("~/test/world.jpg", "~/test/")]
+#endif
+ [InlineData("/", null)]
+ [InlineData("/test", "/")]
+ [InlineData("/test/world.jpg", "/test/")]
+ [Theory]
+ public void GetDirectory(string virtualPath, string expected)
+ {
+ Assert.Equal(expected, (string?)VirtualPathUtility.GetDirectory(virtualPath));
+ }
+
+ [Fact]
+ public void GetDirectoryError()
+ {
+ Assert.Throws(() => VirtualPathUtility.GetDirectory(null!));
+ Assert.Throws(() => VirtualPathUtility.GetDirectory(""));
+ Assert.Throws(() => VirtualPathUtility.GetDirectory("test"));
+ Assert.Throws(() => VirtualPathUtility.GetDirectory("test/world.jpg"));
+ }
+
+ [InlineData("/", "")]
+ [InlineData("/test", "")]
+ [InlineData("test", "")]
+ [InlineData("/test/world.jpg", ".jpg")]
+ [InlineData("test/world.jpg", ".jpg")]
+ [Theory]
+ public void GetExtension(string virtualPath, string expected)
+ {
+ Assert.Equal(expected, (string?)VirtualPathUtility.GetExtension(virtualPath));
+ }
+
+ [Fact]
+ public void GetExtensionError()
+ {
+ Assert.Throws(() => VirtualPathUtility.GetExtension(null!));
+ Assert.Throws(() => VirtualPathUtility.GetExtension(""));
+ }
+
+ [InlineData("/", "")]
+ [InlineData("/test", "test")]
+ [InlineData("/test/world.jpg", "world.jpg")]
+ [InlineData("~/", "")]
+ [InlineData("~/test", "test")]
+ [InlineData("~/test/world.jpg", "world.jpg")]
+ [Theory]
+ public void GetFileName(string virtualPath, string expected)
+ {
+ Assert.Equal(expected, (string?)VirtualPathUtility.GetFileName(virtualPath));
+ }
+
+ [Fact]
+ public void GetFileNameError()
+ {
+ Assert.Throws(() => VirtualPathUtility.GetFileName(null!));
+ Assert.Throws(() => VirtualPathUtility.GetFileName(""));
+ Assert.Throws(() => VirtualPathUtility.GetFileName("test"));
+ Assert.Throws(() => VirtualPathUtility.GetFileName("test/world.jpg"));
+ }
+
+ [InlineData("~", false)]
+ [InlineData("/", true)]
+ [InlineData("~/", false)]
+ [InlineData("/test", true)]
+ [InlineData("test", false)]
+ [InlineData("/test/world.jpg", true)]
+ [InlineData("test/world.jpg", false)]
+ [InlineData("~/test", false)]
+ [InlineData("~/test/world.jpg", false)]
+ [Theory]
+ public void IsAbsolute(string virtualPath, bool expected)
+ {
+ Assert.Equal(expected, VirtualPathUtility.IsAbsolute(virtualPath));
+ }
+
+ [Fact]
+ public void IsAbsoluteError()
+ {
+ Assert.Throws(() => VirtualPathUtility.IsAbsolute(null!));
+ Assert.Throws(() => VirtualPathUtility.IsAbsolute(""));
+ }
+
+ [InlineData("~", true)]
+ [InlineData("/", false)]
+ [InlineData("~/", true)]
+ [InlineData("/test", false)]
+ [InlineData("test", false)]
+ [InlineData("/test/world.jpg", false)]
+ [InlineData("test/world.jpg", false)]
+ [InlineData("~/test", true)]
+ [InlineData("~/test/world.jpg", true)]
+ [Theory]
+ public void IsAppRelative(string virtualPath, bool expected)
+ {
+ Assert.Equal(expected, VirtualPathUtility.IsAppRelative(virtualPath));
+ }
+
+ [Fact]
+ public void IsAppRelativeError()
+ {
+ Assert.Throws(() => VirtualPathUtility.IsAppRelative(null!));
+ Assert.Throws(() => VirtualPathUtility.IsAppRelative(""));
+ }
+
+ // These are conditional so that these tests can be run against net48.
+ // The MakeRelative function doesn't work unless for app relative paths unless running as a web application.
+#if NET6_0_OR_GREATER
+ [InlineData("~/home/index", "~/api/weather/get", "../api/weather/get")]
+ [InlineData("~/home/index", "~/other/index", "../other/index")]
+ [InlineData("~/home/index", "~/home/other", "other")]
+ [InlineData("~/home/index", "/api/weather/get", "../api/weather/get")]
+ [InlineData("/home/index", "~/other/index", "../other/index")]
+ [InlineData("", "~/", "./")]
+ [InlineData("~/", "", "")]
+ [InlineData("", "/", "./")]
+ [InlineData("/", "", "")]
+#endif
+ [InlineData("/home/index", "/api/weather/get", "../api/weather/get")]
+ [InlineData("/home/index", "/other/index", "../other/index")]
+ [InlineData("/home/index", "/home/other", "other")]
+ [InlineData("/directory1/file1.aspx", "/directory2/file2.aspx", "../directory2/file2.aspx")]
+ [Theory]
+ public void MakeRelative(string fromPath, string toPath, string expected)
+ {
+ Assert.Equal(expected, VirtualPathUtility.MakeRelative(fromPath, toPath));
+ }
+
+ [Fact]
+ public void MakeRelativeError()
+ {
+ Assert.Throws(() => VirtualPathUtility.MakeRelative("~/", null!));
+ Assert.Throws(() => VirtualPathUtility.MakeRelative(null!, "~/"));
+ Assert.Throws(() => VirtualPathUtility.MakeRelative("~/hello/", "test"));
+ Assert.Throws(() => VirtualPathUtility.MakeRelative("test", "~/hello/"));
+ }
+ }
+}