aspnetcore/src/Microsoft.AspNet.Routing/Internal/UriBuildingContext.cs

206 lines
6.4 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.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Text;
using System.Text.Encodings.Web;
namespace Microsoft.AspNet.Routing.Internal
{
[DebuggerDisplay("{DebuggerToString(),nq}")]
public class UriBuildingContext
{
// Holds the 'accepted' parts of the uri.
private readonly StringBuilder _uri;
// Holds the 'optional' parts of the uri. 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;
_uri = new StringBuilder();
_buffer = new List<BufferValue>();
Writer = new StringWriter(_uri);
_lastValueOffset = -1;
BufferState = SegmentState.Beginning;
UriState = SegmentState.Beginning;
}
public SegmentState BufferState { get; private set; }
public SegmentState UriState { get; private set; }
public TextWriter Writer { get; }
public bool Accept(string value)
{
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;
}
for (var i = 0; i < _buffer.Count; i++)
{
if (_buffer[i].RequiresEncoding)
{
_urlEncoder.Encode(Writer, _buffer[i].Value);
}
else
{
_uri.Append(_buffer[i].Value);
}
}
_buffer.Clear();
if (UriState == SegmentState.Beginning && BufferState == SegmentState.Beginning)
{
if (_uri.Length != 0)
{
_uri.Append("/");
}
}
BufferState = SegmentState.Inside;
UriState = SegmentState.Inside;
_lastValueOffset = _uri.Length;
// Allow the first segment to have a leading slash.
// This prevents the leading slash from PathString segments from being encoded.
if (_uri.Length == 0 && value.Length > 0 && value[0] == '/')
{
_uri.Append("/");
_urlEncoder.Encode(Writer, value, 1, value.Length - 1);
}
else
{
_urlEncoder.Encode(Writer, value);
}
return true;
}
public void Remove(string literal)
{
Debug.Assert(_lastValueOffset != -1, "Cannot invoke Remove more than once.");
_uri.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 (_uri.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()
{
_uri.Clear();
if (_uri.Capacity > 128)
{
// We don't want to retain too much memory if this is getting pooled.
_uri.Capacity = 128;
}
_buffer.Clear();
if (_buffer.Capacity > 8)
{
_buffer.Capacity = 8;
}
_hasEmptySegment = false;
_lastValueOffset = -1;
BufferState = SegmentState.Beginning;
UriState = SegmentState.Beginning;
}
public override string ToString()
{
// We can ignore any currently buffered segments - they are are guaranteed to be 'defaults'.
if (_uri.Length > 0 && _uri[0] != '/')
{
// Normalize generated paths so that they always contain a leading slash.
_uri.Insert(0, '/');
}
return _uri.ToString();
}
private string DebuggerToString()
{
return string.Format("{{Accepted: '{0}' Buffered: '{1}'}}", _uri, string.Join("", _buffer));
}
}
}