// 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.Text; using Microsoft.AspNetCore.Http.Abstractions; using Microsoft.AspNetCore.Http.Internal; namespace Microsoft.AspNetCore.Http { /// /// Provides correct escaping for Path and PathBase values when needed to reconstruct a request or redirect URI string /// public struct PathString : IEquatable { private static readonly char[] splitChar = { '/' }; /// /// Represents the empty path. This field is read-only. /// public static readonly PathString Empty = new PathString(string.Empty); private readonly string _value; /// /// Initalize the path string with a given value. This value must be in unescaped format. Use /// PathString.FromUriComponent(value) if you have a path value which is in an escaped format. /// /// The unescaped path to be assigned to the Value property. public PathString(string value) { if (!string.IsNullOrEmpty(value) && value[0] != '/') { throw new ArgumentException(Resources.FormatException_PathMustStartWithSlash(nameof(value)), nameof(value)); } _value = value; } /// /// The unescaped path value /// public string Value { get { return _value; } } /// /// True if the path is not empty /// public bool HasValue { get { return !string.IsNullOrEmpty(_value); } } /// /// Provides the path string escaped in a way which is correct for combining into the URI representation. /// /// The escaped path value public override string ToString() { return ToUriComponent(); } /// /// Provides the path string escaped in a way which is correct for combining into the URI representation. /// /// The escaped path value public string ToUriComponent() { if (!HasValue) { return string.Empty; } StringBuilder buffer = null; var start = 0; var count = 0; var requiresEscaping = false; for (int i = 0; i < _value.Length; ++i) { if (PathStringHelper.IsValidPathChar(_value[i])) { if (requiresEscaping) { // the current segment requires escape if (buffer == null) { buffer = new StringBuilder(_value.Length * 3); } buffer.Append(Uri.EscapeDataString(_value.Substring(start, count))); requiresEscaping = false; start = i; count = 0; } count++; } else { if (!requiresEscaping) { // the current segument doesn't require escape if (buffer == null) { buffer = new StringBuilder(_value.Length * 3); } buffer.Append(_value, start, count); requiresEscaping = true; start = i; count = 0; } count++; } } if (count == _value.Length && !requiresEscaping) { return _value; } else { if (count > 0) { if (buffer == null) { buffer = new StringBuilder(_value.Length * 3); } if (requiresEscaping) { buffer.Append(Uri.EscapeDataString(_value.Substring(start, count))); } else { buffer.Append(_value, start, count); } } return buffer.ToString(); } } /// /// Returns an PathString given the path as it is escaped in the URI format. The string MUST NOT contain any /// value that is not a path. /// /// The escaped path as it appears in the URI format. /// The resulting PathString public static PathString FromUriComponent(string uriComponent) { // REVIEW: what is the exactly correct thing to do? return new PathString(Uri.UnescapeDataString(uriComponent)); } /// /// Returns an PathString given the path as from a Uri object. Relative Uri objects are not supported. /// /// The Uri object /// The resulting PathString public static PathString FromUriComponent(Uri uri) { if (uri == null) { throw new ArgumentNullException(nameof(uri)); } // REVIEW: what is the exactly correct thing to do? return new PathString("/" + uri.GetComponents(UriComponents.Path, UriFormat.Unescaped)); } /// /// Determines whether the beginning of this instance matches the specified . /// /// The to compare. /// true if value matches the beginning of this string; otherwise, false. public bool StartsWithSegments(PathString other) { return StartsWithSegments(other, StringComparison.OrdinalIgnoreCase); } /// /// Determines whether the beginning of this instance matches the specified when compared /// using the specified comparison option. /// /// The to compare. /// One of the enumeration values that determines how this and value are compared. /// true if value matches the beginning of this string; otherwise, false. public bool StartsWithSegments(PathString other, StringComparison comparisonType) { var value1 = Value ?? string.Empty; var value2 = other.Value ?? string.Empty; if (value1.StartsWith(value2, comparisonType)) { return value1.Length == value2.Length || value1[value2.Length] == '/'; } return false; } /// /// Determines whether the beginning of this instance matches the specified and returns /// the remaining segments. /// /// The to compare. /// The remaining segments after the match. /// true if value matches the beginning of this string; otherwise, false. public bool StartsWithSegments(PathString other, out PathString remaining) { return StartsWithSegments(other, StringComparison.OrdinalIgnoreCase, out remaining); } /// /// Determines whether the beginning of this instance matches the specified when compared /// using the specified comparison option and returns the remaining segments. /// /// The to compare. /// One of the enumeration values that determines how this and value are compared. /// The remaining segments after the match. /// true if value matches the beginning of this string; otherwise, false. public bool StartsWithSegments(PathString other, StringComparison comparisonType, out PathString remaining) { var value1 = Value ?? string.Empty; var value2 = other.Value ?? string.Empty; if (value1.StartsWith(value2, comparisonType)) { if (value1.Length == value2.Length || value1[value2.Length] == '/') { remaining = new PathString(value1.Substring(value2.Length)); return true; } } remaining = Empty; return false; } /// /// Determines whether the beginning of this instance matches the specified and returns /// the matched and remaining segments. /// /// The to compare. /// The matched segments with the original casing in the source value. /// The remaining segments after the match. /// true if value matches the beginning of this string; otherwise, false. public bool StartsWithSegments(PathString other, out PathString matched, out PathString remaining) { return StartsWithSegments(other, StringComparison.OrdinalIgnoreCase, out matched, out remaining); } /// /// Determines whether the beginning of this instance matches the specified when compared /// using the specified comparison option and returns the matched and remaining segments. /// /// The to compare. /// One of the enumeration values that determines how this and value are compared. /// The matched segments with the original casing in the source value. /// The remaining segments after the match. /// true if value matches the beginning of this string; otherwise, false. public bool StartsWithSegments(PathString other, StringComparison comparisonType, out PathString matched, out PathString remaining) { var value1 = Value ?? string.Empty; var value2 = other.Value ?? string.Empty; if (value1.StartsWith(value2, comparisonType)) { if (value1.Length == value2.Length || value1[value2.Length] == '/') { matched = new PathString(value1.Substring(0, value2.Length)); remaining = new PathString(value1.Substring(value2.Length)); return true; } } remaining = Empty; matched = Empty; return false; } /// /// Adds two PathString instances into a combined PathString value. /// /// The combined PathString value public PathString Add(PathString other) { if (HasValue && other.HasValue && Value[Value.Length - 1] == '/') { // If the path string has a trailing slash and the other string has a leading slash, we need // to trim one of them. return new PathString(Value + other.Value.Substring(1)); } return new PathString(Value + other.Value); } /// /// Combines a PathString and QueryString into the joined URI formatted string value. /// /// The joined URI formatted string value public string Add(QueryString other) { return ToUriComponent() + other.ToUriComponent(); } /// /// Compares this PathString value to another value. The default comparison is StringComparison.OrdinalIgnoreCase. /// /// The second PathString for comparison. /// True if both PathString values are equal public bool Equals(PathString other) { return Equals(other, StringComparison.OrdinalIgnoreCase); } /// /// Compares this PathString value to another value using a specific StringComparison type /// /// The second PathString for comparison /// The StringComparison type to use /// True if both PathString values are equal public bool Equals(PathString other, StringComparison comparisonType) { if (!HasValue && !other.HasValue) { return true; } return string.Equals(_value, other._value, comparisonType); } /// /// Compares this PathString value to another value. The default comparison is StringComparison.OrdinalIgnoreCase. /// /// The second PathString for comparison. /// True if both PathString values are equal public override bool Equals(object obj) { if (ReferenceEquals(null, obj)) { return !HasValue; } return obj is PathString && Equals((PathString)obj); } /// /// Returns the hash code for the PathString value. The hash code is provided by the OrdinalIgnoreCase implementation. /// /// The hash code public override int GetHashCode() { return (HasValue ? StringComparer.OrdinalIgnoreCase.GetHashCode(_value) : 0); } /// /// Operator call through to Equals /// /// The left parameter /// The right parameter /// True if both PathString values are equal public static bool operator ==(PathString left, PathString right) { return left.Equals(right); } /// /// Operator call through to Equals /// /// The left parameter /// The right parameter /// True if both PathString values are not equal public static bool operator !=(PathString left, PathString right) { return !left.Equals(right); } /// /// /// The left parameter /// The right parameter /// The ToString combination of both values public static string operator +(string left, PathString right) { // This overload exists to prevent the implicit string<->PathString converter from // trying to call the PathString+PathString operator for things that are not path strings. return string.Concat(left, right.ToString()); } /// /// /// The left parameter /// The right parameter /// The ToString combination of both values public static string operator +(PathString left, string right) { // This overload exists to prevent the implicit string<->PathString converter from // trying to call the PathString+PathString operator for things that are not path strings. return string.Concat(left.ToString(), right); } /// /// Operator call through to Add /// /// The left parameter /// The right parameter /// The PathString combination of both values public static PathString operator +(PathString left, PathString right) { return left.Add(right); } /// /// Operator call through to Add /// /// The left parameter /// The right parameter /// The PathString combination of both values public static string operator +(PathString left, QueryString right) { return left.Add(right); } /// /// Implicitly creates a new PathString from the given string. /// /// public static implicit operator PathString(string s) { return new PathString(s); } /// /// Implicitly calls ToString(). /// /// public static implicit operator string(PathString path) { return path.ToString(); } } }