diff --git a/src/Microsoft.AspNet.Mvc.Core/ActionResults/FilePathResult.cs b/src/Microsoft.AspNet.Mvc.Core/ActionResults/FilePathResult.cs
index 592c6e5fca..7eb3e77046 100644
--- a/src/Microsoft.AspNet.Mvc.Core/ActionResults/FilePathResult.cs
+++ b/src/Microsoft.AspNet.Mvc.Core/ActionResults/FilePathResult.cs
@@ -5,9 +5,12 @@ using System;
using System.IO;
using System.Threading;
using System.Threading.Tasks;
+using Microsoft.AspNet.FileSystems;
+using Microsoft.AspNet.Hosting;
using Microsoft.AspNet.Http;
using Microsoft.AspNet.HttpFeature;
using Microsoft.AspNet.Mvc.Core;
+using Microsoft.Framework.DependencyInjection;
namespace Microsoft.AspNet.Mvc
{
@@ -31,38 +34,170 @@ namespace Microsoft.AspNet.Mvc
public FilePathResult([NotNull] string fileName, [NotNull] string contentType)
: base(contentType)
{
- if (!Path.IsPathRooted(fileName))
- {
- var message = Resources.FormatFileResult_InvalidPathType_RelativeOrVirtualPath(fileName);
- throw new ArgumentException(message, "fileName");
- }
-
FileName = fileName;
}
+ ///
+ /// Creates a new instance with
+ /// the provided and the
+ /// provided .
+ ///
+ /// The path to the file. The path must be an absolute
+ /// path. Relative and virtual paths are not supported.
+ /// The Content-Type header of the response.
+ public FilePathResult(
+ [NotNull] string fileName,
+ [NotNull] string contentType,
+ [NotNull] IFileSystem fileSystem)
+ : base(contentType)
+ {
+ FileName = fileName;
+ FileSystem = fileSystem;
+ }
+
///
/// Gets the path to the file that will be sent back as the response.
///
public string FileName { get; private set; }
+ ///
+ /// Gets the used to resolve paths.
+ ///
+ public IFileSystem FileSystem { get; private set; }
+
///
protected override Task WriteFileAsync(HttpResponse response, CancellationToken cancellation)
{
var sendFile = response.HttpContext.GetFeature();
+
+ var fileSystem = GetFileSystem(response.HttpContext.RequestServices);
+
+ var filePath = ResolveFilePath(fileSystem);
+
if (sendFile != null)
{
return sendFile.SendFileAsync(
- FileName,
+ filePath,
offset: 0,
length: null,
cancellation: cancellation);
}
else
{
- return CopyStreamToResponse(FileName, response, cancellation);
+ return CopyStreamToResponse(filePath, response, cancellation);
}
}
+ internal string ResolveFilePath(IFileSystem fileSystem)
+ {
+ // Let the file system try to get the file and if it can't,
+ // fallback to trying the path directly unless the path starts with '/'.
+ // In that case we consider it relative and won't try to resolve it as
+ // a full path
+
+ var path = NormalizePath(FileName);
+
+ if (IsPathRooted(path))
+ {
+ // The path is absolute
+ // C:\...\file.ext
+ // C:/.../file.ext
+ return path;
+ }
+
+ IFileInfo fileInfo = null;
+ if (fileSystem.TryGetFileInfo(path, out fileInfo))
+ {
+ // The path is relative and IFileSystem found the file, so return the full
+ // path.
+ return fileInfo.PhysicalPath;
+ }
+
+ // We are absolutely sure the path is relative, and couldn't find the file
+ // on the file system.
+ var message = Resources.FormatFileResult_InvalidPath(path);
+ throw new FileNotFoundException(message, path);
+ }
+
+ // Internal for unit testing purposes only
+ ///
+ /// Creates a normalized representation of the given . The default
+ /// implementation doesn't support files with '\' in the file name and treats the '\' as
+ /// a directory separator. The default implementation will convert all the '\' into '/'
+ /// and will remove leading '~' characters.
+ ///
+ /// The path to normalize.
+ /// The normalized path.
+ protected internal virtual string NormalizePath([NotNull] string path)
+ {
+ // Unix systems support '\' as part of the file name. So '\' is not
+ // a valid directory separator in those systems. Here we make the conscious
+ // choice of replacing '\' for '/' which means that file names with '\' will
+ // not be supported.
+
+ if (path.StartsWith("~/", StringComparison.Ordinal))
+ {
+ // We don't support virtual paths for now, so we just treat them as relative
+ // paths.
+ return path.Substring(1).Replace('\\', '/');
+ }
+
+ if (path.StartsWith("~\\", StringComparison.Ordinal))
+ {
+ // ~\ is not a valid virtual path, and we don't want to replace '\' with '/' as it
+ // ofuscates the error, so just return the original path and throw at a later point
+ // when we can't find the file.
+ return path;
+ }
+
+ return path.Replace('\\', '/');
+ }
+
+ // Internal for unit testing purposes only
+ ///
+ /// Determines if the provided path is absolute or relative. The default implementation considers
+ /// paths starting with '/' to be relative.
+ ///
+ /// The path to examine.
+ /// True if the path is absolute.
+ protected internal virtual bool IsPathRooted([NotNull] string path)
+ {
+ // We consider paths to be rooted if they start with '<>:' and do
+ // not start with '\' or '/'. In those cases, even that the paths are 'traditionally'
+ // rooted, we consider them to be relative.
+ // In Unix rooted paths start with '/' which is not supported by this action result
+ // by default.
+
+ return Path.IsPathRooted(path) && (IsNetworkPath(path) || !StartsWithForwardOrBackSlash(path));
+ }
+
+ private static bool StartsWithForwardOrBackSlash(string path)
+ {
+ return path.StartsWith("/", StringComparison.Ordinal) ||
+ path.StartsWith("\\", StringComparison.Ordinal);
+ }
+
+ private static bool IsNetworkPath(string path)
+ {
+ return path.StartsWith("//", StringComparison.Ordinal) ||
+ path.StartsWith("\\\\", StringComparison.Ordinal);
+ }
+
+ private IFileSystem GetFileSystem(IServiceProvider requestServices)
+ {
+ if (FileSystem != null)
+ {
+ return FileSystem;
+ }
+
+ // For right now until we can use IWebRootFileSystemProvider, see
+ // https://github.com/aspnet/Hosting/issues/86 for details.
+ var hostingEnvironment = requestServices.GetService();
+ FileSystem = new PhysicalFileSystem(hostingEnvironment.WebRoot);
+
+ return FileSystem;
+ }
+
private static async Task CopyStreamToResponse(
string fileName,
HttpResponse response,
diff --git a/src/Microsoft.AspNet.Mvc.Core/Properties/Resources.Designer.cs b/src/Microsoft.AspNet.Mvc.Core/Properties/Resources.Designer.cs
index c5c0401072..3ac0ec9de0 100644
--- a/src/Microsoft.AspNet.Mvc.Core/Properties/Resources.Designer.cs
+++ b/src/Microsoft.AspNet.Mvc.Core/Properties/Resources.Designer.cs
@@ -1515,19 +1515,19 @@ namespace Microsoft.AspNet.Mvc.Core
}
///
- /// "The path to the file must be absolute: {0}"
+ /// Could not find file: {0}
///
- internal static string FileResult_InvalidPathType_RelativeOrVirtualPath
+ internal static string FileResult_InvalidPath
{
- get { return GetString("FileResult_InvalidPathType_RelativeOrVirtualPath"); }
+ get { return GetString("FileResult_InvalidPath"); }
}
///
- /// "The path to the file must be absolute: {0}"
+ /// Could not find file: {0}
///
- internal static string FormatFileResult_InvalidPathType_RelativeOrVirtualPath(object p0)
+ internal static string FormatFileResult_InvalidPath(object p0)
{
- return string.Format(CultureInfo.CurrentCulture, GetString("FileResult_InvalidPathType_RelativeOrVirtualPath"), p0);
+ return string.Format(CultureInfo.CurrentCulture, GetString("FileResult_InvalidPath"), p0);
}
private static string GetString(string name, params string[] formatterNames)
diff --git a/src/Microsoft.AspNet.Mvc.Core/Resources.resx b/src/Microsoft.AspNet.Mvc.Core/Resources.resx
index 14ff825342..cb633c1cca 100644
--- a/src/Microsoft.AspNet.Mvc.Core/Resources.resx
+++ b/src/Microsoft.AspNet.Mvc.Core/Resources.resx
@@ -409,8 +409,8 @@
Multiple actions matched. The following actions matched route data and had all constraints satisfied:{0}{0}{1}
0 is the newline - 1 is a newline separate list of action display names
-
- "The path to the file must be absolute: {0}"
+
+ Could not find file: {0}
{0} is the value for the provided path
\ No newline at end of file
diff --git a/src/Microsoft.AspNet.Mvc.Core/project.json b/src/Microsoft.AspNet.Mvc.Core/project.json
index 7920738b2f..1b616101be 100644
--- a/src/Microsoft.AspNet.Mvc.Core/project.json
+++ b/src/Microsoft.AspNet.Mvc.Core/project.json
@@ -5,7 +5,7 @@
},
"dependencies": {
"Microsoft.AspNet.FileSystems": "1.0.0-*",
- "Microsoft.AspNet.Http": "1.0.0-*",
+ "Microsoft.AspNet.Hosting": "1.0.0-*",
"Microsoft.AspNet.Mvc.HeaderValueAbstractions": "1.0.0-*",
"Microsoft.AspNet.Mvc.Common": { "version": "6.0.0-*", "type": "build" },
"Microsoft.AspNet.Mvc.ModelBinding": "6.0.0-*",
diff --git a/test/Microsoft.AspNet.Mvc.Core.Test/ActionResults/FilePathResultTest.cs b/test/Microsoft.AspNet.Mvc.Core.Test/ActionResults/FilePathResultTest.cs
index e39866680d..d0b4816162 100644
--- a/test/Microsoft.AspNet.Mvc.Core.Test/ActionResults/FilePathResultTest.cs
+++ b/test/Microsoft.AspNet.Mvc.Core.Test/ActionResults/FilePathResultTest.cs
@@ -1,12 +1,18 @@
// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
+using System;
using System.IO;
using System.Threading;
using System.Threading.Tasks;
+using Microsoft.AspNet.Hosting;
using Microsoft.AspNet.HttpFeature;
+using Microsoft.AspNet.FileSystems;
using Microsoft.AspNet.PipelineCore;
using Microsoft.AspNet.Routing;
+using Microsoft.Framework.DependencyInjection;
+using Microsoft.Framework.DependencyInjection.Fallback;
+using Microsoft.Framework.Runtime;
using Moq;
using Xunit;
@@ -30,7 +36,8 @@ namespace Microsoft.AspNet.Mvc
{
// Arrange
var path = Path.GetFullPath(Path.Combine("TestFiles", "FilePathResultTestFile.txt"));
- var result = new FilePathResult(path, "text/plain");
+ var fileSystem = new PhysicalFileSystem(Path.GetFullPath("."));
+ var result = new FilePathResult(path, "text/plain", fileSystem);
var httpContext = new DefaultHttpContext();
httpContext.Response.Body = new MemoryStream();
@@ -47,12 +54,42 @@ namespace Microsoft.AspNet.Mvc
Assert.Equal("FilePathResultTestFile contents", contents);
}
+ [Fact]
+ public async Task ExecuteResultAsync_FallsBackToThePhysicalFileSystem_IfNoFileSystemIsPresent()
+ {
+ // Arrange
+ var path = Path.Combine("TestFiles", "FilePathResultTestFile.txt");
+ var result = new FilePathResult(path, "text/plain");
+
+ var appEnvironment = new Mock();
+ appEnvironment.Setup(app => app.WebRoot)
+ .Returns(Directory.GetCurrentDirectory());
+
+ var httpContext = new DefaultHttpContext();
+ httpContext.Response.Body = new MemoryStream();
+ httpContext.RequestServices = new ServiceCollection()
+ .AddInstance(appEnvironment.Object)
+ .BuildServiceProvider();
+
+ var context = new ActionContext(httpContext, new RouteData(), new ActionDescriptor());
+
+ // Act
+ await result.ExecuteResultAsync(context);
+ httpContext.Response.Body.Position = 0;
+
+ // Assert
+ Assert.NotNull(httpContext.Response.Body);
+ var contents = await new StreamReader(httpContext.Response.Body).ReadToEndAsync();
+ Assert.Equal("FilePathResultTestFile contents", contents);
+ }
+
[Fact]
public async Task ExecuteResultAsync_CallsSendFileAsync_IfIHttpSendFilePresent()
{
// Arrange
var path = Path.GetFullPath(Path.Combine("TestFiles", "FilePathResultTestFile.txt"));
- var result = new FilePathResult(path, "text/plain");
+ var fileSystem = new PhysicalFileSystem(Path.GetFullPath("."));
+ var result = new FilePathResult(path, "text/plain", fileSystem);
var sendFileMock = new Mock();
sendFileMock
@@ -70,5 +107,301 @@ namespace Microsoft.AspNet.Mvc
// Assert
sendFileMock.Verify();
}
+
+ [Fact]
+ public async Task ExecuteResultAsync_WorksWithAbsolutePaths_UsingBackSlash()
+ {
+ // Arrange
+ // path will be C:\...\TestFiles\FilePathResultTestFile.txt
+ var path = Path.GetFullPath(Path.Combine(".", "TestFiles", "FilePathResultTestFile.txt"));
+ // We want ot ensure that the path that we provide has backslashes to ensure they get normalized into
+ // forward slashes.
+ path = path.Replace('/', '\\');
+
+ // Point the FileSystemRoot to a subfolder
+ var fileSystem = new PhysicalFileSystem(Path.GetFullPath("Utils"));
+ var result = new FilePathResult(path, "text/plain", fileSystem);
+
+ var httpContext = new DefaultHttpContext();
+ httpContext.Response.Body = new MemoryStream();
+
+ var context = new ActionContext(httpContext, new RouteData(), new ActionDescriptor());
+
+ // Act
+ await result.ExecuteResultAsync(context);
+ httpContext.Response.Body.Position = 0;
+
+ // Assert
+ Assert.NotNull(httpContext.Response.Body);
+ var contents = await new StreamReader(httpContext.Response.Body).ReadToEndAsync();
+ Assert.Equal("FilePathResultTestFile contents", contents);
+ }
+
+ [Fact]
+ public async Task ExecuteResultAsync_WorksWithAbsolutePaths_UsingForwardSlash()
+ {
+ // Arrange
+ // path will be C:/.../TestFiles/FilePathResultTestFile.txt
+ var path = Path.GetFullPath(Path.Combine(".", "TestFiles", "FilePathResultTestFile.txt"));
+ path = path.Replace(@"\", "/");
+
+ // Point the FileSystemRoot to a subfolder
+ var fileSystem = new PhysicalFileSystem(Path.GetFullPath("Utils"));
+ var result = new FilePathResult(path, "text/plain", fileSystem);
+
+ var httpContext = new DefaultHttpContext();
+ httpContext.Response.Body = new MemoryStream();
+
+ var context = new ActionContext(httpContext, new RouteData(), new ActionDescriptor());
+
+ // Act
+ await result.ExecuteResultAsync(context);
+ httpContext.Response.Body.Position = 0;
+
+ // Assert
+ Assert.NotNull(httpContext.Response.Body);
+ var contents = await new StreamReader(httpContext.Response.Body).ReadToEndAsync();
+ Assert.Equal("FilePathResultTestFile contents", contents);
+ }
+
+ [Theory]
+ // Root of the file system, forward slash and back slash
+ [InlineData("FilePathResultTestFile.txt", "TestFiles/FilePathResultTestFile.txt")]
+ [InlineData("/FilePathResultTestFile.txt", "TestFiles/FilePathResultTestFile.txt")]
+ [InlineData("\\FilePathResultTestFile.txt", "TestFiles/FilePathResultTestFile.txt")]
+ // Paths with subfolders and mixed slash kinds
+ [InlineData("/SubFolder/SubFolderTestFile.txt", "TestFiles/SubFolder/SubFolderTestFile.txt")]
+ [InlineData("\\SubFolder\\SubFolderTestFile.txt", "TestFiles/SubFolder/SubFolderTestFile.txt")]
+ [InlineData("/SubFolder\\SubFolderTestFile.txt", "TestFiles/SubFolder/SubFolderTestFile.txt")]
+ [InlineData("\\SubFolder/SubFolderTestFile.txt", "TestFiles/SubFolder/SubFolderTestFile.txt")]
+ // '.' has no special meaning
+ [InlineData("./FilePathResultTestFile.txt", "TestFiles/FilePathResultTestFile.txt")]
+ [InlineData(".\\FilePathResultTestFile.txt", "TestFiles/FilePathResultTestFile.txt")]
+ [InlineData("./SubFolder/SubFolderTestFile.txt", "TestFiles/SubFolder/SubFolderTestFile.txt")]
+ [InlineData(".\\SubFolder\\SubFolderTestFile.txt", "TestFiles/SubFolder/SubFolderTestFile.txt")]
+ [InlineData("./SubFolder\\SubFolderTestFile.txt", "TestFiles/SubFolder/SubFolderTestFile.txt")]
+ [InlineData(".\\SubFolder/SubFolderTestFile.txt", "TestFiles/SubFolder/SubFolderTestFile.txt")]
+ // Traverse to the parent directory and back to the file system directory
+ [InlineData("..\\TestFiles/FilePathResultTestFile.txt", "TestFiles/FilePathResultTestFile.txt")]
+ [InlineData("..\\TestFiles\\FilePathResultTestFile.txt", "TestFiles/FilePathResultTestFile.txt")]
+ [InlineData("..\\TestFiles/SubFolder/SubFolderTestFile.txt", "TestFiles/SubFolder/SubFolderTestFile.txt")]
+ [InlineData("..\\TestFiles\\SubFolder\\SubFolderTestFile.txt", "TestFiles/SubFolder/SubFolderTestFile.txt")]
+ [InlineData("..\\TestFiles/SubFolder\\SubFolderTestFile.txt", "TestFiles/SubFolder/SubFolderTestFile.txt")]
+ [InlineData("..\\TestFiles\\SubFolder/SubFolderTestFile.txt", "TestFiles/SubFolder/SubFolderTestFile.txt")]
+ // '~/' and '~\' mean the application root folder
+ [InlineData("~/FilePathResultTestFile.txt", "TestFiles/FilePathResultTestFile.txt")]
+ [InlineData("~/SubFolder/SubFolderTestFile.txt", "TestFiles/SubFolder/SubFolderTestFile.txt")]
+ [InlineData("~/SubFolder\\SubFolderTestFile.txt", "TestFiles/SubFolder/SubFolderTestFile.txt")]
+ public void GetFilePath_Resolves_RelativePaths(string path, string relativePathToFile)
+ {
+ // Arrange
+ var fileSystem = new PhysicalFileSystem(Path.GetFullPath("./TestFiles"));
+ var expectedPath = Path.GetFullPath(Path.Combine(Directory.GetCurrentDirectory(), relativePathToFile));
+ var filePathResult = new FilePathResult(path, "text/plain", fileSystem);
+
+ // Act
+ var result = filePathResult.ResolveFilePath(fileSystem);
+
+ // Assert
+ Assert.Equal(expectedPath, result);
+ }
+
+ [Theory]
+ [InlineData("~\\FilePathResultTestFile.txt", "TestFiles/FilePathResultTestFile.txt")]
+ [InlineData("~\\SubFolder\\SubFolderTestFile.txt", "TestFiles/SubFolder/SubFolderTestFile.txt")]
+ [InlineData("~\\SubFolder/SubFolderTestFile.txt", "TestFiles/SubFolder/SubFolderTestFile.txt")]
+ public void GetFilePath_FailsToResolve_InvalidVirtualPaths(string path, string relativePathToFile)
+ {
+ // Arrange
+ var fileSystem = new PhysicalFileSystem(Path.GetFullPath("./TestFiles"));
+ var expectedPath = Path.GetFullPath(Path.Combine(Directory.GetCurrentDirectory(), relativePathToFile));
+ var filePathResult = new FilePathResult(path, "text/plain", fileSystem);
+
+ // Act
+ var ex = Assert.Throws(() => filePathResult.ResolveFilePath(fileSystem));
+
+ // Assert
+ Assert.Equal("Could not find file: " + path, ex.Message);
+ Assert.Equal(path, ex.FileName);
+ }
+
+ [Theory]
+ // Root of the file system, forward slash and back slash
+ [InlineData("FilePathResultTestFile.txt")]
+ [InlineData("/FilePathResultTestFile.txt")]
+ [InlineData("\\FilePathResultTestFile.txt")]
+ // Paths with subfolders and mixed slash kinds
+ [InlineData("/SubFolder/SubFolderTestFile.txt")]
+ [InlineData("\\SubFolder\\SubFolderTestFile.txt")]
+ [InlineData("/SubFolder\\SubFolderTestFile.txt")]
+ [InlineData("\\SubFolder/SubFolderTestFile.txt")]
+ // '.' has no special meaning
+ [InlineData("./FilePathResultTestFile.txt")]
+ [InlineData(".\\FilePathResultTestFile.txt")]
+ [InlineData("./SubFolder/SubFolderTestFile.txt")]
+ [InlineData(".\\SubFolder\\SubFolderTestFile.txt")]
+ [InlineData("./SubFolder\\SubFolderTestFile.txt")]
+ [InlineData(".\\SubFolder/SubFolderTestFile.txt")]
+ // Traverse to the parent directory and back to the file system directory
+ [InlineData("..\\TestFiles/FilePathResultTestFile.txt")]
+ [InlineData("..\\TestFiles\\FilePathResultTestFile.txt")]
+ [InlineData("..\\TestFiles/SubFolder/SubFolderTestFile.txt")]
+ [InlineData("..\\TestFiles\\SubFolder\\SubFolderTestFile.txt")]
+ [InlineData("..\\TestFiles/SubFolder\\SubFolderTestFile.txt")]
+ [InlineData("..\\TestFiles\\SubFolder/SubFolderTestFile.txt")]
+ // '~/' and '~\' mean the application root folder
+ [InlineData("~/FilePathResultTestFile.txt")]
+ [InlineData("~/SubFolder/SubFolderTestFile.txt")]
+ [InlineData("~/SubFolder\\SubFolderTestFile.txt")]
+ public void GetFilePath_ThrowsFileNotFound_IfItCanNotFindTheFile(string path)
+ {
+ // Arrange
+
+ // Point the IFileSystem root to a different subfolder
+ var fileSystem = new PhysicalFileSystem(Path.GetFullPath("./Utils"));
+ var filePathResult = new FilePathResult(path, "text/plain", fileSystem);
+ var expectedFileName = path.TrimStart('~').Replace('\\', '/');
+ var expectedMessage = "Could not find file: " + expectedFileName;
+
+ // Act
+ var ex = Assert.Throws(() => filePathResult.ResolveFilePath(fileSystem));
+
+ // Assert
+ Assert.Equal(expectedMessage, ex.Message);
+ Assert.Equal(expectedFileName, ex.FileName);
+ }
+
+ [Theory]
+ [InlineData("FilePathResultTestFile.txt", "FilePathResultTestFile.txt")]
+ [InlineData("/FilePathResultTestFile.txt", "/FilePathResultTestFile.txt")]
+ [InlineData("\\FilePathResultTestFile.txt", "/FilePathResultTestFile.txt")]
+ // Paths with subfolders and mixed slash kinds
+ [InlineData("/SubFolder/SubFolderTestFile.txt", "/SubFolder/SubFolderTestFile.txt")]
+ [InlineData("\\SubFolder\\SubFolderTestFile.txt", "/SubFolder/SubFolderTestFile.txt")]
+ [InlineData("/SubFolder\\SubFolderTestFile.txt", "/SubFolder/SubFolderTestFile.txt")]
+ [InlineData("\\SubFolder/SubFolderTestFile.txt", "/SubFolder/SubFolderTestFile.txt")]
+ // '.' has no special meaning
+ [InlineData("./FilePathResultTestFile.txt", "./FilePathResultTestFile.txt")]
+ [InlineData(".\\FilePathResultTestFile.txt", "./FilePathResultTestFile.txt")]
+ [InlineData("./SubFolder/SubFolderTestFile.txt", "./SubFolder/SubFolderTestFile.txt")]
+ [InlineData(".\\SubFolder\\SubFolderTestFile.txt", "./SubFolder/SubFolderTestFile.txt")]
+ [InlineData("./SubFolder\\SubFolderTestFile.txt", "./SubFolder/SubFolderTestFile.txt")]
+ [InlineData(".\\SubFolder/SubFolderTestFile.txt", "./SubFolder/SubFolderTestFile.txt")]
+ // Traverse to the parent directory and back to the file system directory
+ [InlineData("..\\TestFiles/FilePathResultTestFile.txt", "../TestFiles/FilePathResultTestFile.txt")]
+ [InlineData("..\\TestFiles\\FilePathResultTestFile.txt", "../TestFiles/FilePathResultTestFile.txt")]
+ [InlineData("..\\TestFiles/SubFolder/SubFolderTestFile.txt", "../TestFiles/SubFolder/SubFolderTestFile.txt")]
+ [InlineData("..\\TestFiles\\SubFolder\\SubFolderTestFile.txt", "../TestFiles/SubFolder/SubFolderTestFile.txt")]
+ [InlineData("..\\TestFiles/SubFolder\\SubFolderTestFile.txt", "../TestFiles/SubFolder/SubFolderTestFile.txt")]
+ [InlineData("..\\TestFiles\\SubFolder/SubFolderTestFile.txt", "../TestFiles/SubFolder/SubFolderTestFile.txt")]
+ // Absolute paths
+ [InlineData("C:\\Folder\\SubFolder\\File.txt", "C:/Folder/SubFolder/File.txt")]
+ [InlineData("C:/Folder/SubFolder/File.txt", "C:/Folder/SubFolder/File.txt")]
+ [InlineData("\\\\NetworkLocation\\Folder\\SubFolder\\File.txt", "//NetworkLocation/Folder/SubFolder/File.txt")]
+ [InlineData("//NetworkLocation/Folder/SubFolder/File.txt", "//NetworkLocation/Folder/SubFolder/File.txt")]
+ public void NormalizePath_ConvertsBackSlashes_IntoForwardSlashes(string path, string expectedPath)
+ {
+ // Arrange
+ var fileResult = new FilePathResult(path, "text/plain", Mock.Of());
+
+ // Act
+ var normalizedPath = fileResult.NormalizePath(path);
+
+ // Assert
+ Assert.Equal(expectedPath, normalizedPath);
+ }
+
+ [Theory]
+ // '~/' and '~\' mean the application root folder
+ [InlineData("~/FilePathResultTestFile.txt", "/FilePathResultTestFile.txt")]
+ [InlineData("~/SubFolder/SubFolderTestFile.txt", "/SubFolder/SubFolderTestFile.txt")]
+ [InlineData("~/SubFolder\\SubFolderTestFile.txt", "/SubFolder/SubFolderTestFile.txt")]
+ public void NormalizePath_ConvertsVirtualPaths_IntoRelativePaths(string path, string expectedPath)
+ {
+ // Arrange
+ var fileResult = new FilePathResult(path, "text/plain", Mock.Of());
+
+ // Act
+ var normalizedPath = fileResult.NormalizePath(path);
+
+ // Assert
+ Assert.Equal(expectedPath, normalizedPath);
+ }
+
+ [Theory]
+ // '~/' and '~\' mean the application root folder
+ [InlineData("~\\FilePathResultTestFile.txt")]
+ [InlineData("~\\SubFolder\\SubFolderTestFile.txt")]
+ [InlineData("~\\SubFolder/SubFolderTestFile.txt")]
+ public void NormalizePath_DoesNotConvert_InvalidVirtualPathsIntoRelativePaths(string path)
+ {
+ // Arrange
+ var fileResult = new FilePathResult(path, "text/plain", Mock.Of());
+
+ // Act
+ var normalizedPath = fileResult.NormalizePath(path);
+
+ // Assert
+ Assert.Equal(path, normalizedPath);
+ }
+
+ [Theory]
+ [InlineData("C:\\Folder\\SubFolder\\File.txt")]
+ [InlineData("C:/Folder/SubFolder/File.txt")]
+ [InlineData("\\\\NetworkLocation\\Folder\\SubFolder\\File.txt")]
+ [InlineData("//NetworkLocation/Folder/SubFolder/File.txt")]
+ public void IsPathRooted_ReturnsTrue_ForAbsolutePaths(string path)
+ {
+ // Arrange
+ var fileResult = new FilePathResult(path, "text/plain", Mock.Of());
+
+ // Act
+ var isRooted = fileResult.IsPathRooted(path);
+
+ // Assert
+ Assert.True(isRooted);
+ }
+
+ [Theory]
+ [InlineData("FilePathResultTestFile.txt")]
+ [InlineData("/FilePathResultTestFile.txt")]
+ [InlineData("\\FilePathResultTestFile.txt")]
+ // Paths with subfolders and mixed slash kinds
+ [InlineData("/SubFolder/SubFolderTestFile.txt")]
+ [InlineData("\\SubFolder\\SubFolderTestFile.txt")]
+ [InlineData("/SubFolder\\SubFolderTestFile.txt")]
+ [InlineData("\\SubFolder/SubFolderTestFile.txt")]
+ // '.' has no special meaning
+ [InlineData("./FilePathResultTestFile.txt")]
+ [InlineData(".\\FilePathResultTestFile.txt")]
+ [InlineData("./SubFolder/SubFolderTestFile.txt")]
+ [InlineData(".\\SubFolder\\SubFolderTestFile.txt")]
+ [InlineData("./SubFolder\\SubFolderTestFile.txt")]
+ [InlineData(".\\SubFolder/SubFolderTestFile.txt")]
+ // Traverse to the parent directory and back to the file system directory
+ [InlineData("..\\TestFiles/FilePathResultTestFile.txt")]
+ [InlineData("..\\TestFiles\\FilePathResultTestFile.txt")]
+ [InlineData("..\\TestFiles/SubFolder/SubFolderTestFile.txt")]
+ [InlineData("..\\TestFiles\\SubFolder\\SubFolderTestFile.txt")]
+ [InlineData("..\\TestFiles/SubFolder\\SubFolderTestFile.txt")]
+ [InlineData("..\\TestFiles\\SubFolder/SubFolderTestFile.txt")]
+ // '~/' and '~\' mean the application root folder
+ [InlineData("~/FilePathResultTestFile.txt")]
+ [InlineData("~\\FilePathResultTestFile.txt")]
+ [InlineData("~/SubFolder/SubFolderTestFile.txt")]
+ [InlineData("~\\SubFolder\\SubFolderTestFile.txt")]
+ [InlineData("~/SubFolder\\SubFolderTestFile.txt")]
+ [InlineData("~\\SubFolder/SubFolderTestFile.txt")]
+ public void IsPathRooted_ReturnsFalse_ForRelativePaths(string path)
+ {
+ // Arrange
+ var fileResult = new FilePathResult(path, "text/plain", Mock.Of());
+
+ // Act
+ var isRooted = fileResult.IsPathRooted(path);
+
+ // Assert
+ Assert.False(isRooted);
+ }
}
}
\ No newline at end of file
diff --git a/test/Microsoft.AspNet.Mvc.Core.Test/TestFiles/SubFolder/SubFolderTestFile.txt b/test/Microsoft.AspNet.Mvc.Core.Test/TestFiles/SubFolder/SubFolderTestFile.txt
new file mode 100644
index 0000000000..1ae10032bf
--- /dev/null
+++ b/test/Microsoft.AspNet.Mvc.Core.Test/TestFiles/SubFolder/SubFolderTestFile.txt
@@ -0,0 +1 @@
+FilePathResultTestFile contents
\ No newline at end of file
diff --git a/test/WebSites/FilesWebSite/project.json b/test/WebSites/FilesWebSite/project.json
index f1e1deac1f..b878714191 100644
--- a/test/WebSites/FilesWebSite/project.json
+++ b/test/WebSites/FilesWebSite/project.json
@@ -7,5 +7,6 @@
"frameworks": {
"aspnet50": { },
"aspnetcore50": { }
- }
+ },
+ "webroot" : "."
}