| File: src\aspnetcore\src\Shared\PathNormalizer\PathNormalizer.cs | Web Access |
| Project: src\aspnetcore\src\Servers\HttpSys\src\Microsoft.AspNetCore.Server.HttpSys.csproj (Microsoft.AspNetCore.Server.HttpSys) |
// 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; using System.Runtime.InteropServices; namespace Microsoft.AspNetCore.Internal; internal static class PathNormalizer { private const byte ByteSlash = (byte)'/'; private const byte ByteDot = (byte)'.'; // In-place implementation of the algorithm from https://tools.ietf.org/html/rfc3986#section-5.2.4 public static int RemoveDotSegments(Span<byte> src) { Debug.Assert(src[0] == '/', "Path segment must always start with a '/'"); ReadOnlySpan<byte> dotSlash = "./"u8; ReadOnlySpan<byte> slashDot = "/."u8; var writtenLength = 0; var readPointer = 0; while (src.Length > readPointer) { var currentSrc = src[readPointer..]; var nextDotSegmentIndex = currentSrc.IndexOf(slashDot); if (nextDotSegmentIndex < 0 && readPointer == 0) { return src.Length; } if (nextDotSegmentIndex < 0) { // Copy the remaining src to dst, and return. currentSrc.CopyTo(src[writtenLength..]); writtenLength += src.Length - readPointer; return writtenLength; } else if (nextDotSegmentIndex > 0) { // Copy until the next segment excluding the trailer. currentSrc[..nextDotSegmentIndex].CopyTo(src[writtenLength..]); writtenLength += nextDotSegmentIndex; readPointer += nextDotSegmentIndex; } var remainingLength = src.Length - readPointer; // Case of /../ or /./ or non-dot segments. if (remainingLength > 3) { var nextIndex = readPointer + 2; if (src[nextIndex] == ByteSlash) { // Case: /./ readPointer = nextIndex; } else if (MemoryMarshal.CreateSpan(ref src[nextIndex], 2).StartsWith(dotSlash)) { // Case: /../ // Remove the last segment and replace the path with / var lastIndex = MemoryMarshal.CreateSpan(ref src[0], writtenLength).LastIndexOf(ByteSlash); // Move write pointer to the end of the previous segment without / or to start position writtenLength = int.Max(0, lastIndex); // Move the read pointer to the next segments beginning including / readPointer += 3; } else { // Not a dot segment e.g. /.a, copy the matched /. and the next character then bump the read pointer src.Slice(readPointer, 3).CopyTo(src[writtenLength..]); writtenLength += 3; readPointer = nextIndex + 1; } } // Ending with /.. or /./ or non-dot segments. else if (remainingLength == 3) { var nextIndex = readPointer + 2; if (src[nextIndex] == ByteSlash) { // Case: /./ Replace the /./ segment with a closing / src[writtenLength++] = ByteSlash; return writtenLength; } else if (src[nextIndex] == ByteDot) { // Case: /.. Remove the last segment and replace the path with / var lastSlashIndex = MemoryMarshal.CreateSpan(ref src[0], writtenLength).LastIndexOf(ByteSlash); // If this was the beginning of the string, then return / if (lastSlashIndex < 0) { Debug.Assert(src[0] == '/'); return 1; } else { writtenLength = lastSlashIndex + 1; } return writtenLength; } else { // Not a dot segment e.g. /.a, copy the remaining part. src[readPointer..].CopyTo(src[writtenLength..]); return writtenLength + 3; } } // Ending with /. else if (remainingLength == 2) { src[writtenLength++] = ByteSlash; return writtenLength; } } return writtenLength; } }