// 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 _buffer; private readonly UrlEncoder _urlEncoder; private bool _hasEmptySegment; private int _lastValueOffset; public UriBuildingContext(UrlEncoder urlEncoder) { _urlEncoder = urlEncoder; _uri = new StringBuilder(); _buffer = new List(); 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)); } } }