Skip to content

Commit 880657b

Browse files
authored
Add HostingEnvironment.MapPath (#572)
This adds a `MapPath` method to `HostingEnvironment`. It essentially just calls the existing `IMapPathUtility.MapPath()` method that `HttpServerUtility.MapPath` calls. With the new `HostingEnvironment.MapPath` method, an `HttpContext` is not required. Before `IMapPathUtility.MapPath()` is called, there's 2 checks to match the input `HostingEnvironment.MapPath` accepts in .NET Framework. - `HostingEnvironment.MapPath` doesn't accept paths such as `file.txt`. In contrast, `HttpServerUtility.MapPath` does accept this. To match `HostingEnvironment.MapPath` in .NET Framework, the new code throws an exception. - `HostingEnvironment.MapPath` does accept paths that are UNC like, e.g. `\\file` or `//file`, however `MapPathUtility.MapPath()` throws an error. To avoid the error and conform with the behavior in .NET Framework (without modifying `MapPathUtility.MapPath()`), the inbound `virtualPath` is modified by collapsing multiple leading slashes to a single slash, e.g. `/file`. Addresses #460
1 parent 2a91466 commit 880657b

File tree

6 files changed

+147
-6
lines changed

6 files changed

+147
-6
lines changed

src/Microsoft.AspNetCore.SystemWebAdapters/Generated/Ref.Standard.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -973,6 +973,7 @@ public static partial class HostingEnvironment
973973
public static bool IsHosted { get { throw new System.PlatformNotSupportedException("Only supported when running on ASP.NET Core or System.Web");} }
974974
public static string SiteName { get { throw new System.PlatformNotSupportedException("Only supported when running on ASP.NET Core or System.Web");} }
975975
public static System.Web.Hosting.VirtualPathProvider VirtualPathProvider { get { throw new System.PlatformNotSupportedException("Only supported when running on ASP.NET Core or System.Web");} }
976+
public static string MapPath(string virtualPath) { throw new System.PlatformNotSupportedException("Only supported when running on ASP.NET Core or System.Web");}
976977
public static void RegisterVirtualPathProvider(System.Web.Hosting.VirtualPathProvider provider) { throw new System.PlatformNotSupportedException("Only supported when running on ASP.NET Core or System.Web");}
977978
}
978979
public abstract partial class VirtualDirectory : System.Web.Hosting.VirtualFileBase

src/Microsoft.AspNetCore.SystemWebAdapters/Hosting/HostingEnvironment.cs

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22
// The .NET Foundation licenses this file to you under the MIT license.
33

44
using System.Web.Caching;
5+
using System.Web.Util;
6+
using Microsoft.Extensions.DependencyInjection;
57

68
namespace System.Web.Hosting;
79

@@ -26,5 +28,29 @@ public static void RegisterVirtualPathProvider(VirtualPathProvider provider)
2628
HostingEnvironmentAccessor.Current.Options.VirtualPathProvider = provider;
2729
}
2830

31+
public static string MapPath(string? virtualPath)
32+
{
33+
if (string.IsNullOrEmpty(virtualPath))
34+
{
35+
throw new ArgumentNullException(nameof(virtualPath));
36+
}
37+
38+
// original implementation disallows paths that are not virtual or do not begin with a forward slash, e.g. file.txt.
39+
if (!VirtualPathUtilityImpl.IsAppRelative(virtualPath) && UrlPath.FixVirtualPathSlashes(virtualPath)[0] != '/')
40+
{
41+
throw new ArgumentException($"The relative virtual path '{virtualPath}' is not allowed here.");
42+
}
43+
44+
// original implementation allows paths starting with // and \\ but MapPathUtility.MapPath() throws
45+
// an error that the path is a physical path. to avoid the error, collapsing multiple leading slash characters
46+
// to a single slash.
47+
if (UrlPath.IsUncSharePath(virtualPath))
48+
{
49+
virtualPath = $"/{virtualPath.TrimStart('/', '\\')}";
50+
}
51+
52+
return HttpRuntime.WebObjectActivator.GetRequiredService<IMapPathUtility>().MapPath("/", virtualPath);
53+
}
54+
2955
public static Cache Cache => HttpRuntime.Cache;
3056
}

