// Copyright (c) .NET Foundation. 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.Collections.Generic; using System.Diagnostics; using System.Text; using Microsoft.Extensions.Primitives; namespace Microsoft.AspNetCore.Mvc.Internal { public static class ViewEnginePath { public static readonly char[] PathSeparators = new[] { '/', '\\' }; private const string CurrentDirectoryToken = "."; private const string ParentDirectoryToken = ".."; public static string CombinePath(string first, string second) { Debug.Assert(!string.IsNullOrEmpty(first)); if (second.StartsWith("/", StringComparison.Ordinal)) { // "second" is already an app-rooted path. Return it as-is. return second; } string result; // Get directory name (including final slash) but do not use Path.GetDirectoryName() to preserve path // normalization. var index = first.LastIndexOf('/'); Debug.Assert(index >= 0); if (index == first.Length - 1) { // If the first ends in a trailing slash e.g. "/Home/", assume it's a directory. result = first + second; } else { result = first.Substring(0, index + 1) + second; } return ResolvePath(result); } public static string ResolvePath(string path) { Debug.Assert(!string.IsNullOrEmpty(path)); var pathSegment = new StringSegment(path); if (path[0] == PathSeparators[0] || path[0] == PathSeparators[1]) { // Leading slashes (e.g. "/Views/Index.cshtml") always generate an empty first token. Ignore these // for purposes of resolution. pathSegment = pathSegment.Subsegment(1); } var tokenizer = new StringTokenizer(pathSegment, PathSeparators); var requiresResolution = false; foreach (var segment in tokenizer) { // Determine if we need to do any path resolution. // We need to resovle paths with multiple path separators (e.g "//" or "\\") or, directory traversals e.g. ("../" or "./"). if (segment.Length == 0 || segment.Equals(ParentDirectoryToken, StringComparison.Ordinal) || segment.Equals(CurrentDirectoryToken, StringComparison.Ordinal)) { requiresResolution = true; break; } } if (!requiresResolution) { return path; } var pathSegments = new List(); foreach (var segment in tokenizer) { if (segment.Length == 0) { // Ignore multiple directory separators continue; } if (segment.Equals(ParentDirectoryToken, StringComparison.Ordinal)) { if (pathSegments.Count == 0) { // Don't resolve the path if we ever escape the file system root. We can't reason about it in a // consistent way. return path; } pathSegments.RemoveAt(pathSegments.Count - 1); } else if (segment.Equals(CurrentDirectoryToken, StringComparison.Ordinal)) { // We already have the current directory continue; } else { pathSegments.Add(segment); } } var builder = new StringBuilder(); for (var i = 0; i < pathSegments.Count; i++) { var segment = pathSegments[i]; builder.Append('/'); builder.Append(segment.Buffer, segment.Offset, segment.Length); } return builder.ToString(); } } }