aspnetcore/src/Shared/HttpSys/RequestProcessing/PathNormalizer.cs

208 lines
7.3 KiB
C#

// 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<byte> 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;
}
}
}