src/Microsoft.AspNetCore.SystemWebAdapters/Hosting/MapPathUtility.cs

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -28,10 +28,19 @@ public string MapPath(string requestPath, string? path)
2828
return rootPath;
2929
}
3030

31-
return Path.Combine(
31+
var hasTrailingSlash = !string.IsNullOrEmpty(path) && (path.EndsWith('/') || path.EndsWith('\\'));
32+
33+
var combined = Path.Combine(
3234
rootPath,
3335
appPath[1..]
34-
.Replace('/', Path.DirectorySeparatorChar))
35-
.TrimEnd(Path.DirectorySeparatorChar);
36+
.Replace('/', Path.DirectorySeparatorChar));
37+
38+
// mirror the input to include or exclude a trailing slash.
39+
if (hasTrailingSlash && !combined.EndsWith(Path.DirectorySeparatorChar))
40+
combined += Path.DirectorySeparatorChar;
41+
else if (!hasTrailingSlash && combined.EndsWith(Path.DirectorySeparatorChar))
42+
combined = combined.TrimEnd(Path.DirectorySeparatorChar);
43+
44+
return combined;
3645
}
3746
}

src/Microsoft.AspNetCore.SystemWebAdapters/Utilities/UrlPath.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -107,7 +107,7 @@ internal static string GetDirectory(string path)
107107
private static bool IsDirectorySeparatorChar(char ch) => ch == '\\' || ch == '/';
108108

109109
// e.g \\server\share\foo or //server/share/foo
110-
private static bool IsUncSharePath(string path) => path.Length > 2 && IsDirectorySeparatorChar(path[0]) && IsDirectorySeparatorChar(path[1]);
110+
internal static bool IsUncSharePath(string path) => path.Length > 2 && IsDirectorySeparatorChar(path[0]) && IsDirectorySeparatorChar(path[1]);
111111

