Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 3 additions & 9 deletions src/libraries/System.Private.Uri/src/System.Private.Uri.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,9 @@
</PropertyGroup>

<ItemGroup>
<Compile Include="$(CommonPath)System\Collections\Generic\ArrayBuilder.cs"
Link="Common\System\Collections\Generic\ArrayBuilder.cs" />
<Compile Include="$(CommonPath)System\HexConverter.cs"
Link="Common\System\HexConverter.cs" />
<Compile Include="$(CommonPath)System\Obsoletions.cs"
Link="Common\System\Obsoletions.cs" />
</ItemGroup>

<ItemGroup>
<Compile Include="$(CoreLibSharedDir)System\Collections\Generic\ValueListBuilder.cs" Link="Common\System\Collections\Generic\ValueListBuilder.cs" />
<Compile Include="$(CommonPath)System\HexConverter.cs" Link="Common\System\HexConverter.cs" />
<Compile Include="$(CommonPath)System\Obsoletions.cs" Link="Common\System\Obsoletions.cs" />
<Compile Include="System\DomainNameHelper.cs" />
<Compile Include="System\GenericUriParser.cs" />
<Compile Include="System\IPv4AddressHelper.cs" />
Expand Down
144 changes: 15 additions & 129 deletions src/libraries/System.Private.Uri/src/System/Uri.cs
Original file line number Diff line number Diff line change
Expand Up @@ -950,7 +950,8 @@ public string[] Segments
}
else
{
ArrayBuilder<string> pathSegments = default;
var pathSegments = new ValueListBuilder<string>(4);

int current = 0;
while (current < path.Length)
{
Expand All @@ -959,10 +960,12 @@ public string[] Segments
{
next = path.Length - 1;
}
pathSegments.Add(path.Substring(current, (next - current) + 1));
pathSegments.Append(path.Substring(current, (next - current) + 1));
current = next + 1;
}
segments = pathSegments.ToArray();

segments = pathSegments.AsSpan().ToArray();
pathSegments.Dispose();
}

