From fc1413c5cd336ab8b1a11e7daaf727b6b3b3c2ae Mon Sep 17 00:00:00 2001 From: Taylor Southwick Date: Fri, 17 Feb 2023 13:04:48 -0800 Subject: [PATCH 1/2] Refactor HttpRuntime usage to minimize statics usage In ASP.NET Framework, HttpRuntime was used as a static holder for the runtime information. We expose this type in the adapters, but it should not be accessed directly internally due to its static nature. This currently causes sporadic test failures. Rather, internally should access the IHttpRuntime if this information is needed. This change includes: - Changes UrlPath to be a class that takes in the IHttpRuntime - Moves the logic of VirtualPathUtility into a VirtualPathUtilityImpl that is an instance class - VirtualPathUtility uses a cached instance of VirtualPathUtilityImpl when needed - Current usage of VirtualPathUtility in the adapters is replaced with the VirtualPathUtilityImpl - Tests are updated to no longer require setting HttpRuntime.Current --- .../Caching/CacheDependency.cs | 17 +- .../HttpRuntime.cs | 9 +- .../HttpServerUtility.cs | 29 ++- .../Utilities/UrlPath.cs | 139 +++++++---- .../VirtualPathUtility.cs | 147 ++---------- .../VirtualPathUtilityImpl.cs | 227 ++++++++++++++++++ .../CacheTests.cs | 39 +-- .../HttpServerUtilityTests.cs | 46 ++-- .../NonGenericCollectionWrapperTests.cs | 3 + .../NonGenericDictionaryWrapperTests.cs | 3 + ...ServerVariablesNameValueCollectionTests.cs | 3 + ...aluesDictionaryNameValueCollectionTests.cs | 3 + .../StringValuesNameValueCollectionTests.cs | 3 + .../VirtualPathUtilityTests.cs | 117 +++++---- 14 files changed, 486 insertions(+), 299 deletions(-) create mode 100644 src/Microsoft.AspNetCore.SystemWebAdapters/VirtualPathUtilityImpl.cs diff --git a/src/Microsoft.AspNetCore.SystemWebAdapters/Caching/CacheDependency.cs b/src/Microsoft.AspNetCore.SystemWebAdapters/Caching/CacheDependency.cs index b920b047c0..0e6daf66f1 100644 --- a/src/Microsoft.AspNetCore.SystemWebAdapters/Caching/CacheDependency.cs +++ b/src/Microsoft.AspNetCore.SystemWebAdapters/Caching/CacheDependency.cs @@ -10,7 +10,6 @@ namespace System.Web.Caching; public class CacheDependency : IDisposable { private readonly List changeMonitors = new(); - private bool hasChanged; private bool disposedValue; private DateTime utcLastModified; private Action? dependencyChangedAction; @@ -56,8 +55,7 @@ public CacheDependency( if (cachekeys is not null && cachekeys.Length != 0) { - changeMonitors.Add(HttpRuntime.Cache.ObjectCache - .CreateCacheEntryChangeMonitor(cachekeys)); + changeMonitors.Add(HttpRuntime.Cache.ObjectCache.CreateCacheEntryChangeMonitor(cachekeys)); } if (dependency is not null) @@ -70,9 +68,9 @@ public CacheDependency( protected internal void FinishInit() { - hasChanged = changeMonitors.Any(cm => cm.HasChanged && (cm.GetLastModifiedUtc() > utcStart)); + HasChanged = changeMonitors.Any(cm => cm.HasChanged && (cm.GetLastModifiedUtc() > utcStart)); utcLastModified = changeMonitors.Max(cm => cm.GetLastModifiedUtc()); - if (hasChanged) + if (HasChanged) { NotifyDependencyChanged(this, EventArgs.Empty); } @@ -93,7 +91,7 @@ protected void NotifyDependencyChanged(object sender, EventArgs e) { if (initCompleted && DateTime.UtcNow > utcStart) { - hasChanged = true; + HasChanged = true; utcLastModified = DateTime.UtcNow; dependencyChangedAction?.Invoke(sender, e); } @@ -104,9 +102,9 @@ protected void NotifyDependencyChanged(object sender, EventArgs e) public void SetCacheDependencyChanged(Action dependencyChangedAction) => this.dependencyChangedAction = dependencyChangedAction; - public virtual string[] GetFileDependencies() => changeMonitors.OfType().SelectMany(cm=>cm.FilePaths).ToArray(); + public virtual string[] GetFileDependencies() => changeMonitors.OfType().SelectMany(cm => cm.FilePaths).ToArray(); - public bool HasChanged => hasChanged; + public bool HasChanged { get; private set; } public DateTime UtcLastModified => changeMonitors .OfType() @@ -116,7 +114,8 @@ public void SetCacheDependencyChanged(Action dependencyChange public virtual string? GetUniqueID() { - if (!uniqueIdInitialized) { + if (!uniqueIdInitialized) + { uniqueId = changeMonitors.Any(cm => cm.UniqueId is null) ? null : string.Join(":", changeMonitors.Select(cm => cm.UniqueId)); diff --git a/src/Microsoft.AspNetCore.SystemWebAdapters/HttpRuntime.cs b/src/Microsoft.AspNetCore.SystemWebAdapters/HttpRuntime.cs index b7d341d8c3..72a08dcc2d 100644 --- a/src/Microsoft.AspNetCore.SystemWebAdapters/HttpRuntime.cs +++ b/src/Microsoft.AspNetCore.SystemWebAdapters/HttpRuntime.cs @@ -9,6 +9,10 @@ public sealed class HttpRuntime { private static IHttpRuntime? _current; + /// + /// Gets the current . This should not be used internally besides where is strictly necessary. + /// If this is needed, it should be retrieved through dependency injection. + /// internal static IHttpRuntime Current { get => _current ?? throw new InvalidOperationException("HttpRuntime is not available in the current environment"); @@ -20,7 +24,8 @@ private HttpRuntime() } public static string AppDomainAppVirtualPath => Current.AppDomainAppVirtualPath; - public static string AppDomainAppPath => Current.AppDomainAppPath; - public static System.Web.Caching.Cache Cache => Current.Cache; + public static string AppDomainAppPath => Current.AppDomainAppPath; + + public static Caching.Cache Cache => Current.Cache; } diff --git a/src/Microsoft.AspNetCore.SystemWebAdapters/HttpServerUtility.cs b/src/Microsoft.AspNetCore.SystemWebAdapters/HttpServerUtility.cs index 94b0edadb0..6f53db2813 100644 --- a/src/Microsoft.AspNetCore.SystemWebAdapters/HttpServerUtility.cs +++ b/src/Microsoft.AspNetCore.SystemWebAdapters/HttpServerUtility.cs @@ -1,7 +1,8 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -using Microsoft.AspNetCore.Hosting; +using System.IO; +using Microsoft.AspNetCore.SystemWebAdapters; using Microsoft.AspNetCore.WebUtilities; using Microsoft.Extensions.DependencyInjection; @@ -21,16 +22,24 @@ internal HttpServerUtility(HttpContextCore context) public string MapPath(string? path) { - var appPath = (string.IsNullOrEmpty(path) ? VirtualPathUtility.GetDirectory(_context.Request.Path) : - VirtualPathUtility.Combine( - VirtualPathUtility.GetDirectory(_context.Request.Path) ?? "/" - , path)); - var rootPath = HttpRuntime.AppDomainAppPath; - if (string.IsNullOrEmpty(appPath)) return rootPath; - return System.IO.Path.Combine(rootPath, + var runtime = _context.RequestServices.GetRequiredService(); + + var appPath = string.IsNullOrEmpty(path) + ? VirtualPathUtilityImpl.GetDirectory(_context.Request.Path) + : new VirtualPathUtilityImpl(runtime).Combine(VirtualPathUtilityImpl.GetDirectory(_context.Request.Path) ?? "/", path); + + var rootPath = runtime.AppDomainAppPath; + + if (string.IsNullOrEmpty(appPath)) + { + return rootPath; + } + + return Path.Combine( + rootPath, appPath[1..] - .Replace('/', System.IO.Path.DirectorySeparatorChar)) - .TrimEnd(System.IO.Path.DirectorySeparatorChar); + .Replace('/', Path.DirectorySeparatorChar)) + .TrimEnd(Path.DirectorySeparatorChar); } [Diagnostics.CodeAnalysis.SuppressMessage("Design", "CA1024:Use properties where appropriate", Justification = Constants.ApiFromAspNet)] diff --git a/src/Microsoft.AspNetCore.SystemWebAdapters/Utilities/UrlPath.cs b/src/Microsoft.AspNetCore.SystemWebAdapters/Utilities/UrlPath.cs index 4e6cf3b2c4..3f0958935d 100644 --- a/src/Microsoft.AspNetCore.SystemWebAdapters/Utilities/UrlPath.cs +++ b/src/Microsoft.AspNetCore.SystemWebAdapters/Utilities/UrlPath.cs @@ -6,6 +6,7 @@ using System.Diagnostics.CodeAnalysis; using System.Globalization; using System.Text; +using Microsoft.AspNetCore.SystemWebAdapters; namespace System.Web.Util; @@ -17,8 +18,15 @@ namespace System.Web.Util; /// 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 sealed class UrlPath { + private readonly IHttpRuntime _runtime; + + public UrlPath(IHttpRuntime runtime) + { + _runtime = runtime; + } + internal const char AppRelativeCharacter = '~'; internal const string AppRelativeCharacterString = "~/"; private const string Invalid_vpath = "'{0}' is not a valid virtual path."; @@ -26,7 +34,7 @@ internal static class UrlPath 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] == '/'; + private static bool HasTrailingSlash(string virtualPath) => virtualPath[^1] == '/'; internal static bool IsRooted(string basepath) => string.IsNullOrEmpty(basepath) || basepath[0] == '/' || basepath[0] == '\\'; @@ -47,27 +55,31 @@ private static bool HasScheme(string virtualPath) // scheme. var indexOfColon = virtualPath.IndexOf(':', StringComparison.Ordinal); if (indexOfColon == -1) + { return false; + } + var indexOfSlash = virtualPath.IndexOf('/', StringComparison.Ordinal); 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; - } + // e.g \\server\share\foo or //server/share/foo + private static bool IsUncSharePath(string path) => path.Length > 2 && IsDirectorySeparatorChar(path[0]) && IsDirectorySeparatorChar(path[1]); private static bool IsAbsolutePhysicalPath(string path) { - if (path == null || path.Length < 3) return false; + if (path == null || path.Length < 3) + { + return false; + } // e.g c:\foo - if (path[1] == ':' && IsDirectorySeparatorChar(path[2])) return true; + if (path[1] == ':' && IsDirectorySeparatorChar(path[2])) + { + return true; + } // e.g \\server\share\foo or //server/share/foo return IsUncSharePath(path); @@ -75,7 +87,6 @@ private static bool IsAbsolutePhysicalPath(string path) internal static void CheckValidVirtualPath(string path) { - // Check if it looks like a physical path (UNC shares and C:) if (IsAbsolutePhysicalPath(path)) { @@ -94,7 +105,7 @@ internal static void CheckValidVirtualPath(string path) } } - internal static string Combine(string appPath, string basepath, string relative) + internal string Combine(string appPath, string basepath, string relative) { string path; @@ -124,9 +135,9 @@ internal static string Combine(string appPath, string basepath, string relative) } // Make sure it's a virtual path - Util.UrlPath.CheckValidVirtualPath(relative); + CheckValidVirtualPath(relative); - if (Util.UrlPath.IsRooted(relative)) + if (IsRooted(relative)) { path = relative; } @@ -134,16 +145,22 @@ internal static string Combine(string appPath, string basepath, string relative) { // 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 { @@ -163,13 +180,10 @@ internal static string SimpleCombine(string basepath, string relative) Debug.Assert(!string.IsNullOrEmpty(relative)); Debug.Assert(relative[0] != '/'); - if (HasTrailingSlash(basepath)) - return basepath + relative; - else - return basepath + "/" + relative; + return HasTrailingSlash(basepath) ? basepath + relative : basepath + "/" + relative; } - internal static string Reduce(string path) + internal string Reduce(string path) { // ignore query string string? queryString = null; @@ -204,7 +218,9 @@ internal static string FixVirtualPathSlashes(string virtualPath) // 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; @@ -213,10 +229,7 @@ internal static string FixVirtualPathSlashes(string virtualPath) return virtualPath; } - internal static string MakeVirtualPathAppAbsolute(string virtualPath) - { - return MakeVirtualPathAppAbsolute(virtualPath, HttpRuntime.AppDomainAppVirtualPath); - } + internal string MakeVirtualPathAppAbsolute(string virtualPath) => MakeVirtualPathAppAbsolute(virtualPath, _runtime.AppDomainAppVirtualPath); // If a virtual path is app relative (i.e. starts with ~/), change it to // start with the actuall app path. @@ -225,7 +238,9 @@ internal static string MakeVirtualPathAppAbsolute(string virtualPath, string app { // 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 @@ -239,12 +254,16 @@ internal static string MakeVirtualPathAppAbsolute(string virtualPath, string app 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)) + if (!IsRooted(virtualPath)) + { throw new ArgumentOutOfRangeException(nameof(virtualPath)); + } // Return it unchanged return virtualPath; @@ -279,7 +298,9 @@ internal static bool VirtualPathStartsWithVirtualPath(string virtualPath1, strin // 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 @@ -294,24 +315,37 @@ internal static bool VirtualPathStartsWithVirtualPath(string virtualPath1, strin internal static bool IsAppRelativePath(string? virtualPath) { - if (virtualPath is null) return false; + if (virtualPath is null) + { + return false; + } + var len = virtualPath.Length; // Empty string case - if (len == 0) return false; + if (len == 0) + { + return false; + } // It must start with ~ - if (virtualPath[0] != AppRelativeCharacter) return false; + if (virtualPath[0] != AppRelativeCharacter) + { + return false; + } // Single character case: "~" - if (len == 1) return true; + 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) + internal string ReduceVirtualPath(string path) { var length = path.Length; @@ -323,12 +357,16 @@ internal static string ReduceVirtualPath(string path) { 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: @@ -344,7 +382,9 @@ internal static string ReduceVirtualPath(string path) examine = path.IndexOf('/', start + 1); if (examine < 0) + { examine = length; + } if (examine - start <= 3 && (examine < 1 || path[examine - 1] == '.') && @@ -353,7 +393,9 @@ internal static string ReduceVirtualPath(string path) 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 @@ -361,7 +403,7 @@ internal static string ReduceVirtualPath(string path) if (list.Count == 1 && IsAppRelativePath(path)) { Debug.Assert(sb.Length == 1); - return ReduceVirtualPath(Util.UrlPath.MakeVirtualPathAppAbsolute(path)); + return ReduceVirtualPath(MakeVirtualPathAppAbsolute(path)); } sb.Length = list[^1]; @@ -376,7 +418,9 @@ internal static string ReduceVirtualPath(string path) } if (examine == length) + { break; + } } var result = sb.ToString(); @@ -385,9 +429,13 @@ internal static string ReduceVirtualPath(string path) if (result.Length == 0) { if (length > 0 && path[0] == '/') + { result = @"/"; + } else + { result = "."; + } } return result; @@ -398,18 +446,23 @@ internal static string ReduceVirtualPath(string path) private const string dummyProtocolAndServer = "file://foo"; private static readonly char[] s_slashChars = new char[] { '\\', '/' }; - internal static string MakeRelative(string fromPath, string toPath) + internal 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); + fromPath = MakeVirtualPathAppAbsolute(fromPath); + toPath = MakeVirtualPathAppAbsolute(toPath); // Make sure both virtual paths are rooted - if (!Util.UrlPath.IsRooted(fromPath)) + if (!IsRooted(fromPath)) + { throw new ArgumentException(string.Format(CultureInfo.InvariantCulture, Path_must_be_rooted, fromPath)); - if (!Util.UrlPath.IsRooted(toPath)) + } + + if (!IsRooted(toPath)) + { throw new ArgumentException(string.Format(CultureInfo.InvariantCulture, Path_must_be_rooted, toPath)); + } // Remove the query string, so that System.Uri doesn't corrupt it string? queryString = null; @@ -476,7 +529,9 @@ internal static string MakeRelative(string fromPath, string toPath) { var ch = virtualPath[i]; if (ch == '/') + { return virtualPath.Substring(i + 1, length - i - 1); + } } } return virtualPath; @@ -485,7 +540,10 @@ internal static string MakeRelative(string fromPath, string toPath) [return: NotNullIfNotNull("virtualPath")] internal static string? GetExtension(string? virtualPath) { - if (virtualPath is null) return null; + if (virtualPath is null) + { + return null; + } var length = virtualPath.Length; for (var i = length; --i >= 0;) @@ -493,13 +551,12 @@ internal static string MakeRelative(string fromPath, string toPath) var ch = virtualPath[i]; if (ch == '.') { - if (i != length - 1) - return virtualPath[i..length]; - else - return string.Empty; + return i != length - 1 ? virtualPath[i..length] : string.Empty; } if (ch == '/') + { break; + } } return string.Empty; } diff --git a/src/Microsoft.AspNetCore.SystemWebAdapters/VirtualPathUtility.cs b/src/Microsoft.AspNetCore.SystemWebAdapters/VirtualPathUtility.cs index 16357762e0..f28ef7fe1e 100644 --- a/src/Microsoft.AspNetCore.SystemWebAdapters/VirtualPathUtility.cs +++ b/src/Microsoft.AspNetCore.SystemWebAdapters/VirtualPathUtility.cs @@ -1,46 +1,22 @@ // 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.Globalization; -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 + private static VirtualPathUtilityImpl Impl { get; } = new VirtualPathUtilityImpl(HttpRuntime.Current); /// 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; - } + public static string? AppendTrailingSlash(string? virtualPath) => VirtualPathUtilityImpl.AppendTrailingSlash(virtualPath); /// Combines a base path and a relative path. /// The combined and . @@ -50,81 +26,42 @@ public static class VirtualPathUtility /// 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); - } + public static string Combine(string basePath, string relativePath) => Impl.Combine(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(CultureInfo.InvariantCulture, 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(CultureInfo.InvariantCulture, Util.UrlPath.Path_must_be_rooted, virtualPath), nameof(virtualPath)); - - return virtualPath[..(slashIndex + 1)]; - } + public static string? GetDirectory(string virtualPath) => VirtualPathUtilityImpl.GetDirectory(virtualPath); /// 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); - } + /// contains one or more characters that are not valid, as defined in . + public static string? GetExtension(string virtualPath) => VirtualPathUtilityImpl.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); - } + /// contains one or more characters that are not valid, as defined in . + public static string? GetFileName(string virtualPath) => VirtualPathUtilityImpl.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); - } + public static bool IsAbsolute(string virtualPath) => VirtualPathUtilityImpl.IsAbsolute(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); - } + public static bool IsAppRelative(string virtualPath) => VirtualPathUtilityImpl.IsAppRelative(virtualPath); /// Returns the relative virtual path from one virtual path containing the root operator (the tilde [~]) to another. /// The relative virtual path from to . @@ -132,18 +69,12 @@ public static bool IsAppRelative(string virtualPath) /// 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); + public static string MakeRelative(string fromPath, string toPath) => Impl.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)]; - } + public static string? RemoveTrailingSlash(string virtualPath) => VirtualPathUtilityImpl.RemoveTrailingSlash(virtualPath); /// Converts a virtual path to an application absolute path. /// The absolute path representation of the specified virtual path. @@ -151,12 +82,7 @@ public static bool IsAppRelative(string virtualPath) /// /// 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)); - } + public static string ToAbsolute(string virtualPath) => Impl.ToAbsolute(virtualPath); /// Converts a virtual path to an application absolute path using the specified application path. /// The absolute virtual path representation of . @@ -165,55 +91,18 @@ public static string ToAbsolute(string virtualPath) /// /// 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. + public static string ToAbsolute(string virtualPath, string applicationPath) => Impl.ToAbsolute(virtualPath, applicationPath); + + /// 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); + public static string ToAppRelative(string virtualPath) => Impl.ToAppRelative(virtualPath); /// 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) - { - ArgumentNullException.ThrowIfNull(virtualPath); - - var appPath = AppendTrailingSlash(applicationPath); - - 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)..]; - } + public static string ToAppRelative(string virtualPath, string applicationPath) => VirtualPathUtilityImpl.ToAppRelative(virtualPath, applicationPath); } diff --git a/src/Microsoft.AspNetCore.SystemWebAdapters/VirtualPathUtilityImpl.cs b/src/Microsoft.AspNetCore.SystemWebAdapters/VirtualPathUtilityImpl.cs new file mode 100644 index 0000000000..ab89d25153 --- /dev/null +++ b/src/Microsoft.AspNetCore.SystemWebAdapters/VirtualPathUtilityImpl.cs @@ -0,0 +1,227 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Diagnostics.CodeAnalysis; +using System.Globalization; +using System.Web.Util; +using Microsoft.AspNetCore.SystemWebAdapters; + +namespace System.Web; + +internal sealed class VirtualPathUtilityImpl +{ + private const string Empty_path_has_no_directory = "Empty path has no directory."; + + private readonly IHttpRuntime _runtime; + private readonly UrlPath _urlPath; + + public VirtualPathUtilityImpl(IHttpRuntime runtime) + { + _runtime = runtime; + _urlPath = new UrlPath(runtime); + } + + [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; + } + + public string Combine(string basePath, string relativePath) + => _urlPath.Combine(_runtime.AppDomainAppVirtualPath, basePath, relativePath); + + 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] != UrlPath.AppRelativeCharacter) + { + throw new ArgumentException(string.Format(CultureInfo.InvariantCulture, UrlPath.Path_must_be_rooted, virtualPath), nameof(virtualPath)); + } + + if ((virtualPath[0] == UrlPath.AppRelativeCharacter && virtualPath.Length == 1) || virtualPath == 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(CultureInfo.InvariantCulture, UrlPath.Path_must_be_rooted, virtualPath), nameof(virtualPath)); + } + + return virtualPath[..(slashIndex + 1)]; + } + + public static string? GetExtension(string virtualPath) + { + if (string.IsNullOrEmpty(virtualPath)) + { + throw new ArgumentNullException(nameof(virtualPath)); + } + + return UrlPath.GetExtension(virtualPath); + } + + public static string? GetFileName(string virtualPath) + { + if (string.IsNullOrEmpty(virtualPath)) + { + throw new ArgumentNullException(nameof(virtualPath)); + } + + if (!IsAppRelative(virtualPath) && !UrlPath.IsRooted(virtualPath)) + { + throw new ArgumentException($"The relative virtual path '{virtualPath}' is not allowed here.", nameof(virtualPath)); + } + + return UrlPath.GetFileName(virtualPath); + } + + public static bool IsAbsolute(string virtualPath) + { + if (string.IsNullOrEmpty(virtualPath)) + { + throw new ArgumentNullException(nameof(virtualPath)); + } + + return UrlPath.IsRooted(virtualPath); + } + + public static bool IsAppRelative(string virtualPath) + { + if (string.IsNullOrEmpty(virtualPath)) + { + throw new ArgumentNullException(nameof(virtualPath)); + } + + return UrlPath.IsAppRelativePath(virtualPath); + } + + public string MakeRelative(string fromPath, string toPath) => _urlPath.MakeRelative(fromPath, toPath); + + 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)]; + } + + public string ToAbsolute(string virtualPath) + { + if (UrlPath.IsRooted(virtualPath)) + { + return virtualPath; + } + + if (IsAppRelative(virtualPath)) + { + return _urlPath.ReduceVirtualPath(_urlPath.MakeVirtualPathAppAbsolute(virtualPath)); + } + + throw new ArgumentException($"The relative virtual path '{virtualPath}' is not allowed here.", nameof(virtualPath)); + } + + public string ToAbsolute(string virtualPath, string applicationPath) + { + if (string.IsNullOrEmpty(applicationPath)) + { + throw new ArgumentNullException(nameof(applicationPath)); + } + + if (!UrlPath.IsRooted(applicationPath)) + { + throw new ArgumentException($"The relative virtual path '{virtualPath}' is not allowed here.", nameof(applicationPath)); + } + + if (UrlPath.IsRooted(virtualPath)) + { + return virtualPath; + } + + var appPath = AppendTrailingSlash(applicationPath); + + if (IsAppRelative(virtualPath)) + { + return _urlPath.ReduceVirtualPath(UrlPath.MakeVirtualPathAppAbsolute(virtualPath, appPath)); + } + + throw new ArgumentException($"The relative virtual path '{virtualPath}' is not allowed here.", nameof(virtualPath)); + } + + public string ToAppRelative(string virtualPath) => ToAppRelative(virtualPath, _runtime.AppDomainAppVirtualPath); + + public static string ToAppRelative(string virtualPath, string applicationPath) + { + ArgumentNullException.ThrowIfNull(virtualPath); + + var appPath = AppendTrailingSlash(applicationPath); + + 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 (StringUtil.StringStartsWithIgnoreCase(appPath, virtualPath)) + { + return UrlPath.AppRelativeCharacterString; + } + } + + if (!UrlPath.VirtualPathStartsWithVirtualPath(virtualPath, appPath)) + { + return virtualPath; + } + + // If they are the same, just return "~/" + if (virtualPathLength == appPathLength) + { + return UrlPath.AppRelativeCharacterString; + } + + // Special case for apps rooted at the root: + if (appPathLength == 1) + { + return UrlPath.AppRelativeCharacter + virtualPath; + } + + return UrlPath.AppRelativeCharacter + virtualPath[(appPathLength - 1)..]; + } +} diff --git a/test/Microsoft.AspNetCore.SystemWebAdapters.CoreServices.Tests/CacheTests.cs b/test/Microsoft.AspNetCore.SystemWebAdapters.CoreServices.Tests/CacheTests.cs index 8f0e3de855..bc93811a04 100644 --- a/test/Microsoft.AspNetCore.SystemWebAdapters.CoreServices.Tests/CacheTests.cs +++ b/test/Microsoft.AspNetCore.SystemWebAdapters.CoreServices.Tests/CacheTests.cs @@ -26,7 +26,7 @@ public void CacheFromHttpContext() coreContext.Setup(c => c.RequestServices).Returns(serviceProvider.Object); var context = new HttpContext(coreContext.Object); - + // Act var result = context.Cache; @@ -55,42 +55,5 @@ public void CacheFromHttpContextWrapper() // Assert Assert.Same(cache, result); } - - [Fact] - public void CacheFromHttpRuntime() - { - // Arrange - var cache = new Cache(); - - var httpRuntime = new Mock(); - httpRuntime.Setup(c=>c.Cache).Returns(cache); - - HttpRuntime.Current = httpRuntime.Object; - - // Act - var result = System.Web.HttpRuntime.Cache; - - // Assert - Assert.Same(cache, result); - } - - [Fact] - public void CacheFromHttpRuntimeFactory() - { - // Arrange - var cache = new Cache(); - - var serviceProvider = new Mock(); - serviceProvider.Setup(s => s.GetService(typeof(Cache))).Returns(cache); - - var httpRuntime = HttpRuntimeFactory.Create(serviceProvider.Object); - HttpRuntime.Current = httpRuntime; - - // Act - var result = System.Web.HttpRuntime.Cache; - - // Assert - Assert.Same(cache, result); - } } } diff --git a/test/Microsoft.AspNetCore.SystemWebAdapters.Tests/HttpServerUtilityTests.cs b/test/Microsoft.AspNetCore.SystemWebAdapters.Tests/HttpServerUtilityTests.cs index 7dd26c532e..829659b59e 100644 --- a/test/Microsoft.AspNetCore.SystemWebAdapters.Tests/HttpServerUtilityTests.cs +++ b/test/Microsoft.AspNetCore.SystemWebAdapters.Tests/HttpServerUtilityTests.cs @@ -6,8 +6,6 @@ using System.Text; using System.Web; using System.Web.Caching; -using AutoFixture; -using Microsoft.AspNetCore.Http; using Moq; using Xunit; @@ -15,14 +13,6 @@ namespace Microsoft.AspNetCore.SystemWebAdapters; public class HttpServerUtilityTests { - private readonly Fixture _fixture; - - public HttpServerUtilityTests() - { - _fixture = new Fixture(); - HttpRuntime.Current = new TestHttpRuntime(); - } - internal sealed class TestHttpRuntime : IHttpRuntime { private Cache? _cache; @@ -96,13 +86,13 @@ public void UrlTokenRoundtrip(string input, string expected) } // Test data from https://docs.microsoft.com/en-us/dotnet/api/system.web.httpserverutility.mappath?view=netframework-4.8 - [InlineData("/RootLevelPage.aspx",null,"")] + [InlineData("/RootLevelPage.aspx", null, "")] [InlineData("/RootLevelPage.aspx", "", "")] - [InlineData("/RootLevelPage.aspx", "/DownOneLevel/DownLevelPage.aspx", "DownOneLevel","DownLevelPage.aspx")] + [InlineData("/RootLevelPage.aspx", "/DownOneLevel/DownLevelPage.aspx", "DownOneLevel", "DownLevelPage.aspx")] [InlineData("/RootLevelPage.aspx", "/NotRealFolder", "NotRealFolder")] [InlineData("/DownOneLevel/DownLevelPage.aspx", null, "DownOneLevel")] [InlineData("/DownOneLevel/DownLevelPage.aspx", "../RootLevelPage.aspx", "RootLevelPage.aspx")] - [InlineData("/api/test/request/info", null, "api","test","request")] + [InlineData("/api/test/request/info", null, "api", "test", "request")] [InlineData("/api/test/request/info", "", "api", "test", "request")] [InlineData("/api/test/request/info", "/UploadedFiles", "UploadedFiles")] [InlineData("/api/test/request/info", "UploadedFiles", "api", "test", "request", "UploadedFiles")] @@ -111,10 +101,21 @@ public void UrlTokenRoundtrip(string input, string expected) public void MapPath(string page, string? path, params string[] segments) { // Arrange - var coreContext = new Mock(); + var runtime = new Mock(); + + runtime.Setup(r => r.AppDomainAppVirtualPath).Returns("/"); + runtime.Setup(r => r.AppDomainAppPath).Returns(RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? + @"C:\ExampleSites\TestMapPath" : "/apps/test-map-path"); + + var services = new Mock(); + services.Setup(s => s.GetService(typeof(IHttpRuntime))).Returns(runtime.Object); + var coreRequest = new Mock(); coreRequest.Setup(c => c.Path).Returns(page); + + var coreContext = new Mock(); coreContext.Setup(c => c.Request).Returns(coreRequest.Object); + coreContext.Setup(c => c.RequestServices).Returns(services.Object); var context = new HttpContext(coreContext.Object); @@ -122,7 +123,7 @@ public void MapPath(string page, string? path, params string[] segments) var result = context.Server.MapPath(path); var relative = System.IO.Path.Combine(segments); - var expected = System.IO.Path.Combine(HttpRuntime.Current.AppDomainAppPath, relative); + var expected = System.IO.Path.Combine(runtime.Object.AppDomainAppPath, relative); // Assert Assert.Equal(expected, result); @@ -136,14 +137,25 @@ public void MapPath(string page, string? path, params string[] segments) public void MapPathException(string page, string? path, Type expected) { // Arrange - var coreContext = new Mock(); + var runtime = new Mock(); + + runtime.Setup(r => r.AppDomainAppVirtualPath).Returns("/"); + runtime.Setup(r => r.AppDomainAppPath).Returns(RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? + @"C:\ExampleSites\TestMapPath" : "/apps/test-map-path"); + + var services = new Mock(); + services.Setup(s => s.GetService(typeof(IHttpRuntime))).Returns(runtime.Object); + var coreRequest = new Mock(); coreRequest.Setup(c => c.Path).Returns(page); + + var coreContext = new Mock(); coreContext.Setup(c => c.Request).Returns(coreRequest.Object); + coreContext.Setup(c => c.RequestServices).Returns(services.Object); var context = new HttpContext(coreContext.Object); // Assert - Assert.Throws(expected, ()=> context.Server.MapPath(path)); + Assert.Throws(expected, () => context.Server.MapPath(path)); } } diff --git a/test/Microsoft.AspNetCore.SystemWebAdapters.Tests/Internal/NonGenericCollectionWrapperTests.cs b/test/Microsoft.AspNetCore.SystemWebAdapters.Tests/Internal/NonGenericCollectionWrapperTests.cs index bad6bd4a6d..47cde79601 100644 --- a/test/Microsoft.AspNetCore.SystemWebAdapters.Tests/Internal/NonGenericCollectionWrapperTests.cs +++ b/test/Microsoft.AspNetCore.SystemWebAdapters.Tests/Internal/NonGenericCollectionWrapperTests.cs @@ -1,3 +1,6 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + using System; using System.Collections; using System.Collections.Generic; diff --git a/test/Microsoft.AspNetCore.SystemWebAdapters.Tests/Internal/NonGenericDictionaryWrapperTests.cs b/test/Microsoft.AspNetCore.SystemWebAdapters.Tests/Internal/NonGenericDictionaryWrapperTests.cs index 78f4f78031..289be47cd9 100644 --- a/test/Microsoft.AspNetCore.SystemWebAdapters.Tests/Internal/NonGenericDictionaryWrapperTests.cs +++ b/test/Microsoft.AspNetCore.SystemWebAdapters.Tests/Internal/NonGenericDictionaryWrapperTests.cs @@ -1,3 +1,6 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + using System; using System.Collections; using System.Collections.Generic; diff --git a/test/Microsoft.AspNetCore.SystemWebAdapters.Tests/Internal/ServerVariablesNameValueCollectionTests.cs b/test/Microsoft.AspNetCore.SystemWebAdapters.Tests/Internal/ServerVariablesNameValueCollectionTests.cs index 30b86d4441..d47f2a46e5 100644 --- a/test/Microsoft.AspNetCore.SystemWebAdapters.Tests/Internal/ServerVariablesNameValueCollectionTests.cs +++ b/test/Microsoft.AspNetCore.SystemWebAdapters.Tests/Internal/ServerVariablesNameValueCollectionTests.cs @@ -1,3 +1,6 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + using System; using AutoFixture; using Microsoft.AspNetCore.Http.Features; diff --git a/test/Microsoft.AspNetCore.SystemWebAdapters.Tests/Internal/StringValuesDictionaryNameValueCollectionTests.cs b/test/Microsoft.AspNetCore.SystemWebAdapters.Tests/Internal/StringValuesDictionaryNameValueCollectionTests.cs index b117e93b75..cd291461cf 100644 --- a/test/Microsoft.AspNetCore.SystemWebAdapters.Tests/Internal/StringValuesDictionaryNameValueCollectionTests.cs +++ b/test/Microsoft.AspNetCore.SystemWebAdapters.Tests/Internal/StringValuesDictionaryNameValueCollectionTests.cs @@ -1,3 +1,6 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + using System; using AutoFixture; using Microsoft.Extensions.Primitives; diff --git a/test/Microsoft.AspNetCore.SystemWebAdapters.Tests/Internal/StringValuesNameValueCollectionTests.cs b/test/Microsoft.AspNetCore.SystemWebAdapters.Tests/Internal/StringValuesNameValueCollectionTests.cs index 4138bf56a5..d10557f792 100644 --- a/test/Microsoft.AspNetCore.SystemWebAdapters.Tests/Internal/StringValuesNameValueCollectionTests.cs +++ b/test/Microsoft.AspNetCore.SystemWebAdapters.Tests/Internal/StringValuesNameValueCollectionTests.cs @@ -1,3 +1,6 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + using System; using System.Collections; using System.Collections.Generic; diff --git a/test/Microsoft.AspNetCore.SystemWebAdapters.Tests/VirtualPathUtilityTests.cs b/test/Microsoft.AspNetCore.SystemWebAdapters.Tests/VirtualPathUtilityTests.cs index c5f063b3ae..d85dc1ed82 100644 --- a/test/Microsoft.AspNetCore.SystemWebAdapters.Tests/VirtualPathUtilityTests.cs +++ b/test/Microsoft.AspNetCore.SystemWebAdapters.Tests/VirtualPathUtilityTests.cs @@ -1,47 +1,41 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + using System; using System.Web; -using System.Web.Caching; +using Moq; using Xunit; namespace Microsoft.AspNetCore.SystemWebAdapters.Tests { public class VirtualPathUtilityTests { - public VirtualPathUtilityTests() => HttpRuntime.Current = new TestRuntime(); - - internal sealed class TestRuntime : IHttpRuntime - { - private Cache? _cache; - - public string AppDomainAppVirtualPath => "/"; - - public string AppDomainAppPath => "C:\\"; - - public Cache Cache => _cache ??= new Cache(); - } - [InlineData("/", "/")] [InlineData("~/", "/")] [InlineData("~/test/../other", "/other")] [Theory] public void ToAbsolute(string virtualPath, string expected) { - Assert.Equal(expected, (string?)VirtualPathUtility.ToAbsolute(virtualPath, "/")); + var virtualPathUtility = CreateUtility(); + + Assert.Equal(expected, virtualPathUtility.ToAbsolute(virtualPath, "/")); } [Fact] public void ToAbsoluteError() { + var virtualPathUtility = CreateUtility(); + // This does not match the documentation - // https://docs.microsoft.com/en-us/dotnet/api/system.web.virtualpathutility.toabsolute?view=netframework-4.8 + // https://docs.microsoft.com/en-us/dotnet/api/system.web._utility.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")); + 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("/", "~/")] @@ -49,23 +43,26 @@ public void ToAbsoluteError() [Theory] public void ToAppRelative(string virtualPath, string expected) { - Assert.Equal(expected, (string?)VirtualPathUtility.ToAppRelative(virtualPath, "/")); + Assert.Equal(expected, VirtualPathUtilityImpl.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)); + var virtualPathUtility = CreateUtility(); + + Assert.Equal(expected, virtualPathUtility.Combine(basePath, relativePath)); } [Fact] public void CombineError() { - Assert.Throws(() => VirtualPathUtility.Combine("~/", "../../")); + var virtualPathUtility = CreateUtility(); + + Assert.Throws(() => virtualPathUtility.Combine("~/", "../../")); } [InlineData("", "")] // This isn't mentioned in the docs but matches the behaviour of ASP.NET 4.x @@ -76,7 +73,7 @@ public void CombineError() [Theory] public void AppendTrailingSlash(string virtualPath, string expected) { - Assert.Equal(expected, (string?)VirtualPathUtility.AppendTrailingSlash(virtualPath)); + Assert.Equal(expected, VirtualPathUtilityImpl.AppendTrailingSlash(virtualPath)); } [InlineData(null, null)] @@ -86,7 +83,7 @@ public void AppendTrailingSlash(string virtualPath, string expected) [Theory] public void RemoveTrailingSlash(string virtualPath, string expected) { - Assert.Equal(expected, (string?)VirtualPathUtility.RemoveTrailingSlash(virtualPath)); + Assert.Equal(expected, VirtualPathUtilityImpl.RemoveTrailingSlash(virtualPath)); } // These are conditional so that these tests can be run against net48. @@ -103,16 +100,16 @@ public void RemoveTrailingSlash(string virtualPath, string expected) [Theory] public void GetDirectory(string virtualPath, string expected) { - Assert.Equal(expected, (string?)VirtualPathUtility.GetDirectory(virtualPath)); + Assert.Equal(expected, VirtualPathUtilityImpl.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")); + Assert.Throws(() => VirtualPathUtilityImpl.GetDirectory(null!)); + Assert.Throws(() => VirtualPathUtilityImpl.GetDirectory("")); + Assert.Throws(() => VirtualPathUtilityImpl.GetDirectory("test")); + Assert.Throws(() => VirtualPathUtilityImpl.GetDirectory("test/world.jpg")); } [InlineData("/", "")] @@ -123,14 +120,14 @@ public void GetDirectoryError() [Theory] public void GetExtension(string virtualPath, string expected) { - Assert.Equal(expected, (string?)VirtualPathUtility.GetExtension(virtualPath)); + Assert.Equal(expected, VirtualPathUtilityImpl.GetExtension(virtualPath)); } [Fact] public void GetExtensionError() { - Assert.Throws(() => VirtualPathUtility.GetExtension(null!)); - Assert.Throws(() => VirtualPathUtility.GetExtension("")); + Assert.Throws(() => VirtualPathUtilityImpl.GetExtension(null!)); + Assert.Throws(() => VirtualPathUtilityImpl.GetExtension("")); } [InlineData("/", "")] @@ -142,16 +139,16 @@ public void GetExtensionError() [Theory] public void GetFileName(string virtualPath, string expected) { - Assert.Equal(expected, (string?)VirtualPathUtility.GetFileName(virtualPath)); + Assert.Equal(expected, VirtualPathUtilityImpl.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")); + Assert.Throws(() => VirtualPathUtilityImpl.GetFileName(null!)); + Assert.Throws(() => VirtualPathUtilityImpl.GetFileName("")); + Assert.Throws(() => VirtualPathUtilityImpl.GetFileName("test")); + Assert.Throws(() => VirtualPathUtilityImpl.GetFileName("test/world.jpg")); } [InlineData("~", false)] @@ -166,14 +163,14 @@ public void GetFileNameError() [Theory] public void IsAbsolute(string virtualPath, bool expected) { - Assert.Equal(expected, VirtualPathUtility.IsAbsolute(virtualPath)); + Assert.Equal(expected, VirtualPathUtilityImpl.IsAbsolute(virtualPath)); } [Fact] public void IsAbsoluteError() { - Assert.Throws(() => VirtualPathUtility.IsAbsolute(null!)); - Assert.Throws(() => VirtualPathUtility.IsAbsolute("")); + Assert.Throws(() => VirtualPathUtilityImpl.IsAbsolute(null!)); + Assert.Throws(() => VirtualPathUtilityImpl.IsAbsolute("")); } [InlineData("~", true)] @@ -188,14 +185,14 @@ public void IsAbsoluteError() [Theory] public void IsAppRelative(string virtualPath, bool expected) { - Assert.Equal(expected, VirtualPathUtility.IsAppRelative(virtualPath)); + Assert.Equal(expected, VirtualPathUtilityImpl.IsAppRelative(virtualPath)); } [Fact] public void IsAppRelativeError() { - Assert.Throws(() => VirtualPathUtility.IsAppRelative(null!)); - Assert.Throws(() => VirtualPathUtility.IsAppRelative("")); + Assert.Throws(() => VirtualPathUtilityImpl.IsAppRelative(null!)); + Assert.Throws(() => VirtualPathUtilityImpl.IsAppRelative("")); } // These are conditional so that these tests can be run against net48. @@ -218,16 +215,30 @@ public void IsAppRelativeError() [Theory] public void MakeRelative(string fromPath, string toPath, string expected) { - Assert.Equal(expected, VirtualPathUtility.MakeRelative(fromPath, toPath)); + var virtualPathUtility = CreateUtility(); + + 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/")); + var virtualPathUtility = CreateUtility(); + + Assert.Throws(() => virtualPathUtility.MakeRelative("~/", null!)); + Assert.Throws(() => virtualPathUtility.MakeRelative(null!, "~/")); + Assert.Throws(() => virtualPathUtility.MakeRelative("~/hello/", "test")); + Assert.Throws(() => virtualPathUtility.MakeRelative("test", "~/hello/")); + } + + private static VirtualPathUtilityImpl CreateUtility() + { + var runtime = new Mock(); + + runtime.Setup(r => r.AppDomainAppPath).Returns(@"C:\"); + runtime.Setup(r => r.AppDomainAppVirtualPath).Returns("/"); + + return new VirtualPathUtilityImpl(runtime.Object); } } } From 260a0ac746ac88b9df92fd90200a06071c87b4b8 Mon Sep 17 00:00:00 2001 From: Taylor Southwick Date: Fri, 17 Feb 2023 14:18:25 -0800 Subject: [PATCH 2/2] update refs --- .../Generated/Header.txt | 1 + .../Generated/Ref.Standard.cs | 3 ++- .../Generated/TypeForwards.Framework.cs | 1 + 3 files changed, 4 insertions(+), 1 deletion(-) diff --git a/src/Microsoft.AspNetCore.SystemWebAdapters/Generated/Header.txt b/src/Microsoft.AspNetCore.SystemWebAdapters/Generated/Header.txt index f0a9812166..d6fbbe3b44 100644 --- a/src/Microsoft.AspNetCore.SystemWebAdapters/Generated/Header.txt +++ b/src/Microsoft.AspNetCore.SystemWebAdapters/Generated/Header.txt @@ -17,3 +17,4 @@ #pragma warning disable CS0809 // Obsolete member overrides non-obsolete member #pragma warning disable CA1063 // Implement IDisposable Correctly #pragma warning disable CA1816 // Dispose methods should call SuppressFinalize + diff --git a/src/Microsoft.AspNetCore.SystemWebAdapters/Generated/Ref.Standard.cs b/src/Microsoft.AspNetCore.SystemWebAdapters/Generated/Ref.Standard.cs index 60372589e3..4944f8f891 100644 --- a/src/Microsoft.AspNetCore.SystemWebAdapters/Generated/Ref.Standard.cs +++ b/src/Microsoft.AspNetCore.SystemWebAdapters/Generated/Ref.Standard.cs @@ -17,6 +17,7 @@ #pragma warning disable CS0809 // Obsolete member overrides non-obsolete member #pragma warning disable CA1063 // Implement IDisposable Correctly #pragma warning disable CA1816 // Dispose methods should call SuppressFinalize + namespace System.Web { public partial class HttpBrowserCapabilities : System.Web.Configuration.HttpCapabilitiesBase @@ -547,7 +548,7 @@ public partial class CacheDependency : System.IDisposable public CacheDependency(string[] filenames, System.DateTime start) { throw new System.PlatformNotSupportedException("Only supported when running on ASP.NET Core or System.Web");} public CacheDependency(string[] filenames, string[] cachekeys, System.DateTime start) { throw new System.PlatformNotSupportedException("Only supported when running on ASP.NET Core or System.Web");} public CacheDependency(string[] filenames, string[] cachekeys, System.Web.Caching.CacheDependency dependency, System.DateTime start) { throw new System.PlatformNotSupportedException("Only supported when running on ASP.NET Core or System.Web");} - public bool HasChanged { get { throw new System.PlatformNotSupportedException("Only supported when running on ASP.NET Core or System.Web");} } + public bool HasChanged { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw new System.PlatformNotSupportedException("Only supported when running on ASP.NET Core or System.Web");} } public System.DateTime UtcLastModified { get { throw new System.PlatformNotSupportedException("Only supported when running on ASP.NET Core or System.Web");} } protected virtual void DependencyDispose() { throw new System.PlatformNotSupportedException("Only supported when running on ASP.NET Core or System.Web");} public void Dispose() { } diff --git a/src/Microsoft.AspNetCore.SystemWebAdapters/Generated/TypeForwards.Framework.cs b/src/Microsoft.AspNetCore.SystemWebAdapters/Generated/TypeForwards.Framework.cs index 81f1f7f130..c6112ae152 100644 --- a/src/Microsoft.AspNetCore.SystemWebAdapters/Generated/TypeForwards.Framework.cs +++ b/src/Microsoft.AspNetCore.SystemWebAdapters/Generated/TypeForwards.Framework.cs @@ -17,6 +17,7 @@ #pragma warning disable CS0809 // Obsolete member overrides non-obsolete member #pragma warning disable CA1063 // Implement IDisposable Correctly #pragma warning disable CA1816 // Dispose methods should call SuppressFinalize + [assembly:System.Runtime.CompilerServices.TypeForwardedTo(typeof(System.Web.HttpBrowserCapabilities))] [assembly:System.Runtime.CompilerServices.TypeForwardedTo(typeof(System.Web.HttpBrowserCapabilitiesBase))] [assembly:System.Runtime.CompilerServices.TypeForwardedTo(typeof(System.Web.HttpBrowserCapabilitiesWrapper))]