112112
private static bool IsAbsolutePhysicalPath(string path)
113113
{
Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
4+
using System;
5+
using System.IO;
6+
using System.Runtime.InteropServices;
7+
using System.Text;
8+
using System.Web;
9+
using System.Web.Hosting;
10+
using Microsoft.AspNetCore.Hosting;
11+
using Microsoft.AspNetCore.Http;
12+
using Microsoft.Extensions.DependencyInjection;
13+
using Microsoft.Extensions.DependencyInjection.Extensions;
14+
using Microsoft.Extensions.Options;
15+
using Moq;
16+
using Xunit;
17+
18+
namespace Microsoft.AspNetCore.SystemWebAdapters;
19+
20+
public class HostingEnvironmentTests
21+
{
22+
[InlineData("/DownOneLevel/DownLevelPage.aspx", "DownOneLevel\\DownLevelPage.aspx")]
23+
[InlineData("~/MyUploadedFiles", "MyUploadedFiles")]
24+
[InlineData("/UploadedFiles", "UploadedFiles")]
25+
[InlineData("/NotRealFolder", "NotRealFolder")]
26+
[InlineData("/TrailingSlash/", "TrailingSlash\\")]
27+
[InlineData("\\TrailingSlash2\\", "TrailingSlash2\\")]
28+
[InlineData("\\\\SomeServer\\Share\\Path", "SomeServer\\Share\\Path")]
29+
[InlineData("//SomeServer/Share/Path", "SomeServer\\Share\\Path")]
30+
[InlineData("~/", "\\")]
31+
[InlineData("/", "\\")]
32+
[Theory]
33+
public void MapPath(string? virtualPath, string expectedRelativePath)
34+
{
35+
// Arrange
36+
var options = new SystemWebAdaptersOptions
37+
{
38+
AppDomainAppVirtualPath = "/",
39+
AppDomainAppPath = RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? @"C:\ExampleSites\TestMapPath" : "/apps/test-map-path"
40+
};
41+
var ioptions = Options.Create(options);
42+
var utility = new MapPathUtility(ioptions, new(ioptions));
43+
44+
var services = new ServiceCollection();
45+
services.AddSingleton<IMapPathUtility>(utility);
46+
var serviceProvider = services.BuildServiceProvider();
47+
48+
string result;
49+
50+
// using ensures Dispose() runs which clears the static "Current" property.
51+
using (var hostingEnvironmentAccessor = new HostingEnvironmentAccessor(serviceProvider, ioptions))
52+
{
53+
// Act
54+
result = HostingEnvironment.MapPath(virtualPath);
55+
}
56+
57+
// for Linux/MacOS
58+
expectedRelativePath = (expectedRelativePath ?? string.Empty).Replace('\\', Path.DirectorySeparatorChar);
59+
60+
var expected = Path.Join(options.AppDomainAppPath, expectedRelativePath);
61+
62+
// Assert
63+
Assert.Equal(expected, result);
64+
}
65+
66+
[InlineData("../OutsideApplication", typeof(ArgumentException))]
67+
[InlineData("C:\\OutsideApplication", typeof(ArgumentException))]
68+
[InlineData("../RootLevelPage.aspx", typeof(ArgumentException))]
69+
[InlineData("/../OutsideApplication.aspx", typeof(HttpException))]
70+
[InlineData("File", typeof(ArgumentException))]
71+
[InlineData("", typeof(ArgumentNullException))]
72+
[InlineData(null, typeof(ArgumentNullException))]
73+
[Theory]
74+
public void MapPathException(string? virtualPath, Type expected)
75+
{
76+
// Arrange
77+
var options = new SystemWebAdaptersOptions
78+
{
79+
AppDomainAppVirtualPath = "/",
80+
AppDomainAppPath = RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? @"C:\ExampleSites\TestMapPath" : "/apps/test-map-path"
81+
};
82+
var ioptions = Options.Create(options);
83+
var utility = new MapPathUtility(ioptions, new(ioptions));
84+
85+
var services = new ServiceCollection();
86+
services.AddSingleton<IMapPathUtility>(utility);
87+
var serviceProvider = services.BuildServiceProvider();
88+
89+
// using ensures Dispose() runs which clears the static "Current" property.
90+
using (var hostingEnvironmentAccessor = new HostingEnvironmentAccessor(serviceProvider, ioptions))
91+
{
92+
// Assert
93+
Assert.Throws(expected, () => HostingEnvironment.MapPath(virtualPath));
94+
}
95+
}
96+
}

test/Microsoft.AspNetCore.SystemWebAdapters.Tests/HttpServerUtilityTests.cs

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
// The .NET Foundation licenses this file to you under the MIT license.
33

44
using System;
5+
using System.IO;
56
using System.Runtime.InteropServices;
67
using System.Text;
78
using System.Web;
@@ -85,6 +86,10 @@ public void UrlTokenRoundtrip(string input, string expected)
8586
[InlineData("/api/test/request/info", "/UploadedFiles", "UploadedFiles")]
8687
[InlineData("/api/test/request/info", "UploadedFiles", "api", "test", "request", "UploadedFiles")]
8788
[InlineData("/api/test/request/info", "~/MyUploadedFiles", "MyUploadedFiles")]
89+
[InlineData("/api/test/request/info", "~/", "\\")]
90+
[InlineData("/api/test/request/info", "/", "\\")]
91+
[InlineData("/api/test/request/info", "~/TrailingSlash/", "TrailingSlash\\")]
92+
[InlineData("/api/test/request/info", "path/file", "api", "test", "request", "path", "file")]
8893
[Theory]
8994
public void MapPath(string page, string? path, params string[] segments)
9095
{
@@ -101,8 +106,12 @@ public void MapPath(string page, string? path, params string[] segments)
101106
// Act
102107
var result = utility.MapPath(page, path);
103108

104-
var relative = System.IO.Path.Combine(segments);
105-
var expected = System.IO.Path.Combine(options.AppDomainAppPath, relative);
109+
var relative = Path.Join(segments);
110+
111+
// for Linux/MacOS
112+
relative = relative.Replace('\\', Path.DirectorySeparatorChar);
113+
114+
var expected = Path.Join(options.AppDomainAppPath, relative);
106115

107116
// Assert
108117
Assert.Equal(expected, result);

0 commit comments

Comments
 (0)