#if NET using System.Buffers; #endif using System.Diagnostics; using System.Diagnostics.CodeAnalysis; using System.Globalization; using System.Runtime.CompilerServices; using System.Text; using System.Text.RegularExpressions; namespace ModelContextProtocol; /// Provides basic support for parsing and formatting URI templates. /// /// This implementation should correctly handle valid URI templates, but it has undefined output for invalid templates, /// e.g. it may treat portions of invalid templates as literals rather than throwing. /// internal static partial class UriTemplate { /// Regex pattern for finding URI template expressions and parsing out the operator and varname. private const string UriTemplateExpressionPattern = """ { # opening brace (?[+#./;?&]?) # optional operator (? (?:[A-Za-z0-9_]|%[0-9A-Fa-f]{2}) # varchar: letter, digit, underscore, or pct-encoded (?:\.?(?:[A-Za-z0-9_]|%[0-9A-Fa-f]{2}))* # optionally dot-separated subsequent varchars ) (?: :[1-9][0-9]{0,3} )? # optional prefix modifier (1–4 digits) \*? # optional explode (?:, # comma separator, followed by the same as above (? (?:[A-Za-z0-9_]|%[0-9A-Fa-f]{2}) (?:\.?(?:[A-Za-z0-9_]|%[0-9A-Fa-f]{2}))* ) (?: :[1-9][0-9]{0,3} )? \*? )* # zero or more additional vars } # closing brace """; /// Gets a regex for finding URI template expressions and parsing out the operator and varname. /// /// This regex is for parsing a static URI template. /// It is not for parsing a URI according to a template. /// #if NET [GeneratedRegex(UriTemplateExpressionPattern, RegexOptions.IgnorePatternWhitespace)] private static partial Regex UriTemplateExpression(); #else private static Regex UriTemplateExpression() => s_uriTemplateExpression; private static readonly Regex s_uriTemplateExpression = new(UriTemplateExpressionPattern, RegexOptions.IgnorePatternWhitespace | RegexOptions.Compiled); #endif #if NET /// SearchValues for characters that needn't be escaped when allowing reserved characters. private static readonly SearchValues s_appendWhenAllowReserved = SearchValues.Create( "abcdefghijklmnopqrstuvwxyz" + // ASCII lowercase letters "ABCDEFGHIJKLMNOPQRSTUVWXYZ" + // ASCII uppercase letters "0123456789" + // ASCII digits "-._~" + // unreserved characters ":/?#[]@!$&'()*+,;="); // reserved characters #endif /// Create a for matching a URI against a URI template. /// The template against which to match. /// A regex pattern that can be used to match the specified URI template. public static Regex CreateParser(string uriTemplate) { DefaultInterpolatedStringHandler pattern = new(0, 0, CultureInfo.InvariantCulture, stackalloc char[256]); pattern.AppendFormatted('^'); int lastIndex = 0; for (Match m = UriTemplateExpression().Match(uriTemplate); m.Success; m = m.NextMatch()) { pattern.AppendFormatted(Regex.Escape(uriTemplate[lastIndex..m.Index])); lastIndex = m.Index + m.Length; var captures = m.Groups["varname"].Captures; List paramNames = new(captures.Count); foreach (Capture c in captures) { paramNames.Add(c.Value); } switch (m.Groups["operator"].Value) { case "#": AppendExpression(ref pattern, paramNames, '#', "[^,]+"); break; case "/": AppendExpression(ref pattern, paramNames, '/', "[^/?]+"); break; default: AppendExpression(ref pattern, paramNames, null, "[^/?&]+"); break; case "?": AppendQueryExpression(ref pattern, paramNames, '?'); break; case "&": AppendQueryExpression(ref pattern, paramNames, '&'); break; } } pattern.AppendFormatted(Regex.Escape(uriTemplate.Substring(lastIndex))); pattern.AppendFormatted('$'); return new Regex( pattern.ToStringAndClear(), RegexOptions.IgnoreCase | RegexOptions.CultureInvariant | #if NET RegexOptions.NonBacktracking); #else RegexOptions.Compiled, TimeSpan.FromSeconds(10)); #endif // Appends a regex fragment to `pattern` that matches an optional query string starting // with the given `prefix` (? or &), and up to one occurrence of each name in // `paramNames`. Each parameter is made optional and captured by a named group // of the form “paramName=value”. static void AppendQueryExpression(ref DefaultInterpolatedStringHandler pattern, List paramNames, char prefix) { Debug.Assert(prefix is '?' or '&'); pattern.AppendFormatted("(?:\\"); pattern.AppendFormatted(prefix); if (paramNames.Count > 0) { AppendParameter(ref pattern, paramNames[0]); for (int i = 1; i < paramNames.Count; i++) { pattern.AppendFormatted("\\&?"); AppendParameter(ref pattern, paramNames[i]); } static void AppendParameter(ref DefaultInterpolatedStringHandler pattern, string paramName) { paramName = Regex.Escape(paramName); pattern.AppendFormatted("(?:"); pattern.AppendFormatted(paramName); pattern.AppendFormatted("=(?<"); pattern.AppendFormatted(paramName); pattern.AppendFormatted(">[^/?&]+))?"); } } pattern.AppendFormatted(")?"); } // Chooses a regex character‐class (`valueChars`) based on the initial `prefix` to define which // characters make up a parameter value. Then, for each name in `paramNames`, it optionally // appends the escaped `prefix` (only on the first parameter, then switches to ','), and // adds an optional named capture group `(?valueChars)` to match and capture that value. static void AppendExpression(ref DefaultInterpolatedStringHandler pattern, List paramNames, char? prefix, string valueChars) { Debug.Assert(prefix is '#' or '/' or null); if (paramNames.Count > 0) { if (prefix is not null) { pattern.AppendFormatted('\\'); pattern.AppendFormatted(prefix); pattern.AppendFormatted('?'); } AppendParameter(ref pattern, paramNames[0], valueChars); for (int i = 1; i < paramNames.Count; i++) { pattern.AppendFormatted("\\,?"); AppendParameter(ref pattern, paramNames[i], valueChars); } static void AppendParameter(ref DefaultInterpolatedStringHandler pattern, string paramName, string valueChars) { pattern.AppendFormatted("(?<"); pattern.AppendFormatted(Regex.Escape(paramName)); pattern.AppendFormatted('>'); pattern.AppendFormatted(valueChars); pattern.AppendFormatted(")?"); } } } } /// /// Expand a URI template using the given variable values. /// public static string FormatUri(string uriTemplate, IReadOnlyDictionary arguments) { Throw.IfNull(uriTemplate); ReadOnlySpan uriTemplateSpan = uriTemplate.AsSpan(); DefaultInterpolatedStringHandler builder = new(0, 0, CultureInfo.InvariantCulture, stackalloc char[256]); while (!uriTemplateSpan.IsEmpty) { // Find the next expression. int openBracePos = uriTemplateSpan.IndexOf('{'); if (openBracePos < 0) { if (uriTemplate.Length == uriTemplateSpan.Length) { return uriTemplate; } builder.AppendFormatted(uriTemplateSpan); break; } // Append as a literal everything before the next expression. builder.AppendFormatted(uriTemplateSpan.Slice(0, openBracePos)); uriTemplateSpan = uriTemplateSpan.Slice(openBracePos + 1); int closeBracePos = uriTemplateSpan.IndexOf('}'); if (closeBracePos < 0) { throw new FormatException($"Unmatched '{{' in URI template '{uriTemplate}'"); } ReadOnlySpan expression = uriTemplateSpan.Slice(0, closeBracePos); uriTemplateSpan = uriTemplateSpan.Slice(closeBracePos + 1); if (expression.IsEmpty) { continue; } // The start of the expression may be a modifier; if it is, slice it off the expression. char modifier = expression[0]; (string Prefix, string Separator, bool Named, bool IncludeNameIfEmpty, bool IncludeSeparatorIfEmpty, bool AllowReserved, bool PrefixEmptyExpansions, int ExpressionSlice) modifierBehavior = modifier switch { '+' => (string.Empty, ",", false, false, true, true, false, 1), '#' => ("#", ",", false, false, true, true, true, 1), '.' => (".", ".", false, false, true, false, true, 1), '/' => ("/", "/", false, false, true, false, false, 1), ';' => (";", ";", true, true, false, false, false, 1), '?' => ("?", "&", true, true, true, false, false, 1), '&' => ("&", "&", true, true, true, false, false, 1), _ => (string.Empty, ",", false, false, true, false, false, 0), }; expression = expression.Slice(modifierBehavior.ExpressionSlice); List expansions = []; // Process each varspec in the comma-delimited list in the expression (if it doesn't have any // commas, it will be the whole expression). while (!expression.IsEmpty) { // Find the next name. int commaPos = expression.IndexOf(','); ReadOnlySpan name; if (commaPos < 0) { name = expression; expression = []; } else { name = expression.Slice(0, commaPos); expression = expression.Slice(commaPos + 1); } bool explode = false; int prefixLength = -1; // If the name ends with a *, it means we should explode the value into separate // name=value pairs. If it has a colon, it means we should only take the first N characters // of the value. If it has both, the * takes precedence and we ignore the colon. if (!name.IsEmpty && name[name.Length - 1] == '*') { explode = true; name = name.Slice(0, name.Length - 1); } else if (name.IndexOf(':') >= 0) { int colonPos = name.IndexOf(':'); if (colonPos < 0) { throw new FormatException($"Invalid varspec '{name.ToString()}'"); } if (!int.TryParse(name.Slice(colonPos + 1) #if !NET .ToString() #endif , out prefixLength)) { throw new FormatException($"Invalid prefix length in varspec '{name.ToString()}'"); } name = name.Slice(0, colonPos); } // Look up the value for this name. If it doesn't exist, skip it. string nameString = name.ToString(); if (!arguments.TryGetValue(nameString, out var value) || value is null) { continue; } if (value is IEnumerable list) { var items = list.Select(i => Encode(i, modifierBehavior.AllowReserved)); if (explode) { if (modifierBehavior.Named) { foreach (var item in items) { expansions.Add($"{nameString}={item}"); } } else { foreach (var item in items) { expansions.Add(item); } } } else { var joined = string.Join(",", items); expansions.Add(joined.Length > 0 && modifierBehavior.Named ? $"{nameString}={joined}" : joined); } } else if (value is IReadOnlyDictionary assoc) { var pairs = assoc.Select(kvp => ( Encode(kvp.Key, modifierBehavior.AllowReserved), Encode(kvp.Value, modifierBehavior.AllowReserved) )); if (explode) { foreach (var (k, v) in pairs) { expansions.Add($"{k}={v}"); } } else { var joined = string.Join(",", pairs.Select(p => $"{p.Item1},{p.Item2}")); if (joined.Length > 0) { expansions.Add(modifierBehavior.Named ? $"{nameString}={joined}" : joined); } } } else { string s = value as string ?? (value is IFormattable f ? f.ToString(null, CultureInfo.InvariantCulture) : value.ToString()) ?? string.Empty; s = Encode((uint)prefixLength < s.Length ? s.Substring(0, prefixLength) : s, modifierBehavior.AllowReserved); if (!modifierBehavior.Named) { expansions.Add(s); } else if (s.Length != 0 || modifierBehavior.IncludeNameIfEmpty) { expansions.Add( s.Length != 0 ? $"{nameString}={s}" : modifierBehavior.IncludeSeparatorIfEmpty ? $"{nameString}=" : nameString); } } } if (expansions.Count > 0 && (modifierBehavior.PrefixEmptyExpansions || !expansions.All(string.IsNullOrEmpty))) { builder.AppendLiteral(modifierBehavior.Prefix); AppendJoin(ref builder, modifierBehavior.Separator, expansions); } } return builder.ToStringAndClear(); } private static void AppendJoin(ref DefaultInterpolatedStringHandler builder, string separator, List values) { int count = values.Count; if (count > 0) { builder.AppendLiteral(values[0]); for (int i = 1; i < count; i++) { builder.AppendLiteral(separator); builder.AppendLiteral(values[i]); } } } private static string Encode(string value, bool allowReserved) { if (!allowReserved) { return Uri.EscapeDataString(value); } DefaultInterpolatedStringHandler builder = new(0, 0, CultureInfo.InvariantCulture, stackalloc char[256]); int i = 0; #if NET i = value.AsSpan().IndexOfAnyExcept(s_appendWhenAllowReserved); if (i < 0) { return value; } builder.AppendFormatted(value.AsSpan(0, i)); #endif for (; i < value.Length; ++i) { char c = value[i]; if (((uint)((c | 0x20) - 'a') <= 'z' - 'a') || ((uint)(c - '0') <= '9' - '0') || "-._~:/?#[]@!$&'()*+,;=".Contains(c)) { builder.AppendFormatted(c); } else if (c == '%' && i < value.Length - 2 && Uri.IsHexDigit(value[i + 1]) && Uri.IsHexDigit(value[i + 2])) { builder.AppendFormatted(value.AsSpan(i, 3)); i += 2; } else { AppendHex(ref builder, c); } } return builder.ToStringAndClear(); static void AppendHex(ref DefaultInterpolatedStringHandler builder, char c) { ReadOnlySpan hexDigits = ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F']; if (c <= 0x7F) { builder.AppendFormatted('%'); builder.AppendFormatted(hexDigits[c >> 4]); builder.AppendFormatted(hexDigits[c & 0xF]); } else { #if NET Span utf8 = stackalloc byte[Encoding.UTF8.GetMaxByteCount(1)]; foreach (byte b in utf8.Slice(0, new Rune(c).EncodeToUtf8(utf8))) #else foreach (byte b in Encoding.UTF8.GetBytes([c])) #endif { builder.AppendFormatted('%'); builder.AppendFormatted(hexDigits[b >> 4]); builder.AppendFormatted(hexDigits[b & 0xF]); } } } } /// /// Defines an equality comparer for Uri templates as follows: /// 1. Non-templated Uris use regular System.Uri equality comparison (host name is case insensitive). /// 2. Templated Uris use regular string equality. /// /// We do this because non-templated resources are looked up directly from the resource dictionary /// and we need to make sure equality is implemented correctly. Templated Uris are resolved in a /// fallback step using linear traversal of the resource dictionary, so their equality is only /// there to distinguish between different templates. /// public sealed class UriTemplateComparer : IEqualityComparer { public static IEqualityComparer Instance { get; } = new UriTemplateComparer(); public bool Equals(string? uriTemplate1, string? uriTemplate2) { if (TryParseAsNonTemplatedUri(uriTemplate1, out Uri? uri1) && TryParseAsNonTemplatedUri(uriTemplate2, out Uri? uri2)) { return uri1 == uri2; } return string.Equals(uriTemplate1, uriTemplate2, StringComparison.Ordinal); } public int GetHashCode([DisallowNull] string uriTemplate) { if (TryParseAsNonTemplatedUri(uriTemplate, out Uri? uri)) { return uri.GetHashCode(); } else { return StringComparer.Ordinal.GetHashCode(uriTemplate); } } private static bool TryParseAsNonTemplatedUri(string? uriTemplate, [NotNullWhen(true)] out Uri? uri) { if (uriTemplate is null || uriTemplate.Contains('{')) { uri = null; return false; } return Uri.TryCreate(uriTemplate, UriKind.Absolute, out uri); } } }