|
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
using System.Collections.Generic;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Globalization;
using System.Text;
namespace System.Net.Http.Headers
{
public class ContentDispositionHeaderValue : ICloneable
{
#region Fields
private const string fileName = "filename";
private const string name = "name";
private const string fileNameStar = "filename*";
private const string creationDate = "creation-date";
private const string modificationDate = "modification-date";
private const string readDate = "read-date";
private const string size = "size";
// Use UnvalidatedObjectCollection<T> since we may have multiple parameters with the same name.
private UnvalidatedObjectCollection<NameValueHeaderValue>? _parameters;
private string _dispositionType = null!;
#endregion Fields
#region Properties
public string DispositionType
{
get { return _dispositionType; }
set
{
HeaderUtilities.CheckValidToken(value);
_dispositionType = value;
}
}
public ICollection<NameValueHeaderValue> Parameters => _parameters ??= new UnvalidatedObjectCollection<NameValueHeaderValue>();
public string? Name
{
get { return GetName(name); }
set { SetName(name, value); }
}
public string? FileName
{
get { return GetName(fileName); }
set { SetName(fileName, value); }
}
public string? FileNameStar
{
get { return GetName(fileNameStar); }
set { SetName(fileNameStar, value); }
}
public DateTimeOffset? CreationDate
{
get { return GetDate(creationDate); }
set { SetDate(creationDate, value); }
}
public DateTimeOffset? ModificationDate
{
get { return GetDate(modificationDate); }
set { SetDate(modificationDate, value); }
}
public DateTimeOffset? ReadDate
{
get { return GetDate(readDate); }
set { SetDate(readDate, value); }
}
public long? Size
{
get
{
NameValueHeaderValue? sizeParameter = NameValueHeaderValue.Find(_parameters, size);
ulong value;
if (sizeParameter != null)
{
string? sizeString = sizeParameter.Value;
if (ulong.TryParse(sizeString, NumberStyles.Integer, CultureInfo.InvariantCulture, out value))
{
return (long)value;
}
}
return null;
}
set
{
NameValueHeaderValue? sizeParameter = NameValueHeaderValue.Find(_parameters, size);
if (value == null)
{
// Remove parameter.
if (sizeParameter != null)
{
_parameters!.Remove(sizeParameter);
}
}
else
{
ArgumentOutOfRangeException.ThrowIfNegative(value.GetValueOrDefault());
if (sizeParameter != null)
{
sizeParameter.Value = value.Value.ToString(CultureInfo.InvariantCulture);
}
else
{
string sizeString = value.Value.ToString(CultureInfo.InvariantCulture);
Parameters.Add(new NameValueHeaderValue(size, sizeString));
}
}
}
}
#endregion Properties
#region Constructors
private ContentDispositionHeaderValue()
{
// Used by the parser to create a new instance of this type.
}
protected ContentDispositionHeaderValue(ContentDispositionHeaderValue source)
{
Debug.Assert(source != null);
_dispositionType = source._dispositionType;
_parameters = source._parameters.Clone();
}
public ContentDispositionHeaderValue(string dispositionType)
{
HeaderUtilities.CheckValidToken(dispositionType);
_dispositionType = dispositionType;
}
#endregion Constructors
#region Overloads
public override string ToString()
{
StringBuilder sb = StringBuilderCache.Acquire();
sb.Append(_dispositionType);
NameValueHeaderValue.ToString(_parameters, ';', true, sb);
return StringBuilderCache.GetStringAndRelease(sb);
}
public override bool Equals([NotNullWhen(true)] object? obj)
{
ContentDispositionHeaderValue? other = obj as ContentDispositionHeaderValue;
if (other == null)
{
return false;
}
return string.Equals(_dispositionType, other._dispositionType, StringComparison.OrdinalIgnoreCase) &&
HeaderUtilities.AreEqualCollections(_parameters, other._parameters);
}
public override int GetHashCode()
{
// The dispositionType string is case-insensitive.
return StringComparer.OrdinalIgnoreCase.GetHashCode(_dispositionType) ^ NameValueHeaderValue.GetHashCode(_parameters);
}
// Implement ICloneable explicitly to allow derived types to "override" the implementation.
object ICloneable.Clone()
{
return new ContentDispositionHeaderValue(this);
}
#endregion Overloads
#region Parsing
public static ContentDispositionHeaderValue Parse(string input)
{
int index = 0;
return (ContentDispositionHeaderValue)GenericHeaderParser.ContentDispositionParser.ParseValue(input,
null, ref index);
}
public static bool TryParse([NotNullWhen(true)] string? input, [NotNullWhen(true)] out ContentDispositionHeaderValue? parsedValue)
{
int index = 0;
parsedValue = null;
if (GenericHeaderParser.ContentDispositionParser.TryParseValue(input, null, ref index, out object? output))
{
parsedValue = (ContentDispositionHeaderValue)output!;
return true;
}
return false;
}
internal static int GetDispositionTypeLength(string? input, int startIndex, out object? parsedValue)
{
Debug.Assert(startIndex >= 0);
parsedValue = null;
if (string.IsNullOrEmpty(input) || (startIndex >= input.Length))
{
return 0;
}
// Caller must remove leading whitespace. If not, we'll return 0.
string? dispositionType;
int dispositionTypeLength = GetDispositionTypeExpressionLength(input, startIndex, out dispositionType);
if (dispositionTypeLength == 0)
{
return 0;
}
int current = startIndex + dispositionTypeLength;
current += HttpRuleParser.GetWhitespaceLength(input, current);
ContentDispositionHeaderValue contentDispositionHeader = new ContentDispositionHeaderValue();
contentDispositionHeader._dispositionType = dispositionType!;
// If we're not done and we have a parameter delimiter, then we have a list of parameters.
if ((current < input.Length) && (input[current] == ';'))
{
current++; // Skip delimiter.
int parameterLength = NameValueHeaderValue.GetNameValueListLength(input, current, ';',
(UnvalidatedObjectCollection<NameValueHeaderValue>)contentDispositionHeader.Parameters);
if (parameterLength == 0)
{
return 0;
}
parsedValue = contentDispositionHeader;
return current + parameterLength - startIndex;
}
// We have a ContentDisposition header without parameters.
parsedValue = contentDispositionHeader;
return current - startIndex;
}
private static int GetDispositionTypeExpressionLength(string input, int startIndex, out string? dispositionType)
{
Debug.Assert((input != null) && (input.Length > 0) && (startIndex < input.Length));
// This method just parses the disposition type string, it does not parse parameters.
dispositionType = null;
// Parse the disposition type, i.e. <dispositiontype> in content-disposition string
// "<dispositiontype>; param1=value1; param2=value2".
int typeLength = HttpRuleParser.GetTokenLength(input, startIndex);
if (typeLength == 0)
{
return 0;
}
dispositionType = input.Substring(startIndex, typeLength);
return typeLength;
}
#endregion Parsing
#region Helpers
// Gets a parameter of the given name and attempts to extract a date.
// Returns null if the parameter is not present or the format is incorrect.
private DateTimeOffset? GetDate(string parameter)
{
NameValueHeaderValue? dateParameter = NameValueHeaderValue.Find(_parameters, parameter);
DateTimeOffset date;
if (dateParameter != null)
{
ReadOnlySpan<char> dateString = dateParameter.Value;
// Should have quotes, remove them.
if (IsQuoted(dateString))
{
dateString = dateString.Slice(1, dateString.Length - 2);
}
if (HttpDateParser.TryParse(dateString, out date))
{
return date;
}
}
return null;
}
// Add the given parameter to the list. Remove if date is null.
private void SetDate(string parameter, DateTimeOffset? date)
{
NameValueHeaderValue? dateParameter = NameValueHeaderValue.Find(_parameters, parameter);
if (date == null)
{
// Remove parameter.
if (dateParameter != null)
{
_parameters!.Remove(dateParameter);
}
}
else
{
// Must always be quoted.
string dateString = $"\"{date.GetValueOrDefault():r}\"";
if (dateParameter != null)
{
dateParameter.Value = dateString;
}
else
{
Parameters.Add(new NameValueHeaderValue(parameter, dateString));
}
}
}
// Gets a parameter of the given name and attempts to decode it if necessary.
// Returns null if the parameter is not present or the raw value if the encoding is incorrect.
private string? GetName(string parameter)
{
NameValueHeaderValue? nameParameter = NameValueHeaderValue.Find(_parameters, parameter);
if (nameParameter != null)
{
string? result;
// filename*=utf-8'lang'%7FMyString
if (parameter.EndsWith('*'))
{
Debug.Assert(nameParameter.Value != null);
if (TryDecode5987(nameParameter.Value, out result))
{
return result;
}
return null; // Unrecognized encoding.
}
// filename="=?utf-8?B?BDFSDFasdfasdc==?="
if (TryDecodeMime(nameParameter.Value, out result))
{
return result;
}
// May not have been encoded.
return nameParameter.Value;
}
return null;
}
// Add/update the given parameter in the list, encoding if necessary.
// Remove if value is null/Empty
private void SetName(string parameter, string? value)
{
NameValueHeaderValue? nameParameter = NameValueHeaderValue.Find(_parameters, parameter);
if (string.IsNullOrEmpty(value))
{
// Remove parameter.
if (nameParameter != null)
{
_parameters!.Remove(nameParameter);
}
}
else
{
string processedValue;
if (parameter.EndsWith('*'))
{
processedValue = HeaderUtilities.Encode5987(value);
}
else
{
processedValue = EncodeAndQuoteMime(value);
}
if (nameParameter != null)
{
nameParameter.Value = processedValue;
}
else
{
Parameters.Add(new NameValueHeaderValue(parameter, processedValue));
}
}
}
// Returns input for decoding failures, as the content might not be encoded.
private static string EncodeAndQuoteMime(string input)
{
string result = input;
bool needsQuotes = false;
// Remove bounding quotes, they'll get re-added later.
if (IsQuoted(result))
{
result = result.Substring(1, result.Length - 2);
needsQuotes = true;
}
if (result.Contains('"')) // Only bounding quotes are allowed.
{
throw new ArgumentException(SR.Format(CultureInfo.InvariantCulture,
SR.net_http_headers_invalid_value, input));
}
else if (!Ascii.IsValid(result))
{
needsQuotes = true; // Encoded data must always be quoted, the equals signs are invalid in tokens.
result = EncodeMime(result); // =?utf-8?B?asdfasdfaesdf?=
}
else if (!needsQuotes && HttpRuleParser.GetTokenLength(result, 0) != result.Length)
{
needsQuotes = true;
}
if (needsQuotes)
{
// Re-add quotes "value".
result = "\"" + result + "\"";
}
return result;
}
// Returns true if the value starts and ends with a quote.
private static bool IsQuoted(ReadOnlySpan<char> value)
{
return
value.Length > 1 &&
value[0] == '"' &&
value[value.Length - 1] == '"';
}
// Encode using MIME encoding.
private static string EncodeMime(string input)
{
byte[] buffer = Encoding.UTF8.GetBytes(input);
string encodedName = Convert.ToBase64String(buffer);
return "=?utf-8?B?" + encodedName + "?=";
}
// Attempt to decode MIME encoded strings.
private static bool TryDecodeMime(string? input, [NotNullWhen(true)] out string? output)
{
Debug.Assert(input != null);
output = null;
string? processedInput = input;
// Require quotes, min of "=?e?b??="
if (!IsQuoted(processedInput) || processedInput.Length < 10)
{
return false;
}
Span<Range> parts = stackalloc Range[6];
ReadOnlySpan<char> processedInputSpan = processedInput;
// "=, encodingName, encodingType, encodedData, ="
if (processedInputSpan.Split(parts, '?') != 5 ||
processedInputSpan[parts[0]] is not "\"=" ||
processedInputSpan[parts[4]] is not "=\"" ||
!processedInputSpan[parts[2]].Equals("b", StringComparison.OrdinalIgnoreCase))
{
// Not encoded.
// This does not support multi-line encoding.
// Only base64 encoding is supported, not quoted printable.
return false;
}
try
{
Encoding encoding = Encoding.GetEncoding(processedInput[parts[1]]);
byte[] bytes = Convert.FromBase64String(processedInput[parts[3]]);
output = encoding.GetString(bytes, 0, bytes.Length);
return true;
}
catch (ArgumentException)
{
// Unknown encoding or bad characters.
}
catch (FormatException)
{
// Bad base64 decoding.
}
return false;
}
// Attempt to decode using RFC 5987 encoding.
// encoding'language'my%20string
private static bool TryDecode5987(string input, out string? output)
{
output = null;
int quoteIndex = input.IndexOf('\'');
if (quoteIndex == -1)
{
return false;
}
int lastQuoteIndex = input.LastIndexOf('\'');
if (quoteIndex == lastQuoteIndex || input.IndexOf('\'', quoteIndex + 1) != lastQuoteIndex)
{
return false;
}
string encodingString = input.Substring(0, quoteIndex);
string dataString = input.Substring(lastQuoteIndex + 1);
StringBuilder decoded = new StringBuilder();
try
{
Encoding encoding = Encoding.GetEncoding(encodingString);
byte[] unescapedBytes = new byte[dataString.Length];
int unescapedBytesCount = 0;
for (int index = 0; index < dataString.Length; index++)
{
if (Uri.IsHexEncoding(dataString, index)) // %FF
{
// Unescape and cache bytes, multi-byte characters must be decoded all at once.
unescapedBytes[unescapedBytesCount++] = (byte)Uri.HexUnescape(dataString, ref index);
index--; // HexUnescape did +=3; Offset the for loop's ++
}
else
{
if (unescapedBytesCount > 0)
{
// Decode any previously cached bytes.
decoded.Append(encoding.GetString(unescapedBytes, 0, unescapedBytesCount));
unescapedBytesCount = 0;
}
decoded.Append(dataString[index]); // Normal safe character.
}
}
if (unescapedBytesCount > 0)
{
// Decode any previously cached bytes.
decoded.Append(encoding.GetString(unescapedBytes, 0, unescapedBytesCount));
}
}
catch (ArgumentException)
{
return false; // Unknown encoding or bad characters.
}
output = decoded.ToString();
return true;
}
#endregion Helpers
}
}
|