// 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.Diagnostics; namespace Microsoft.AspNetCore.HttpSys.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 unsafe int RemoveDotSegments(Span input) { fixed (byte* start = input) { var end = start + input.Length; return RemoveDotSegments(start, end); } } public static unsafe int RemoveDotSegments(byte* start, byte* end) { if (!ContainsDotSegments(start, end)) { return (int)(end - start); } var src = start; var dst = start; while (src < end) { var ch1 = *src; Debug.Assert(ch1 == '/', "Path segment must always start with a '/'"); byte ch2, ch3, ch4; switch (end - src) { case 1: break; case 2: ch2 = *(src + 1); if (ch2 == ByteDot) { // B. if the input buffer begins with a prefix of "/./" or "/.", // where "." is a complete path segment, then replace that // prefix with "/" in the input buffer; otherwise, src += 1; *src = ByteSlash; continue; } break; case 3: ch2 = *(src + 1); ch3 = *(src + 2); if (ch2 == ByteDot && ch3 == ByteDot) { // C. if the input buffer begins with a prefix of "/../" or "/..", // where ".." is a complete path segment, then replace that // prefix with "/" in the input buffer and remove the last // segment and its preceding "/" (if any) from the output // buffer; otherwise, src += 2; *src = ByteSlash; if (dst > start) { do { dst--; } while (dst > start && *dst != ByteSlash); } continue; } else if (ch2 == ByteDot && ch3 == ByteSlash) { // B. if the input buffer begins with a prefix of "/./" or "/.", // where "." is a complete path segment, then replace that // prefix with "/" in the input buffer; otherwise, src += 2; continue; } break; default: ch2 = *(src + 1); ch3 = *(src + 2); ch4 = *(src + 3); if (ch2 == ByteDot && ch3 == ByteDot && ch4 == ByteSlash) { // C. if the input buffer begins with a prefix of "/../" or "/..", // where ".." is a complete path segment, then replace that // prefix with "/" in the input buffer and remove the last // segment and its preceding "/" (if any) from the output // buffer; otherwise, src += 3; if (dst > start) { do { dst--; } while (dst > start && *dst != ByteSlash); } continue; } else if (ch2 == ByteDot && ch3 == ByteSlash) { // B. if the input buffer begins with a prefix of "/./" or "/.", // where "." is a complete path segment, then replace that // prefix with "/" in the input buffer; otherwise, src += 2; continue; } break; } // E. move the first path segment in the input buffer to the end of // the output buffer, including the initial "/" character (if // any) and any subsequent characters up to, but not including, // the next "/" character or the end of the input buffer. do { *dst++ = ch1; ch1 = *++src; } while (src < end && ch1 != ByteSlash); } if (dst == start) { *dst++ = ByteSlash; } return (int)(dst - start); } public static unsafe bool ContainsDotSegments(byte* start, byte* end) { var src = start; var dst = start; while (src < end) { var ch1 = *src; Debug.Assert(ch1 == '/', "Path segment must always start with a '/'"); byte ch2, ch3, ch4; switch (end - src) { case 1: break; case 2: ch2 = *(src + 1); if (ch2 == ByteDot) { return true; } break; case 3: ch2 = *(src + 1); ch3 = *(src + 2); if ((ch2 == ByteDot && ch3 == ByteDot) || (ch2 == ByteDot && ch3 == ByteSlash)) { return true; } break; default: ch2 = *(src + 1); ch3 = *(src + 2); ch4 = *(src + 3); if ((ch2 == ByteDot && ch3 == ByteDot && ch4 == ByteSlash) || (ch2 == ByteDot && ch3 == ByteSlash)) { return true; } break; } do { ch1 = *++src; } while (src < end && ch1 != ByteSlash); } return false; } } }