return segments;
Expand Down Expand Up @@ -4438,7 +4441,11 @@ private unsafe void GetCanonicalPath(ref ValueStringBuilder dest, UriFormat form
if (InFact(Flags.ShouldBeCompressed) && dest.Length - offset > 0)
{
// It will also convert back slashes if needed
dest.Length = offset + Compress(dest.RawChars.Slice(offset, dest.Length - offset), _syntax);
dest.Length = offset + UriHelper.Compress(
dest.RawChars.Slice(offset, dest.Length - offset),
_syntax.InFact(UriSyntaxFlags.ConvertPathSlashes),
_syntax.InFact(UriSyntaxFlags.CanonicalizeAsFilePath));

if (dest[start] == '\\')
dest[start] = '/';

Expand Down Expand Up @@ -4576,131 +4583,10 @@ private static unsafe void UnescapeOnly(char* pch, int start, ref int end, char

private static void Compress(char[] dest, int start, ref int destLength, UriParser syntax)
{
destLength = start + Compress(dest.AsSpan(start, destLength - start), syntax);
}

//
// This will compress any "\" "/../" "/./" "///" "/..../" /XXX.../, etc found in the input
//
// The passed syntax controls whether to use aggressive compression or the one specified in RFC 2396
//
private static int Compress(Span<char> span, UriParser syntax)
{
if (syntax.InFact(UriSyntaxFlags.ConvertPathSlashes))
{
span.Replace('\\', '/');
}

int slashCount = 0;
int lastSlash = 0;
int dotCount = 0;
int removeSegments = 0;

for (int i = span.Length - 1; i >= 0; i--)
{
char ch = span[i];

// compress multiple '/' for file URI
if (ch == '/')
{
++slashCount;
}
else
{
if (slashCount > 1)
{
// else preserve repeated slashes
lastSlash = i + 1;
}
slashCount = 0;
}

if (ch == '.')
{
++dotCount;
continue;
}
else if (dotCount != 0)
{
bool skipSegment = syntax.NotAny(UriSyntaxFlags.CanonicalizeAsFilePath)
&& (dotCount > 2 || ch != '/');

// Cases:
// /./ = remove this segment
// /../ = remove this segment, mark next for removal
// /....x = DO NOT TOUCH, leave as is
// x.../ = DO NOT TOUCH, leave as is, except for V2 legacy mode
if (!skipSegment && ch == '/')
{
if ((lastSlash == i + dotCount + 1 // "/..../"
|| (lastSlash == 0 && i + dotCount + 1 == span.Length)) // "/..."
&& (dotCount <= 2))
{
// /./ or /.<eos> or /../ or /..<eos>

// span.Remove(i + 1, dotCount + (lastSlash == 0 ? 0 : 1));
lastSlash = i + 1 + dotCount + (lastSlash == 0 ? 0 : 1);
span.Slice(lastSlash).CopyTo(span.Slice(i + 1));
span = span.Slice(0, span.Length - (lastSlash - i - 1));

lastSlash = i;
if (dotCount == 2)
{
// We have 2 dots in between like /../ or /..<eos>,
// Mark next segment for removal and remove this /../ or /..
++removeSegments;
}
dotCount = 0;
continue;
}
}
// .NET 4.5 no longer removes trailing dots in a path segment x.../ or x...<eos>
dotCount = 0;

// Here all other cases go such as
// x.[..]y or /.[..]x or (/x.[...][/] && removeSegments !=0)
}

// Now we may want to remove a segment because of previous /../
if (ch == '/')
{
if (removeSegments != 0)
{
--removeSegments;

span.Slice(lastSlash + 1).CopyTo(span.Slice(i + 1));
span = span.Slice(0, span.Length - (lastSlash - i));
}
lastSlash = i;
}
}

if (span.Length != 0 && syntax.InFact(UriSyntaxFlags.CanonicalizeAsFilePath))
{
if (slashCount <= 1)
{
if (removeSegments != 0 && span[0] != '/')
{
//remove first not rooted segment
lastSlash++;
span.Slice(lastSlash).CopyTo(span);
return span.Length - lastSlash;
}
else if (dotCount != 0)
{
// If final string starts with a segment looking like .[...]/ or .[...]<eos>
// then we remove this first segment
if (lastSlash == dotCount || (lastSlash == 0 && dotCount == span.Length))
{
dotCount += lastSlash == 0 ? 0 : 1;
span.Slice(dotCount).CopyTo(span);
return span.Length - dotCount;
}
}
}
}

return span.Length;
destLength = start + UriHelper.Compress(
dest.AsSpan(start, destLength - start),
syntax.InFact(UriSyntaxFlags.ConvertPathSlashes),
syntax.InFact(UriSyntaxFlags.CanonicalizeAsFilePath));
}

//
Expand Down
158 changes: 158 additions & 0 deletions src/libraries/System.Private.Uri/src/System/UriHelper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
// The .NET Foundation licenses this file to you under the MIT license.

using System.Buffers;
using System.Collections.Generic;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Runtime.InteropServices;
Expand Down Expand Up @@ -657,5 +658,162 @@ public static bool StripBidiControlCharacters(ReadOnlySpan<char> strToClean, [No
});
return true;
}

// This will compress any "\" "/../" "/./" "///" "/..../" /XXX.../, etc found in the input
//
// The passed options control whether to use aggressive compression or the one specified in RFC 2396
public static int Compress(Span<char> span, bool convertPathSlashes, bool canonicalizeAsFilePath)
{
if (span.IsEmpty)
{
return 0;
}

if (convertPathSlashes)
{
span.Replace('\\', '/');
}

ValueListBuilder<(int Start, int Length)> removedSegments = default;

int slashCount = 0;
int lastSlash = 0;
int dotCount = 0;
int removeSegments = 0;

for (int i = span.Length - 1; i >= 0; i--)
{
char ch = span[i];

// compress multiple '/' for file URI
if (ch == '/')
{
++slashCount;
}
else
{
if (slashCount > 1)
{
// else preserve repeated slashes
lastSlash = i + 1;
}
slashCount = 0;
}

if (ch == '.')
{
++dotCount;
continue;
}
else if (dotCount != 0)
{
bool skipSegment = canonicalizeAsFilePath && (dotCount > 2 || ch != '/');

// Cases:
// /./ = remove this segment
// /../ = remove this segment, mark next for removal
// /....x = DO NOT TOUCH, leave as is
// x.../ = DO NOT TOUCH, leave as is, except for V2 legacy mode
if (!skipSegment && ch == '/')
{
if ((lastSlash == i + dotCount + 1 // "/..../"
|| (lastSlash == 0 && i + dotCount + 1 == span.Length)) // "/..."
&& (dotCount <= 2))
{
// /./ or /.<eos> or /../ or /..<eos>
removedSegments.Append((i + 1, dotCount + (lastSlash == 0 ? 0 : 1)));

lastSlash = i;
if (dotCount == 2)
{
// We have 2 dots in between like /../ or /..<eos>,
// Mark next segment for removal and remove this /../ or /..
++removeSegments;
}
dotCount = 0;
continue;
}
}
// .NET 4.5 no longer removes trailing dots in a path segment x.../ or x...<eos>
dotCount = 0;

// Here all other cases go such as
// x.[..]y or /.[..]x or (/x.[...][/] && removeSegments !=0)
}

// Now we may want to remove a segment because of previous /../
if (ch == '/')
{
if (removeSegments != 0)
{
removeSegments--;
removedSegments.Append((i + 1, lastSlash - i));
}

lastSlash = i;
}
}

if (canonicalizeAsFilePath)
{
if (slashCount <= 1)
{
if (removeSegments != 0 && span[0] != '/')
{
// remove first not rooted segment
removedSegments.Append((0, lastSlash + 1));
}
else if (dotCount != 0)
{
// If final string starts with a segment looking like .[...]/ or .[...]<eos>
// then we remove this first segment
if (lastSlash == dotCount || (lastSlash == 0 && dotCount == span.Length))
{
removedSegments.Append((0, dotCount + (lastSlash == 0 ? 0 : 1)));
}
}
}
}

if (removedSegments.Length == 0)
{
return span.Length;
}

// Merge any remaining segments.
// Write and read offsets are only ever the same for the first segment.
// Copying the first section would no-op anyway, so we start with the first removed segment.
int writeOffset = removedSegments[^1].Start;
int readOffset = writeOffset;

for (int i = removedSegments.Length - 1; i >= 0; i--)
{
(int start, int length) = removedSegments[i];

Debug.Assert(start >= readOffset && length > 0 && start + length <= span.Length);

if (readOffset != start)
{
Debug.Assert(readOffset > writeOffset);

int segmentLength = start - readOffset;
span.Slice(readOffset, segmentLength).CopyTo(span.Slice(writeOffset));
writeOffset += segmentLength;
}

readOffset = start + length;
}

if (readOffset != span.Length)
{
Debug.Assert(readOffset > writeOffset);

span.Slice(readOffset).CopyTo(span.Slice(writeOffset));
writeOffset += span.Length - readOffset;
}

removedSegments.Dispose();
return writeOffset;
}
}
}
Loading
Loading