From f8035d6b04b94e579818e9b072ffcc4a95e5ea77 Mon Sep 17 00:00:00 2001 From: jacalvar Date: Wed, 8 Oct 2014 17:15:55 -0700 Subject: [PATCH] [Fixes #1201] Handle virtual paths in FilePathResult --- .../ActionResults/FilePathResult.cs | 151 +++++++- .../Properties/Resources.Designer.cs | 12 +- src/Microsoft.AspNet.Mvc.Core/Resources.resx | 4 +- src/Microsoft.AspNet.Mvc.Core/project.json | 2 +- .../ActionResults/FilePathResultTest.cs | 337 +++++++++++++++++- .../TestFiles/SubFolder/SubFolderTestFile.txt | 1 + test/WebSites/FilesWebSite/project.json | 3 +- 7 files changed, 490 insertions(+), 20 deletions(-) create mode 100644 test/Microsoft.AspNet.Mvc.Core.Test/TestFiles/SubFolder/SubFolderTestFile.txt 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" : "." }