|
// 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.Text;
using System.Text.Encodings.Web;
using Microsoft.AspNetCore.Http;
namespace Microsoft.AspNetCore.Routing;
[DebuggerDisplay("{DebuggerToString(),nq}")]
internal sealed class UriBuildingContext
{
// Holds the 'accepted' parts of the path.
private readonly StringBuilder _path;
private readonly StringBuilder _query;
// Holds the 'optional' parts of the path. We need a secondary buffer to handle cases where an optional
// segment is in the middle of the uri. We don't know if we need to write it out - if it's
// followed by other optional segments than we will just throw it away.
private readonly List<BufferValue> _buffer;
private readonly UrlEncoder _urlEncoder;
private bool _hasEmptySegment;
private int _lastValueOffset;
public UriBuildingContext(UrlEncoder urlEncoder)
{
_urlEncoder = urlEncoder;
_path = new StringBuilder();
_query = new StringBuilder();
_buffer = new List<BufferValue>();
PathWriter = new StringWriter(_path);
QueryWriter = new StringWriter(_query);
_lastValueOffset = -1;
BufferState = SegmentState.Beginning;
UriState = SegmentState.Beginning;
}
public bool LowercaseUrls { get; set; }
public bool LowercaseQueryStrings { get; set; }
public bool AppendTrailingSlash { get; set; }
public SegmentState BufferState { get; private set; }
public SegmentState UriState { get; private set; }
public TextWriter PathWriter { get; }
public TextWriter QueryWriter { get; }
public bool Accept(string? value)
{
return Accept(value, encodeSlashes: true);
}
public bool Accept(string? value, bool encodeSlashes)
{
if (string.IsNullOrEmpty(value))
{
if (UriState == SegmentState.Inside || BufferState == SegmentState.Inside)
{
// We can't write an 'empty' part inside a segment
return false;
}
else
{
_hasEmptySegment = true;
return true;
}
}
else if (_hasEmptySegment)
{
// We're trying to write text after an empty segment - this is not allowed.
return false;
}
// NOTE: this needs to be above all 'EncodeValue' and _path.Append calls
if (LowercaseUrls)
{
value = value.ToLowerInvariant();
}
var buffer = _buffer;
for (var i = 0; i < buffer.Count; i++)
{
var bufferValue = buffer[i].Value;
if (LowercaseUrls)
{
bufferValue = bufferValue.ToLowerInvariant();
}
if (buffer[i].RequiresEncoding)
{
EncodeValue(bufferValue);
}
else
{
_path.Append(bufferValue);
}
}
buffer.Clear();
if (UriState == SegmentState.Beginning && BufferState == SegmentState.Beginning)
{
if (_path.Length != 0)
{
_path.Append('/');
}
}
BufferState = SegmentState.Inside;
UriState = SegmentState.Inside;
_lastValueOffset = _path.Length;
// Allow the first segment to have a leading slash.
// This prevents the leading slash from PathString segments from being encoded.
if (_path.Length == 0 && value.Length > 0 && value[0] == '/')
{
_path.Append('/');
EncodeValue(value, 1, value.Length - 1, encodeSlashes);
}
else
{
EncodeValue(value, encodeSlashes);
}
return true;
}
public void Remove()
{
Debug.Assert(_lastValueOffset != -1, "Cannot invoke Remove more than once.");
_path.Length = _lastValueOffset;
_lastValueOffset = -1;
}
public bool Buffer(string? value)
{
if (string.IsNullOrEmpty(value))
{
if (BufferState == SegmentState.Inside)
{
// We can't write an 'empty' part inside a segment
return false;
}
else
{
_hasEmptySegment = true;
return true;
}
}
else if (_hasEmptySegment)
{
// We're trying to write text after an empty segment - this is not allowed.
return false;
}
if (UriState == SegmentState.Inside)
{
// We've already written part of this segment so there's no point in buffering, we need to
// write out the rest or give up.
var result = Accept(value);
// We've already checked the conditions that could result in a rejected part, so this should
// always be true.
Debug.Assert(result);
return result;
}
if (UriState == SegmentState.Beginning && BufferState == SegmentState.Beginning)
{
if (_path.Length != 0 || _buffer.Count != 0)
{
_buffer.Add(new BufferValue("/", requiresEncoding: false));
}
BufferState = SegmentState.Inside;
}
_buffer.Add(new BufferValue(value, requiresEncoding: true));
return true;
}
public void EndSegment()
{
BufferState = SegmentState.Beginning;
UriState = SegmentState.Beginning;
}
public void Clear()
{
_path.Clear();
if (_path.Capacity > 128)
{
// We don't want to retain too much memory if this is getting pooled.
_path.Capacity = 128;
}
_query.Clear();
if (_query.Capacity > 128)
{
_query.Capacity = 128;
}
_buffer.Clear();
if (_buffer.Capacity > 8)
{
_buffer.Capacity = 8;
}
_hasEmptySegment = false;
_lastValueOffset = -1;
BufferState = SegmentState.Beginning;
UriState = SegmentState.Beginning;
AppendTrailingSlash = false;
LowercaseQueryStrings = false;
LowercaseUrls = false;
}
// Used by TemplateBinder.BindValues - the legacy code path of IRouter
public override string ToString()
{
// We can ignore any currently buffered segments - they are are guaranteed to be 'defaults'.
if (_path.Length > 0 && _path[0] != '/')
{
// Normalize generated paths so that they always contain a leading slash.
_path.Insert(0, '/');
}
return _path.ToString() + _query.ToString();
}
// Used by TemplateBinder.TryBindValues - the new code path of LinkGenerator
public PathString ToPathString()
{
PathString pathString;
if (_path.Length > 0)
{
if (_path[0] != '/')
{
// Normalize generated paths so that they always contain a leading slash.
_path.Insert(0, '/');
}
if (AppendTrailingSlash && _path[_path.Length - 1] != '/')
{
_path.Append('/');
}
pathString = new PathString(_path.ToString());
}
else
{
pathString = PathString.Empty;
}
return pathString;
}
// Used by TemplateBinder.TryBindValues - the new code path of LinkGenerator
public QueryString ToQueryString()
{
if (_query.Length > 0 && _query[0] != '?')
{
// Normalize generated query so that they always contain a leading ?.
_query.Insert(0, '?');
}
return new QueryString(_query.ToString());
}
private void EncodeValue(string value)
{
EncodeValue(value, encodeSlashes: true);
}
private void EncodeValue(string value, bool encodeSlashes)
{
EncodeValue(value, start: 0, characterCount: value.Length, encodeSlashes);
}
// For testing
internal void EncodeValue(string value, int start, int characterCount, bool encodeSlashes)
{
// Just encode everything if its ok to encode slashes
if (encodeSlashes)
{
_urlEncoder.Encode(PathWriter, value, start, characterCount);
}
else
{
int end;
int length = start + characterCount;
while ((end = value.IndexOf('/', start, characterCount)) >= 0)
{
_urlEncoder.Encode(PathWriter, value, start, end - start);
_path.Append('/');
start = end + 1;
characterCount = length - start;
}
if (end < 0 && characterCount >= 0)
{
_urlEncoder.Encode(PathWriter, value, start, length - start);
}
}
}
private string DebuggerToString()
{
return $@"Accepted = ""{_path}"", Buffered = ""{string.Join("", _buffer)}""";
}
private readonly struct BufferValue
{
public BufferValue(string value, bool requiresEncoding)
{
Value = value;
RequiresEncoding = requiresEncoding;
}
public bool RequiresEncoding { get; }
public string Value { get; }
}
}
|