| File: Formatters\TextOutputFormatter.cs | Web Access |
| Project: src\aspnetcore\src\Mvc\Mvc.Core\src\Microsoft.AspNetCore.Mvc.Core.csproj (Microsoft.AspNetCore.Mvc.Core) |
// Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System.Runtime.CompilerServices; using System.Text; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc.Core; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Primitives; using Microsoft.Net.Http.Headers; namespace Microsoft.AspNetCore.Mvc.Formatters; /// <summary> /// Writes an object in a given text format to the output stream. /// </summary> public abstract class TextOutputFormatter : OutputFormatter { private IDictionary<string, string>? _outputMediaTypeCache; /// <summary> /// Initializes a new instance of the <see cref="TextOutputFormatter"/> class. /// </summary> protected TextOutputFormatter() { SupportedEncodings = new List<Encoding>(); } /// <summary> /// Gets the mutable collection of character encodings supported by /// this <see cref="TextOutputFormatter"/>. The encodings are /// used when writing the data. /// </summary> public IList<Encoding> SupportedEncodings { get; } private IDictionary<string, string> OutputMediaTypeCache { get { if (_outputMediaTypeCache == null) { var cache = new Dictionary<string, string>(); foreach (var mediaType in SupportedMediaTypes) { cache.Add(mediaType, MediaType.ReplaceEncoding(mediaType, Encoding.UTF8)); } // Safe race condition, worst case scenario we initialize the field multiple times with dictionaries containing // the same values. _outputMediaTypeCache = cache; } return _outputMediaTypeCache; } } /// <summary> /// Determines the best <see cref="Encoding"/> amongst the supported encodings /// for reading or writing an HTTP entity body based on the provided content type. /// </summary> /// <param name="context">The formatter context associated with the call. /// </param> /// <returns>The <see cref="Encoding"/> to use when reading the request or writing the response.</returns> public virtual Encoding SelectCharacterEncoding(OutputFormatterWriteContext context) { ArgumentNullException.ThrowIfNull(context); if (SupportedEncodings.Count == 0) { var message = Resources.FormatTextOutputFormatter_SupportedEncodingsMustNotBeEmpty( nameof(SupportedEncodings)); throw new InvalidOperationException(message); } var acceptCharsetHeaderValues = GetAcceptCharsetHeaderValues(context); var encoding = MatchAcceptCharacterEncoding(acceptCharsetHeaderValues); if (encoding != null) { return encoding; } if (context.ContentType.HasValue) { var parsedContentType = new MediaType(context.ContentType); var contentTypeCharset = parsedContentType.Charset; if (contentTypeCharset.HasValue) { for (var i = 0; i < SupportedEncodings.Count; i++) { var supportedEncoding = SupportedEncodings[i]; if (contentTypeCharset.Equals(supportedEncoding.WebName, StringComparison.OrdinalIgnoreCase)) { // This is supported. return SupportedEncodings[i]; } } } } return SupportedEncodings[0]; } /// <inheritdoc /> public override Task WriteAsync(OutputFormatterWriteContext context) { ArgumentNullException.ThrowIfNull(context); var selectedMediaType = context.ContentType; if (!selectedMediaType.HasValue) { // If content type is not set then set it based on supported media types. if (SupportedEncodings.Count > 0) { selectedMediaType = new StringSegment(SupportedMediaTypes[0]); } else { throw new InvalidOperationException(Resources.FormatOutputFormatterNoMediaType(GetType().FullName)); } } var selectedEncoding = SelectCharacterEncoding(context); if (selectedEncoding != null) { // Override the content type value even if one already existed. var mediaTypeWithCharset = GetMediaTypeWithCharset(selectedMediaType.Value!, selectedEncoding); selectedMediaType = new StringSegment(mediaTypeWithCharset); } else { const int statusCode = StatusCodes.Status406NotAcceptable; context.HttpContext.Response.StatusCode = statusCode; if (context.HttpContext.RequestServices.GetService<IProblemDetailsService>() is { } problemDetailsService) { return problemDetailsService.TryWriteAsync(new () { HttpContext = context.HttpContext, ProblemDetails = { Status = statusCode } }).AsTask(); } return Task.CompletedTask; } context.ContentType = selectedMediaType; WriteResponseHeaders(context); return WriteResponseBodyAsync(context, selectedEncoding); } /// <inheritdoc /> public sealed override Task WriteResponseBodyAsync(OutputFormatterWriteContext context) { var message = Resources.FormatTextOutputFormatter_WriteResponseBodyAsyncNotSupported( $"{nameof(WriteResponseBodyAsync)}({nameof(OutputFormatterWriteContext)})", nameof(TextOutputFormatter), $"{nameof(WriteResponseBodyAsync)}({nameof(OutputFormatterWriteContext)},{nameof(Encoding)})"); throw new InvalidOperationException(message); } /// <summary> /// Writes the response body. /// </summary> /// <param name="context">The formatter context associated with the call.</param> /// <param name="selectedEncoding">The <see cref="Encoding"/> that should be used to write the response.</param> /// <returns>A task which can write the response body.</returns> public abstract Task WriteResponseBodyAsync(OutputFormatterWriteContext context, Encoding selectedEncoding); internal static IList<StringWithQualityHeaderValue> GetAcceptCharsetHeaderValues(OutputFormatterWriteContext context) { var request = context.HttpContext.Request; if (StringWithQualityHeaderValue.TryParseList(request.Headers.AcceptCharset, out var result)) { return result; } return Array.Empty<StringWithQualityHeaderValue>(); } private string GetMediaTypeWithCharset(string mediaType, Encoding encoding) { if (string.Equals(encoding.WebName, Encoding.UTF8.WebName, StringComparison.OrdinalIgnoreCase) && OutputMediaTypeCache.TryGetValue(mediaType, out var mediaTypeWithCharset)) { return mediaTypeWithCharset; } return MediaType.ReplaceEncoding(mediaType, encoding); } private Encoding? MatchAcceptCharacterEncoding(IList<StringWithQualityHeaderValue> acceptCharsetHeaders) { if (acceptCharsetHeaders != null && acceptCharsetHeaders.Count > 0) { var acceptValues = Sort(acceptCharsetHeaders); for (var i = 0; i < acceptValues.Count; i++) { var charset = acceptValues[i].Value; if (!StringSegment.IsNullOrEmpty(charset)) { for (var j = 0; j < SupportedEncodings.Count; j++) { var encoding = SupportedEncodings[j]; if (charset.Equals(encoding.WebName, StringComparison.OrdinalIgnoreCase) || charset.Equals("*", StringComparison.Ordinal)) { return encoding; } } } } } return null; } // We may have to filter q=0 values and reorder the rest by quality. StringWithQualityHeaderValue // is a reference type, so it can't live in a stack buffer, but its indices can. Real Accept-Charset // headers are tiny (a handful of charsets at most), so for anything within the threshold we sort a // stack-allocated inline-array buffer of indices with an insertion sort and never touch the heap for // scratch. No real client sends more than a handful of charsets, so the >32 branch only exists for // correctness; there we fall back to a plain List.Sort and don't bother preserving the tie ordering, // because nothing observable can depend on it. private const int SortStackAllocThreshold = 32; // Inline-array buffer of SortStackAllocThreshold indices. Preferred over stackalloc because the // compiler puts stronger guarantees on inline arrays (no stack cookie, better bounds analysis). [InlineArray(SortStackAllocThreshold)] private struct IndexBuffer { #pragma warning disable CA1823 // Avoid unused private fields #pragma warning disable IDE0044 // Add readonly modifier #pragma warning disable IDE0051 // Remove unused private members private int _element0; #pragma warning restore IDE0051 // Remove unused private members #pragma warning restore IDE0044 // Add readonly modifier #pragma warning restore CA1823 // Avoid unused private fields } private static IList<StringWithQualityHeaderValue> Sort(IList<StringWithQualityHeaderValue> values) { var sortNeeded = false; for (var i = 0; i < values.Count; i++) { var value = values[i]; if (value.Quality == HeaderQuality.NoMatch) { // Exclude this one } else if (value.Quality != null) { sortNeeded = true; } } if (!sortNeeded) { return values; } return values.Count <= SortStackAllocThreshold ? SortSmall(values) : SortLarge(values); } // Fast path for realistic headers. We insert the surviving indices into a stack-allocated buffer in // descending quality order, filtering out the q=0 rejections as we go. Building descending directly // (rather than sorting ascending and reversing) means an already-preferred-first header appends with // no shifting, so the common case is O(n); the worst case is an O(n^2) insertion sort, which is // irrelevant for at most SortStackAllocThreshold entries. QualityComparer keeps a concrete charset // ahead of an equal-quality wildcard and treats two equal-quality concrete values as equal, so // shifting past equals (Compare <= 0) lands a later header entry ahead of earlier equals, preserving // the previous last-entry-wins selection. private static IList<StringWithQualityHeaderValue> SortSmall(IList<StringWithQualityHeaderValue> values) { var buffer = new IndexBuffer(); Span<int> indices = buffer; var count = 0; for (var i = 0; i < values.Count; i++) { var value = values[i]; if (value.Quality == HeaderQuality.NoMatch) { continue; } var j = count; while (j > 0 && StringWithQualityHeaderValueComparer.QualityComparer.Compare(values[indices[j - 1]], value) <= 0) { indices[j] = indices[j - 1]; j--; } indices[j] = i; count++; } var sorted = new List<StringWithQualityHeaderValue>(count); for (var i = 0; i < count; i++) { sorted.Add(values[indices[i]]); } return sorted; } // Pathological path that no real client will ever exercise: more than SortStackAllocThreshold // charsets in a single Accept-Charset header. We don't allocate an index buffer or preserve the // tie ordering here; a plain List.Sort is unstable, but with this many client-declared charsets // there is nothing observable that could depend on how equal-quality values are ordered. private static IList<StringWithQualityHeaderValue> SortLarge(IList<StringWithQualityHeaderValue> values) { var sorted = new List<StringWithQualityHeaderValue>(values.Count); for (var i = 0; i < values.Count; i++) { var value = values[i]; if (value.Quality != HeaderQuality.NoMatch) { sorted.Add(value); } } // QualityComparer sorts ascending; reverse for the descending order we return. sorted.Sort(StringWithQualityHeaderValueComparer.QualityComparer); sorted.Reverse(); return sorted; } }