Implement strongly typed headers.
This commit is contained in:
parent
4377bb24ce
commit
4fb21644fc
|
|
@ -1,7 +1,7 @@
|
|||
|
||||
Microsoft Visual Studio Solution File, Format Version 12.00
|
||||
# Visual Studio 14
|
||||
VisualStudioVersion = 14.0.21916.0
|
||||
VisualStudioVersion = 14.0.22410.0
|
||||
MinimumVisualStudioVersion = 10.0.40219.1
|
||||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{A5A15F1C-885A-452A-A731-B0173DDBD913}"
|
||||
EndProject
|
||||
|
|
@ -33,6 +33,10 @@ Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "Microsoft.AspNet.WebUtiliti
|
|||
EndProject
|
||||
Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "Microsoft.AspNet.WebUtilities.Tests", "test\Microsoft.AspNet.WebUtilities.Tests\Microsoft.AspNet.WebUtilities.Tests.kproj", "{93C10E50-BCBB-4D8E-9492-D46E1396225B}"
|
||||
EndProject
|
||||
Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "Microsoft.Net.Http.Headers", "src\Microsoft.Net.Http.Headers\Microsoft.Net.Http.Headers.kproj", "{60AA2FDB-8121-4826-8D00-9A143FEFAF66}"
|
||||
EndProject
|
||||
Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "Microsoft.Net.Http.Headers.Tests", "test\Microsoft.Net.Http.Headers.Tests\Microsoft.Net.Http.Headers.Tests.kproj", "{E6BB7AD1-BD10-4A23-B780-F4A86ADF00D1}"
|
||||
EndProject
|
||||
Global
|
||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||
Debug|Any CPU = Debug|Any CPU
|
||||
|
|
@ -173,6 +177,30 @@ Global
|
|||
{93C10E50-BCBB-4D8E-9492-D46E1396225B}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU
|
||||
{93C10E50-BCBB-4D8E-9492-D46E1396225B}.Release|Mixed Platforms.Build.0 = Release|Any CPU
|
||||
{93C10E50-BCBB-4D8E-9492-D46E1396225B}.Release|x86.ActiveCfg = Release|Any CPU
|
||||
{60AA2FDB-8121-4826-8D00-9A143FEFAF66}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{60AA2FDB-8121-4826-8D00-9A143FEFAF66}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{60AA2FDB-8121-4826-8D00-9A143FEFAF66}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU
|
||||
{60AA2FDB-8121-4826-8D00-9A143FEFAF66}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU
|
||||
{60AA2FDB-8121-4826-8D00-9A143FEFAF66}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||
{60AA2FDB-8121-4826-8D00-9A143FEFAF66}.Debug|x86.Build.0 = Debug|Any CPU
|
||||
{60AA2FDB-8121-4826-8D00-9A143FEFAF66}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{60AA2FDB-8121-4826-8D00-9A143FEFAF66}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{60AA2FDB-8121-4826-8D00-9A143FEFAF66}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU
|
||||
{60AA2FDB-8121-4826-8D00-9A143FEFAF66}.Release|Mixed Platforms.Build.0 = Release|Any CPU
|
||||
{60AA2FDB-8121-4826-8D00-9A143FEFAF66}.Release|x86.ActiveCfg = Release|Any CPU
|
||||
{60AA2FDB-8121-4826-8D00-9A143FEFAF66}.Release|x86.Build.0 = Release|Any CPU
|
||||
{E6BB7AD1-BD10-4A23-B780-F4A86ADF00D1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{E6BB7AD1-BD10-4A23-B780-F4A86ADF00D1}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{E6BB7AD1-BD10-4A23-B780-F4A86ADF00D1}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU
|
||||
{E6BB7AD1-BD10-4A23-B780-F4A86ADF00D1}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU
|
||||
{E6BB7AD1-BD10-4A23-B780-F4A86ADF00D1}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||
{E6BB7AD1-BD10-4A23-B780-F4A86ADF00D1}.Debug|x86.Build.0 = Debug|Any CPU
|
||||
{E6BB7AD1-BD10-4A23-B780-F4A86ADF00D1}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{E6BB7AD1-BD10-4A23-B780-F4A86ADF00D1}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{E6BB7AD1-BD10-4A23-B780-F4A86ADF00D1}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU
|
||||
{E6BB7AD1-BD10-4A23-B780-F4A86ADF00D1}.Release|Mixed Platforms.Build.0 = Release|Any CPU
|
||||
{E6BB7AD1-BD10-4A23-B780-F4A86ADF00D1}.Release|x86.ActiveCfg = Release|Any CPU
|
||||
{E6BB7AD1-BD10-4A23-B780-F4A86ADF00D1}.Release|x86.Build.0 = Release|Any CPU
|
||||
EndGlobalSection
|
||||
GlobalSection(SolutionProperties) = preSolution
|
||||
HideSolutionNode = FALSE
|
||||
|
|
@ -191,5 +219,7 @@ Global
|
|||
{AE25EF21-7F91-4B86-B73E-AF746821D339} = {F31FF137-390C-49BF-A3BD-7C6ED3597C21}
|
||||
{A2FB7838-0031-4FAD-BA3E-83C30B3AF406} = {A5A15F1C-885A-452A-A731-B0173DDBD913}
|
||||
{93C10E50-BCBB-4D8E-9492-D46E1396225B} = {F31FF137-390C-49BF-A3BD-7C6ED3597C21}
|
||||
{60AA2FDB-8121-4826-8D00-9A143FEFAF66} = {A5A15F1C-885A-452A-A731-B0173DDBD913}
|
||||
{E6BB7AD1-BD10-4A23-B780-F4A86ADF00D1} = {F31FF137-390C-49BF-A3BD-7C6ED3597C21}
|
||||
EndGlobalSection
|
||||
EndGlobal
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<Project ToolsVersion="14.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
|
||||
<PropertyGroup>
|
||||
<VisualStudioVersion Condition="'$(VisualStudioVersion)' == ''">14.0</VisualStudioVersion>
|
||||
|
|
@ -14,4 +14,9 @@
|
|||
<SchemaVersion>2.0</SchemaVersion>
|
||||
</PropertyGroup>
|
||||
<Import Project="$(VSToolsPath)\AspNet\Microsoft.Web.AspNet.targets" Condition="'$(VSToolsPath)' != ''" />
|
||||
</Project>
|
||||
<ProjectExtensions>
|
||||
<VisualStudio>
|
||||
<UserProperties project_1json__JSONSchema="http://www.asp.net/media/4878834/project.json" />
|
||||
</VisualStudio>
|
||||
</ProjectExtensions>
|
||||
</Project>
|
||||
|
|
@ -0,0 +1,202 @@
|
|||
// Copyright (c) Microsoft Open Technologies, Inc. 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.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using Microsoft.AspNet.Http.Headers;
|
||||
using Microsoft.Net.Http.Headers;
|
||||
|
||||
namespace Microsoft.AspNet.Http
|
||||
{
|
||||
public static class HeaderDictionaryTypeExtensions
|
||||
{
|
||||
public static RequestHeaders GetTypedHeaders(this HttpRequest request)
|
||||
{
|
||||
return new RequestHeaders(request.Headers);
|
||||
}
|
||||
|
||||
public static ResponseHeaders GetTypedHeaders(this HttpResponse response)
|
||||
{
|
||||
return new ResponseHeaders(response.Headers);
|
||||
}
|
||||
|
||||
public static DateTimeOffset? GetDate([NotNull] this IHeaderDictionary headers, [NotNull] string name)
|
||||
{
|
||||
return headers.Get<DateTimeOffset?>(name);
|
||||
}
|
||||
|
||||
public static void Set([NotNull] this IHeaderDictionary headers, [NotNull] string name, object value)
|
||||
{
|
||||
if (value == null)
|
||||
{
|
||||
headers.Remove(name);
|
||||
}
|
||||
else
|
||||
{
|
||||
headers[name] = value.ToString();
|
||||
}
|
||||
}
|
||||
|
||||
public static void SetList<T>([NotNull] this IHeaderDictionary headers, [NotNull] string name, IList<T> values)
|
||||
{
|
||||
if (values == null || values.Count == 0)
|
||||
{
|
||||
headers.Remove(name);
|
||||
}
|
||||
else
|
||||
{
|
||||
headers.SetValues(name, values.Select(value => value.ToString()).ToArray());
|
||||
}
|
||||
}
|
||||
|
||||
public static void SetDate([NotNull] this IHeaderDictionary headers, [NotNull] string name, DateTimeOffset? value)
|
||||
{
|
||||
if (value.HasValue)
|
||||
{
|
||||
headers[name] = HeaderUtilities.FormatDate(value.Value);
|
||||
}
|
||||
else
|
||||
{
|
||||
headers.Remove(name);
|
||||
}
|
||||
}
|
||||
|
||||
public static void Append([NotNull] this IHeaderDictionary headers, [NotNull] string name, [NotNull] object value)
|
||||
{
|
||||
headers.Append(name, value.ToString());
|
||||
}
|
||||
|
||||
public static void AppendList<T>([NotNull] this IHeaderDictionary headers, [NotNull] string name, [NotNull] IList<T> values)
|
||||
{
|
||||
headers.AppendValues(name, values.Select(value => value.ToString()).ToArray());
|
||||
}
|
||||
|
||||
private static IDictionary<Type, object> KnownParsers = new Dictionary<Type, object>()
|
||||
{
|
||||
{ typeof(CacheControlHeaderValue), new Func<string, CacheControlHeaderValue>(value => { CacheControlHeaderValue result; return CacheControlHeaderValue.TryParse(value, out result) ? result : null; }) },
|
||||
{ typeof(ContentDispositionHeaderValue), new Func<string, ContentDispositionHeaderValue>(value => { ContentDispositionHeaderValue result; return ContentDispositionHeaderValue.TryParse(value, out result) ? result : null; }) },
|
||||
{ typeof(ContentRangeHeaderValue), new Func<string, ContentRangeHeaderValue>(value => { ContentRangeHeaderValue result; return ContentRangeHeaderValue.TryParse(value, out result) ? result : null; }) },
|
||||
{ typeof(MediaTypeHeaderValue), new Func<string, MediaTypeHeaderValue>(value => { MediaTypeHeaderValue result; return MediaTypeHeaderValue.TryParse(value, out result) ? result : null; }) },
|
||||
{ typeof(RangeConditionHeaderValue), new Func<string, RangeConditionHeaderValue>(value => { RangeConditionHeaderValue result; return RangeConditionHeaderValue.TryParse(value, out result) ? result : null; }) },
|
||||
{ typeof(RangeHeaderValue), new Func<string, RangeHeaderValue>(value => { RangeHeaderValue result; return RangeHeaderValue.TryParse(value, out result) ? result : null; }) },
|
||||
{ typeof(EntityTagHeaderValue), new Func<string, EntityTagHeaderValue>(value => { EntityTagHeaderValue result; return EntityTagHeaderValue.TryParse(value, out result) ? result : null; }) },
|
||||
{ typeof(DateTimeOffset?), new Func<string, DateTimeOffset?>(value => { DateTimeOffset result; return HeaderUtilities.TryParseDate(value, out result) ? result : (DateTimeOffset?)null; }) },
|
||||
{ typeof(long?), new Func<string, long?>(value => { long result; return HeaderUtilities.TryParseInt64(value, out result) ? result : (long?)null; }) },
|
||||
};
|
||||
|
||||
private static IDictionary<Type, object> KnownListParsers = new Dictionary<Type, object>()
|
||||
{
|
||||
{ typeof(MediaTypeHeaderValue), new Func<IList<string>, IList<MediaTypeHeaderValue>>(value => { IList<MediaTypeHeaderValue> result; return MediaTypeHeaderValue.TryParseList(value, out result) ? result : null; }) },
|
||||
{ typeof(StringWithQualityHeaderValue), new Func<IList<string>, IList<StringWithQualityHeaderValue>>(value => { IList<StringWithQualityHeaderValue> result; return StringWithQualityHeaderValue.TryParseList(value, out result) ? result : null; }) },
|
||||
{ typeof(CookieHeaderValue), new Func<IList<string>, IList<CookieHeaderValue>>(value => { IList<CookieHeaderValue> result; return CookieHeaderValue.TryParseList(value, out result) ? result : null; }) },
|
||||
{ typeof(EntityTagHeaderValue), new Func<IList<string>, IList<EntityTagHeaderValue>>(value => { IList<EntityTagHeaderValue> result; return EntityTagHeaderValue.TryParseList(value, out result) ? result : null; }) },
|
||||
{ typeof(SetCookieHeaderValue), new Func<IList<string>, IList<SetCookieHeaderValue>>(value => { IList<SetCookieHeaderValue> result; return SetCookieHeaderValue.TryParseList(value, out result) ? result : null; }) },
|
||||
};
|
||||
|
||||
public static T Get<T>([NotNull] this IHeaderDictionary headers, string name)
|
||||
{
|
||||
object temp;
|
||||
if (KnownParsers.TryGetValue(typeof(T), out temp))
|
||||
{
|
||||
var func = (Func<string, T>)temp;
|
||||
return func(headers[name]);
|
||||
}
|
||||
|
||||
var value = headers[name];
|
||||
if (string.IsNullOrWhiteSpace(value))
|
||||
{
|
||||
return default(T);
|
||||
}
|
||||
|
||||
return GetViaReflection<T>(value);
|
||||
}
|
||||
|
||||
public static IList<T> GetList<T>([NotNull] this IHeaderDictionary headers, string name)
|
||||
{
|
||||
object temp;
|
||||
if (KnownListParsers.TryGetValue(typeof(T), out temp))
|
||||
{
|
||||
var func = (Func<IList<string>, IList<T>>)temp;
|
||||
return func(headers.GetValues(name));
|
||||
}
|
||||
|
||||
var values = headers.GetValues(name);
|
||||
if (values == null || !values.Any())
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return GetListViaReflection<T>(values);
|
||||
}
|
||||
|
||||
private static T GetViaReflection<T>(string value)
|
||||
{
|
||||
// TODO: Cache the reflected type for later? Only if success?
|
||||
var type = typeof(T);
|
||||
var method = type.GetMethods(BindingFlags.Public | BindingFlags.Static)
|
||||
.Where(methodInfo =>
|
||||
{
|
||||
if (string.Equals("TryParse", methodInfo.Name, StringComparison.Ordinal)
|
||||
&& methodInfo.ReturnParameter.ParameterType.Equals(typeof(bool)))
|
||||
{
|
||||
var methodParams = methodInfo.GetParameters();
|
||||
return methodParams.Length == 2
|
||||
&& methodParams[0].ParameterType.Equals(typeof(string))
|
||||
&& methodParams[1].IsOut
|
||||
&& methodParams[1].ParameterType.Equals(type.MakeByRefType());
|
||||
}
|
||||
return false;
|
||||
}).FirstOrDefault();
|
||||
|
||||
if (method == null)
|
||||
{
|
||||
throw new NotSupportedException(string.Format(
|
||||
"The given type '{0}' does not have a TryParse method with the required signature 'public static bool TryParse(string, out {0}).", nameof(T)));
|
||||
}
|
||||
|
||||
var parameters = new object[] { value, null };
|
||||
var success = (bool)method.Invoke(null, parameters);
|
||||
if (success)
|
||||
{
|
||||
return (T)parameters[1];
|
||||
}
|
||||
return default(T);
|
||||
}
|
||||
|
||||
private static IList<T> GetListViaReflection<T>(IList<string> values)
|
||||
{
|
||||
// TODO: Cache the reflected type for later? Only if success?
|
||||
var type = typeof(T);
|
||||
var method = type.GetMethods(BindingFlags.Public | BindingFlags.Static)
|
||||
.Where(methodInfo =>
|
||||
{
|
||||
if (string.Equals("TryParseList", methodInfo.Name, StringComparison.Ordinal)
|
||||
&& methodInfo.ReturnParameter.ParameterType.Equals(typeof(Boolean)))
|
||||
{
|
||||
var methodParams = methodInfo.GetParameters();
|
||||
return methodParams.Length == 2
|
||||
&& methodParams[0].ParameterType.Equals(typeof(IList<string>))
|
||||
&& methodParams[1].IsOut
|
||||
&& methodParams[1].ParameterType.Equals(typeof(IList<T>).MakeByRefType());
|
||||
}
|
||||
return false;
|
||||
}).FirstOrDefault();
|
||||
|
||||
if (method == null)
|
||||
{
|
||||
throw new NotSupportedException(string.Format(
|
||||
"The given type '{0}' does not have a TryParseList method with the required signature 'public static bool TryParseList(IList<string>, out IList<{0}>).", nameof(T)));
|
||||
}
|
||||
|
||||
var parameters = new object[] { values, null };
|
||||
var success = (bool)method.Invoke(null, parameters);
|
||||
if (success)
|
||||
{
|
||||
return (IList<T>)parameters[1];
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<Project ToolsVersion="14.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
|
||||
<PropertyGroup>
|
||||
<VisualStudioVersion Condition="'$(VisualStudioVersion)' == ''">14.0</VisualStudioVersion>
|
||||
|
|
@ -14,4 +14,9 @@
|
|||
<SchemaVersion>2.0</SchemaVersion>
|
||||
</PropertyGroup>
|
||||
<Import Project="$(VSToolsPath)\AspNet\Microsoft.Web.AspNet.targets" Condition="'$(VSToolsPath)' != ''" />
|
||||
</Project>
|
||||
<ProjectExtensions>
|
||||
<VisualStudio>
|
||||
<UserProperties project_1json__JSONSchema="http://www.asp.net/media/4878834/project.json" />
|
||||
</VisualStudio>
|
||||
</ProjectExtensions>
|
||||
</Project>
|
||||
|
|
@ -5,9 +5,8 @@ using System;
|
|||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
using Microsoft.AspNet.Http;
|
||||
|
||||
namespace Microsoft.AspNet.WebUtilities
|
||||
namespace Microsoft.AspNet.Http.Extensions
|
||||
{
|
||||
// The IEnumerable interface is required for the collection initialization syntax: new QueryBuilder() { { "key", "value" } };
|
||||
public class QueryBuilder : IEnumerable<KeyValuePair<string, string>>
|
||||
|
|
@ -0,0 +1,259 @@
|
|||
// Copyright (c) Microsoft Open Technologies, Inc. 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.Collections.Generic;
|
||||
using Microsoft.Net.Http.Headers;
|
||||
|
||||
namespace Microsoft.AspNet.Http.Headers
|
||||
{
|
||||
public class RequestHeaders
|
||||
{
|
||||
public RequestHeaders([NotNull] IHeaderDictionary headers)
|
||||
{
|
||||
Headers = headers;
|
||||
}
|
||||
|
||||
public IHeaderDictionary Headers { get; private set; }
|
||||
|
||||
public IList<MediaTypeHeaderValue> Accept
|
||||
{
|
||||
get
|
||||
{
|
||||
return Headers.GetList<MediaTypeHeaderValue>(HeaderNames.Accept);
|
||||
}
|
||||
set
|
||||
{
|
||||
Headers.SetList(HeaderNames.Accept, value);
|
||||
}
|
||||
}
|
||||
|
||||
public IList<StringWithQualityHeaderValue> AcceptCharset
|
||||
{
|
||||
get
|
||||
{
|
||||
return Headers.GetList<StringWithQualityHeaderValue>(HeaderNames.AcceptCharset);
|
||||
}
|
||||
set
|
||||
{
|
||||
Headers.SetList(HeaderNames.AcceptCharset, value);
|
||||
}
|
||||
}
|
||||
|
||||
public IList<StringWithQualityHeaderValue> AcceptEncoding
|
||||
{
|
||||
get
|
||||
{
|
||||
return Headers.GetList<StringWithQualityHeaderValue>(HeaderNames.AcceptEncoding);
|
||||
}
|
||||
set
|
||||
{
|
||||
Headers.SetList(HeaderNames.AcceptEncoding, value);
|
||||
}
|
||||
}
|
||||
|
||||
public IList<StringWithQualityHeaderValue> AcceptLanguage
|
||||
{
|
||||
get
|
||||
{
|
||||
return Headers.GetList<StringWithQualityHeaderValue>(HeaderNames.AcceptLanguage);
|
||||
}
|
||||
set
|
||||
{
|
||||
Headers.SetList(HeaderNames.AcceptLanguage, value);
|
||||
}
|
||||
}
|
||||
|
||||
public CacheControlHeaderValue CacheControl
|
||||
{
|
||||
get
|
||||
{
|
||||
return Headers.Get<CacheControlHeaderValue>(HeaderNames.CacheControl);
|
||||
}
|
||||
set
|
||||
{
|
||||
Headers.Set(HeaderNames.CacheControl, value);
|
||||
}
|
||||
}
|
||||
|
||||
public ContentDispositionHeaderValue ContentDisposition
|
||||
{
|
||||
get
|
||||
{
|
||||
return Headers.Get<ContentDispositionHeaderValue>(HeaderNames.ContentDisposition);
|
||||
}
|
||||
set
|
||||
{
|
||||
Headers.Set(HeaderNames.ContentDisposition, value);
|
||||
}
|
||||
}
|
||||
|
||||
public long? ContentLength
|
||||
{
|
||||
get
|
||||
{
|
||||
return Headers.Get<long?>(HeaderNames.ContentLength);
|
||||
}
|
||||
set
|
||||
{
|
||||
Headers.Set(HeaderNames.ContentLength, value.HasValue ? HeaderUtilities.FormatInt64(value.Value) : null);
|
||||
}
|
||||
}
|
||||
|
||||
public ContentRangeHeaderValue ContentRange
|
||||
{
|
||||
get
|
||||
{
|
||||
return Headers.Get<ContentRangeHeaderValue>(HeaderNames.ContentRange);
|
||||
}
|
||||
set
|
||||
{
|
||||
Headers.Set(HeaderNames.ContentRange, value);
|
||||
}
|
||||
}
|
||||
|
||||
public MediaTypeHeaderValue ContentType
|
||||
{
|
||||
get
|
||||
{
|
||||
return Headers.Get<MediaTypeHeaderValue>(HeaderNames.ContentType);
|
||||
}
|
||||
set
|
||||
{
|
||||
Headers.Set(HeaderNames.ContentType, value);
|
||||
}
|
||||
}
|
||||
|
||||
public IList<CookieHeaderValue> Cookie
|
||||
{
|
||||
get
|
||||
{
|
||||
return Headers.GetList<CookieHeaderValue>(HeaderNames.Cookie);
|
||||
}
|
||||
set
|
||||
{
|
||||
Headers.SetList(HeaderNames.Cookie, value);
|
||||
}
|
||||
}
|
||||
|
||||
public DateTimeOffset? Date
|
||||
{
|
||||
get
|
||||
{
|
||||
return Headers.GetDate(HeaderNames.Date);
|
||||
}
|
||||
set
|
||||
{
|
||||
Headers.SetDate(HeaderNames.Date, value);
|
||||
}
|
||||
}
|
||||
|
||||
public DateTimeOffset? Expires
|
||||
{
|
||||
get
|
||||
{
|
||||
return Headers.GetDate(HeaderNames.Expires);
|
||||
}
|
||||
set
|
||||
{
|
||||
Headers.SetDate(HeaderNames.Expires, value);
|
||||
}
|
||||
}
|
||||
|
||||
public HostString Host
|
||||
{
|
||||
get
|
||||
{
|
||||
return HostString.FromUriComponent(Headers[HeaderNames.Host]);
|
||||
}
|
||||
set
|
||||
{
|
||||
Headers[HeaderNames.Host] = value.ToUriComponent();
|
||||
}
|
||||
}
|
||||
|
||||
public IList<EntityTagHeaderValue> IfMatch
|
||||
{
|
||||
get
|
||||
{
|
||||
return Headers.GetList<EntityTagHeaderValue>(HeaderNames.IfMatch);
|
||||
}
|
||||
set
|
||||
{
|
||||
Headers.SetList(HeaderNames.IfMatch, value);
|
||||
}
|
||||
}
|
||||
|
||||
public DateTimeOffset? IfModifiedSince
|
||||
{
|
||||
get
|
||||
{
|
||||
return Headers.GetDate(HeaderNames.IfModifiedSince);
|
||||
}
|
||||
set
|
||||
{
|
||||
Headers.SetDate(HeaderNames.IfModifiedSince, value);
|
||||
}
|
||||
}
|
||||
|
||||
public IList<EntityTagHeaderValue> IfNoneMatch
|
||||
{
|
||||
get
|
||||
{
|
||||
return Headers.GetList<EntityTagHeaderValue>(HeaderNames.IfNoneMatch);
|
||||
}
|
||||
set
|
||||
{
|
||||
Headers.SetList(HeaderNames.IfNoneMatch, value);
|
||||
}
|
||||
}
|
||||
|
||||
public RangeConditionHeaderValue IfRange
|
||||
{
|
||||
get
|
||||
{
|
||||
return Headers.Get<RangeConditionHeaderValue>(HeaderNames.IfRange);
|
||||
}
|
||||
set
|
||||
{
|
||||
Headers.Set(HeaderNames.IfRange, value);
|
||||
}
|
||||
}
|
||||
|
||||
public DateTimeOffset? IfUnmodifiedSince
|
||||
{
|
||||
get
|
||||
{
|
||||
return Headers.GetDate(HeaderNames.IfUnmodifiedSince);
|
||||
}
|
||||
set
|
||||
{
|
||||
Headers.SetDate(HeaderNames.IfUnmodifiedSince, value);
|
||||
}
|
||||
}
|
||||
|
||||
public DateTimeOffset? LastModified
|
||||
{
|
||||
get
|
||||
{
|
||||
return Headers.GetDate(HeaderNames.LastModified);
|
||||
}
|
||||
set
|
||||
{
|
||||
Headers.SetDate(HeaderNames.LastModified, value);
|
||||
}
|
||||
}
|
||||
|
||||
public RangeHeaderValue Range
|
||||
{
|
||||
get
|
||||
{
|
||||
return Headers.Get<RangeHeaderValue>(HeaderNames.Range);
|
||||
}
|
||||
set
|
||||
{
|
||||
Headers.Set(HeaderNames.Range, value);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,156 @@
|
|||
// Copyright (c) Microsoft Open Technologies, Inc. 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.Collections.Generic;
|
||||
using Microsoft.AspNet.Http.Extensions;
|
||||
using Microsoft.Net.Http.Headers;
|
||||
|
||||
namespace Microsoft.AspNet.Http.Headers
|
||||
{
|
||||
public class ResponseHeaders
|
||||
{
|
||||
public ResponseHeaders([NotNull] IHeaderDictionary headers)
|
||||
{
|
||||
Headers = headers;
|
||||
}
|
||||
|
||||
public IHeaderDictionary Headers { get; private set; }
|
||||
|
||||
public CacheControlHeaderValue CacheControl
|
||||
{
|
||||
get
|
||||
{
|
||||
return Headers.Get<CacheControlHeaderValue>(HeaderNames.CacheControl);
|
||||
}
|
||||
set
|
||||
{
|
||||
Headers.Set(HeaderNames.CacheControl, value);
|
||||
}
|
||||
}
|
||||
|
||||
public ContentDispositionHeaderValue ContentDisposition
|
||||
{
|
||||
get
|
||||
{
|
||||
return Headers.Get<ContentDispositionHeaderValue>(HeaderNames.ContentDisposition);
|
||||
}
|
||||
set
|
||||
{
|
||||
Headers.Set(HeaderNames.ContentDisposition, value);
|
||||
}
|
||||
}
|
||||
|
||||
public long? ContentLength
|
||||
{
|
||||
get
|
||||
{
|
||||
return Headers.Get<long?>(HeaderNames.ContentLength);
|
||||
}
|
||||
set
|
||||
{
|
||||
Headers.Set(HeaderNames.ContentLength, value.HasValue ? HeaderUtilities.FormatInt64(value.Value) : null);
|
||||
}
|
||||
}
|
||||
|
||||
public ContentRangeHeaderValue ContentRange
|
||||
{
|
||||
get
|
||||
{
|
||||
return Headers.Get<ContentRangeHeaderValue>(HeaderNames.ContentRange);
|
||||
}
|
||||
set
|
||||
{
|
||||
Headers.Set(HeaderNames.ContentRange, value);
|
||||
}
|
||||
}
|
||||
|
||||
public MediaTypeHeaderValue ContentType
|
||||
{
|
||||
get
|
||||
{
|
||||
return Headers.Get<MediaTypeHeaderValue>(HeaderNames.ContentType);
|
||||
}
|
||||
set
|
||||
{
|
||||
Headers.Set(HeaderNames.ContentType, value);
|
||||
}
|
||||
}
|
||||
|
||||
public DateTimeOffset? Date
|
||||
{
|
||||
get
|
||||
{
|
||||
return Headers.GetDate(HeaderNames.Date);
|
||||
}
|
||||
set
|
||||
{
|
||||
Headers.SetDate(HeaderNames.Date, value);
|
||||
}
|
||||
}
|
||||
|
||||
public EntityTagHeaderValue ETag
|
||||
{
|
||||
get
|
||||
{
|
||||
return Headers.Get<EntityTagHeaderValue>(HeaderNames.ETag);
|
||||
}
|
||||
set
|
||||
{
|
||||
Headers.Set(HeaderNames.ETag, value);
|
||||
}
|
||||
}
|
||||
public DateTimeOffset? Expires
|
||||
{
|
||||
get
|
||||
{
|
||||
return Headers.GetDate(HeaderNames.Expires);
|
||||
}
|
||||
set
|
||||
{
|
||||
Headers.SetDate(HeaderNames.Expires, value);
|
||||
}
|
||||
}
|
||||
|
||||
public DateTimeOffset? LastModified
|
||||
{
|
||||
get
|
||||
{
|
||||
return Headers.GetDate(HeaderNames.LastModified);
|
||||
}
|
||||
set
|
||||
{
|
||||
Headers.SetDate(HeaderNames.LastModified, value);
|
||||
}
|
||||
}
|
||||
|
||||
public UriHelper Location
|
||||
{
|
||||
get
|
||||
{
|
||||
Uri uri;
|
||||
if (Uri.TryCreate(Headers[HeaderNames.Location], UriKind.RelativeOrAbsolute, out uri))
|
||||
{
|
||||
return new UriHelper(uri);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
set
|
||||
{
|
||||
Headers.Set(HeaderNames.Location, value);
|
||||
}
|
||||
}
|
||||
|
||||
public IList<SetCookieHeaderValue> SetCookie
|
||||
{
|
||||
get
|
||||
{
|
||||
return Headers.GetList<SetCookieHeaderValue>(HeaderNames.SetCookie);
|
||||
}
|
||||
set
|
||||
{
|
||||
Headers.SetList(HeaderNames.SetCookie, value);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,7 +1,9 @@
|
|||
using System;
|
||||
using Microsoft.AspNet.Http;
|
||||
// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved.
|
||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
namespace Microsoft.AspNet.WebUtilities
|
||||
using System;
|
||||
|
||||
namespace Microsoft.AspNet.Http.Extensions
|
||||
{
|
||||
/// <summary>
|
||||
/// A helper class for constructing encoded Uris for use in headers and other Uris.
|
||||
|
|
@ -44,6 +46,11 @@ namespace Microsoft.AspNet.WebUtilities
|
|||
|
||||
public FragmentString Fragment { get; set; }
|
||||
|
||||
public bool IsFullUri
|
||||
{
|
||||
get { return !string.IsNullOrEmpty(Scheme) && Host.HasValue; }
|
||||
}
|
||||
|
||||
// Always returns at least '/'
|
||||
public string GetPartialUri()
|
||||
{
|
||||
|
|
@ -67,6 +74,11 @@ namespace Microsoft.AspNet.WebUtilities
|
|||
return Scheme + "://" + Host + path + Query + Fragment;
|
||||
}
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
return IsFullUri ? GetFullUri() : GetPartialUri();
|
||||
}
|
||||
|
||||
public static string Create(PathString pathBase,
|
||||
PathString path = new PathString(),
|
||||
QueryString query = new QueryString(),
|
||||
|
|
@ -3,7 +3,8 @@
|
|||
"description": "ASP.NET 5 common extension methods for HTTP abstractions and IApplicationBuilder.",
|
||||
"dependencies": {
|
||||
"Microsoft.AspNet.Http": "1.0.0-*",
|
||||
"Microsoft.Framework.DependencyInjection": "1.0.0-*"
|
||||
"Microsoft.Framework.DependencyInjection": "1.0.0-*",
|
||||
"Microsoft.Net.Http.Headers": "1.0.0-*"
|
||||
},
|
||||
"frameworks" : {
|
||||
"aspnet50" : {
|
||||
|
|
|
|||
|
|
@ -93,30 +93,6 @@ namespace Microsoft.AspNet.Http
|
|||
/// <returns>The Content-Type header.</returns>
|
||||
public abstract string ContentType { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the Cache-Control header.
|
||||
/// </summary>
|
||||
/// <returns>The Cache-Control header.</returns>
|
||||
// (TODO header conventions?) public abstract string CacheControl { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the Media-Type header.
|
||||
/// </summary>
|
||||
/// <returns>The Media-Type header.</returns>
|
||||
// (TODO header conventions?) public abstract string MediaType { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or set the Accept header.
|
||||
/// </summary>
|
||||
/// <returns>The Accept header.</returns>
|
||||
public abstract string Accept { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or set the Accept-Charset header.
|
||||
/// </summary>
|
||||
/// <returns>The Accept-Charset header.</returns>
|
||||
public abstract string AcceptCharset { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or set the owin.RequestBody Stream.
|
||||
/// </summary>
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<Project ToolsVersion="14.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
|
||||
<PropertyGroup>
|
||||
<VisualStudioVersion Condition="'$(VisualStudioVersion)' == ''">14.0</VisualStudioVersion>
|
||||
|
|
@ -14,4 +14,9 @@
|
|||
<SchemaVersion>2.0</SchemaVersion>
|
||||
</PropertyGroup>
|
||||
<Import Project="$(VSToolsPath)\AspNet\Microsoft.Web.AspNet.targets" Condition="'$(VSToolsPath)' != ''" />
|
||||
</Project>
|
||||
<ProjectExtensions>
|
||||
<VisualStudio>
|
||||
<UserProperties project_1json__JSONSchema="http://www.asp.net/media/4878834/project.json" />
|
||||
</VisualStudio>
|
||||
</ProjectExtensions>
|
||||
</Project>
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<Project ToolsVersion="14.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
|
||||
<PropertyGroup>
|
||||
<VisualStudioVersion Condition="'$(VisualStudioVersion)' == ''">14.0</VisualStudioVersion>
|
||||
|
|
@ -14,4 +14,9 @@
|
|||
<SchemaVersion>2.0</SchemaVersion>
|
||||
</PropertyGroup>
|
||||
<Import Project="$(VSToolsPath)\AspNet\Microsoft.Web.AspNet.targets" Condition="'$(VSToolsPath)' != ''" />
|
||||
</Project>
|
||||
<ProjectExtensions>
|
||||
<VisualStudio>
|
||||
<UserProperties project_1json__JSONSchema="http://www.asp.net/media/4878834/project.json" />
|
||||
</VisualStudio>
|
||||
</ProjectExtensions>
|
||||
</Project>
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<Project ToolsVersion="14.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
|
||||
<PropertyGroup>
|
||||
<VisualStudioVersion Condition="'$(VisualStudioVersion)' == ''">14.0</VisualStudioVersion>
|
||||
|
|
@ -14,4 +14,9 @@
|
|||
<SchemaVersion>2.0</SchemaVersion>
|
||||
</PropertyGroup>
|
||||
<Import Project="$(VSToolsPath)\AspNet\Microsoft.Web.AspNet.targets" Condition="'$(VSToolsPath)' != ''" />
|
||||
</Project>
|
||||
<ProjectExtensions>
|
||||
<VisualStudio>
|
||||
<UserProperties project_1json__JSONSchema="http://www.asp.net/media/4878834/project.json" />
|
||||
</VisualStudio>
|
||||
</ProjectExtensions>
|
||||
</Project>
|
||||
|
|
@ -294,7 +294,7 @@ namespace Microsoft.AspNet.Owin
|
|||
public bool ContainsKey(Type key)
|
||||
{
|
||||
// Does this type implement the requested interface?
|
||||
if (key.GetTypeInfo().IsAssignableFrom(this.GetType().GetTypeInfo()))
|
||||
if (key.GetTypeInfo().IsAssignableFrom(GetType().GetTypeInfo()))
|
||||
{
|
||||
// Check for conditional features
|
||||
if (key == typeof(IHttpSendFileFeature))
|
||||
|
|
|
|||
|
|
@ -1,9 +1,9 @@
|
|||
// Copyright (c) Microsoft Open Technologies, Inc. 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.Collections.Generic;
|
||||
using Microsoft.AspNet.Http;
|
||||
using Microsoft.Net.Http.Headers;
|
||||
|
||||
namespace Microsoft.AspNet.PipelineCore.Collections
|
||||
{
|
||||
|
|
@ -26,11 +26,10 @@ namespace Microsoft.AspNet.PipelineCore.Collections
|
|||
|
||||
private static string GetName(string contentDisposition)
|
||||
{
|
||||
// TODO: Strongly typed headers will take care of this
|
||||
// Content-Disposition: form-data; name="myfile1"; filename="Misc 002.jpg"
|
||||
var offset = contentDisposition.IndexOf("name=\"") + "name=\"".Length;
|
||||
var key = contentDisposition.Substring(offset, contentDisposition.IndexOf("\"", offset) - offset); // Remove quotes
|
||||
return key;
|
||||
ContentDispositionHeaderValue cd;
|
||||
ContentDispositionHeaderValue.TryParse(contentDisposition, out cd);
|
||||
return HeaderUtilities.RemoveQuotes(cd?.Name);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -16,6 +16,10 @@ namespace Microsoft.AspNet.PipelineCore.Collections
|
|||
/// </summary>
|
||||
public class HeaderDictionary : IHeaderDictionary
|
||||
{
|
||||
public HeaderDictionary() : this(new Dictionary<string, string[]>(StringComparer.OrdinalIgnoreCase))
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="T:Microsoft.Owin.HeaderDictionary" /> class.
|
||||
/// </summary>
|
||||
|
|
|
|||
|
|
@ -150,18 +150,6 @@ namespace Microsoft.AspNet.PipelineCore
|
|||
set { Headers[Constants.Headers.ContentType] = value; }
|
||||
}
|
||||
|
||||
public override string Accept
|
||||
{
|
||||
get { return Headers[Constants.Headers.Accept]; }
|
||||
set { Headers[Constants.Headers.Accept] = value; }
|
||||
}
|
||||
|
||||
public override string AcceptCharset
|
||||
{
|
||||
get { return Headers[Constants.Headers.AcceptCharset]; }
|
||||
set { Headers[Constants.Headers.AcceptCharset] = value; }
|
||||
}
|
||||
|
||||
public override bool HasFormContentType
|
||||
{
|
||||
get { return FormFeature.HasFormContentType; }
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ using System.Threading.Tasks;
|
|||
using Microsoft.AspNet.Http;
|
||||
using Microsoft.AspNet.PipelineCore.Collections;
|
||||
using Microsoft.AspNet.WebUtilities;
|
||||
using Microsoft.Net.Http.Headers;
|
||||
|
||||
namespace Microsoft.AspNet.PipelineCore
|
||||
{
|
||||
|
|
@ -28,6 +29,16 @@ namespace Microsoft.AspNet.PipelineCore
|
|||
_request = request;
|
||||
}
|
||||
|
||||
private MediaTypeHeaderValue ContentType
|
||||
{
|
||||
get
|
||||
{
|
||||
MediaTypeHeaderValue mt;
|
||||
MediaTypeHeaderValue.TryParse(_request.ContentType, out mt);
|
||||
return mt;
|
||||
}
|
||||
}
|
||||
|
||||
public bool HasFormContentType
|
||||
{
|
||||
get
|
||||
|
|
@ -38,7 +49,8 @@ namespace Microsoft.AspNet.PipelineCore
|
|||
return true;
|
||||
}
|
||||
|
||||
return HasApplicationFormContentType() || HasMultipartFormContentType();
|
||||
var conentType = ContentType;
|
||||
return HasApplicationFormContentType(conentType) || HasMultipartFormContentType(conentType);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -82,23 +94,25 @@ namespace Microsoft.AspNet.PipelineCore
|
|||
// Some of these code paths use StreamReader which does not support cancellation tokens.
|
||||
using (cancellationToken.Register(_request.HttpContext.Abort))
|
||||
{
|
||||
var contentType = ContentType;
|
||||
// Check the content-type
|
||||
if (HasApplicationFormContentType())
|
||||
if (HasApplicationFormContentType(contentType))
|
||||
{
|
||||
// TODO: Read the charset from the content-type header after we get strongly typed headers
|
||||
formFields = await FormReader.ReadFormAsync(_request.Body, cancellationToken);
|
||||
var encoding = FilterEncoding(contentType.Encoding);
|
||||
formFields = await FormReader.ReadFormAsync(_request.Body, encoding, cancellationToken);
|
||||
}
|
||||
else if (HasMultipartFormContentType())
|
||||
else if (HasMultipartFormContentType(contentType))
|
||||
{
|
||||
var formAccumulator = new KeyValueAccumulator<string, string>(StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
var boundary = GetBoundary(_request.ContentType);
|
||||
var boundary = GetBoundary(contentType);
|
||||
var multipartReader = new MultipartReader(boundary, _request.Body);
|
||||
var section = await multipartReader.ReadNextSectionAsync(cancellationToken);
|
||||
while (section != null)
|
||||
{
|
||||
var headers = new HeaderDictionary(section.Headers);
|
||||
var contentDisposition = headers["Content-Disposition"];
|
||||
ContentDispositionHeaderValue contentDisposition;
|
||||
ContentDispositionHeaderValue.TryParse(headers.Get(HeaderNames.ContentDisposition), out contentDisposition);
|
||||
if (HasFileContentDisposition(contentDisposition))
|
||||
{
|
||||
// Find the end
|
||||
|
|
@ -116,12 +130,11 @@ namespace Microsoft.AspNet.PipelineCore
|
|||
//
|
||||
// value
|
||||
|
||||
// TODO: Strongly typed headers will take care of this
|
||||
var offset = contentDisposition.IndexOf("name=") + "name=".Length;
|
||||
var key = contentDisposition.Substring(offset + 1, contentDisposition.Length - offset - 2); // Remove quotes
|
||||
|
||||
// TODO: Read the charset from the content-disposition header after we get strongly typed headers
|
||||
using (var reader = new StreamReader(section.Body, Encoding.UTF8, detectEncodingFromByteOrderMarks: true, bufferSize: 1024, leaveOpen: true))
|
||||
var key = HeaderUtilities.RemoveQuotes(contentDisposition.Name);
|
||||
MediaTypeHeaderValue mediaType;
|
||||
MediaTypeHeaderValue.TryParse(headers.Get(HeaderNames.ContentType), out mediaType);
|
||||
var encoding = FilterEncoding(mediaType?.Encoding);
|
||||
using (var reader = new StreamReader(section.Body, encoding, detectEncodingFromByteOrderMarks: true, bufferSize: 1024, leaveOpen: true))
|
||||
{
|
||||
var value = await reader.ReadToEndAsync();
|
||||
formAccumulator.Append(key, value);
|
||||
|
|
@ -129,7 +142,7 @@ namespace Microsoft.AspNet.PipelineCore
|
|||
}
|
||||
else
|
||||
{
|
||||
System.Diagnostics.Debug.Assert(false, "Unrecognized content-disposition for this section: " + contentDisposition);
|
||||
System.Diagnostics.Debug.Assert(false, "Unrecognized content-disposition for this section: " + headers.Get(HeaderNames.ContentDisposition));
|
||||
}
|
||||
|
||||
section = await multipartReader.ReadNextSectionAsync(cancellationToken);
|
||||
|
|
@ -143,48 +156,50 @@ namespace Microsoft.AspNet.PipelineCore
|
|||
return Form;
|
||||
}
|
||||
|
||||
private bool HasApplicationFormContentType()
|
||||
private Encoding FilterEncoding(Encoding encoding)
|
||||
{
|
||||
// TODO: Strongly typed headers will take care of this for us
|
||||
// Content-Type: application/x-www-form-urlencoded; charset=utf-8
|
||||
var contentType = _request.ContentType;
|
||||
return !string.IsNullOrEmpty(contentType) && contentType.IndexOf("application/x-www-form-urlencoded", StringComparison.OrdinalIgnoreCase) >= 0;
|
||||
}
|
||||
|
||||
private bool HasMultipartFormContentType()
|
||||
{
|
||||
// TODO: Strongly typed headers will take care of this for us
|
||||
// Content-Type: multipart/form-data; boundary=----WebKitFormBoundarymx2fSWqWSd0OxQqq
|
||||
var contentType = _request.ContentType;
|
||||
return !string.IsNullOrEmpty(contentType) && contentType.IndexOf("multipart/form-data", StringComparison.OrdinalIgnoreCase) >= 0;
|
||||
}
|
||||
|
||||
private bool HasFormDataContentDisposition(string contentDisposition)
|
||||
{
|
||||
// TODO: Strongly typed headers will take care of this for us
|
||||
// Content-Disposition: form-data; name="key";
|
||||
return !string.IsNullOrEmpty(contentDisposition) && contentDisposition.Contains("form-data") && !contentDisposition.Contains("filename=");
|
||||
}
|
||||
|
||||
private bool HasFileContentDisposition(string contentDisposition)
|
||||
{
|
||||
// TODO: Strongly typed headers will take care of this for us
|
||||
// Content-Disposition: form-data; name="myfile1"; filename="Misc 002.jpg"
|
||||
return !string.IsNullOrEmpty(contentDisposition) && contentDisposition.Contains("form-data") && contentDisposition.Contains("filename=");
|
||||
}
|
||||
|
||||
// Content-Type: multipart/form-data; boundary=----WebKitFormBoundarymx2fSWqWSd0OxQqq
|
||||
private static string GetBoundary(string contentType)
|
||||
{
|
||||
// TODO: Strongly typed headers will take care of this for us
|
||||
// TODO: Limit the length of boundary we accept. The spec says ~70 chars.
|
||||
var elements = contentType.Split(' ');
|
||||
var element = elements.Where(entry => entry.StartsWith("boundary=")).First();
|
||||
var boundary = element.Substring("boundary=".Length);
|
||||
// Remove quotes
|
||||
if (boundary.Length >= 2 && boundary[0] == '"' && boundary[boundary.Length - 1] == '"')
|
||||
// UTF-7 is insecure and should not be honored. UTF-8 will succeed for most cases.
|
||||
if (encoding == null || Encoding.UTF7.Equals(encoding))
|
||||
{
|
||||
boundary = boundary.Substring(1, boundary.Length - 2);
|
||||
return Encoding.UTF8;
|
||||
}
|
||||
return encoding;
|
||||
}
|
||||
|
||||
private bool HasApplicationFormContentType(MediaTypeHeaderValue contentType)
|
||||
{
|
||||
// Content-Type: application/x-www-form-urlencoded; charset=utf-8
|
||||
return contentType != null && contentType.MediaType.Equals("application/x-www-form-urlencoded", StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
private bool HasMultipartFormContentType(MediaTypeHeaderValue contentType)
|
||||
{
|
||||
// Content-Type: multipart/form-data; boundary=----WebKitFormBoundarymx2fSWqWSd0OxQqq
|
||||
return contentType != null && contentType.MediaType.Equals("multipart/form-data", StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
private bool HasFormDataContentDisposition(ContentDispositionHeaderValue contentDisposition)
|
||||
{
|
||||
// Content-Disposition: form-data; name="key";
|
||||
return contentDisposition != null && contentDisposition.DispositionType.Equals("form-data")
|
||||
&& string.IsNullOrEmpty(contentDisposition.FileName) && string.IsNullOrEmpty(contentDisposition.FileNameStar);
|
||||
}
|
||||
|
||||
private bool HasFileContentDisposition(ContentDispositionHeaderValue contentDisposition)
|
||||
{
|
||||
// Content-Disposition: form-data; name="myfile1"; filename="Misc 002.jpg"
|
||||
return contentDisposition != null && contentDisposition.DispositionType.Equals("form-data")
|
||||
&& (!string.IsNullOrEmpty(contentDisposition.FileName) || !string.IsNullOrEmpty(contentDisposition.FileNameStar));
|
||||
}
|
||||
|
||||
// Content-Type: multipart/form-data; boundary="----WebKitFormBoundarymx2fSWqWSd0OxQqq"
|
||||
// TODO: Limit the length of boundary we accept. The spec says ~70 chars.
|
||||
private static string GetBoundary(MediaTypeHeaderValue contentType)
|
||||
{
|
||||
var boundary = HeaderUtilities.RemoveQuotes(contentType.Boundary);
|
||||
if (string.IsNullOrWhiteSpace(boundary))
|
||||
{
|
||||
throw new InvalidOperationException("Missing content-type boundary.");
|
||||
}
|
||||
return boundary;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -518,25 +518,6 @@ namespace Microsoft.AspNet.PipelineCore.Infrastructure
|
|||
request.HttpContext.Items[key] = value;
|
||||
}
|
||||
|
||||
internal static IDictionary<string, string> GetCookies(HttpRequest request)
|
||||
{
|
||||
var cookies = GetItem<IDictionary<string, string>>(request, "Microsoft.Owin.Cookies#dictionary");
|
||||
if (cookies == null)
|
||||
{
|
||||
cookies = new Dictionary<string, string>(StringComparer.Ordinal);
|
||||
SetItem(request, "Microsoft.Owin.Cookies#dictionary", cookies);
|
||||
}
|
||||
|
||||
string text = GetHeader(request.Headers, "Cookie");
|
||||
if (GetItem<string>(request, "Microsoft.Owin.Cookies#text") != text)
|
||||
{
|
||||
cookies.Clear();
|
||||
ParseDelimited(text, SemicolonAndComma, AddCookieCallback, cookies);
|
||||
SetItem(request, "Microsoft.Owin.Cookies#text", text);
|
||||
}
|
||||
return cookies;
|
||||
}
|
||||
|
||||
internal static void ParseCookies(string cookiesHeader, IDictionary<string, string> cookiesCollection)
|
||||
{
|
||||
ParseDelimited(cookiesHeader, SemicolonAndComma, AddCookieCallback, cookiesCollection);
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<Project ToolsVersion="14.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
|
||||
<PropertyGroup>
|
||||
<VisualStudioVersion Condition="'$(VisualStudioVersion)' == ''">14.0</VisualStudioVersion>
|
||||
|
|
@ -14,4 +14,9 @@
|
|||
<SchemaVersion>2.0</SchemaVersion>
|
||||
</PropertyGroup>
|
||||
<Import Project="$(VSToolsPath)\AspNet\Microsoft.Web.AspNet.targets" Condition="'$(VSToolsPath)' != ''" />
|
||||
</Project>
|
||||
<ProjectExtensions>
|
||||
<VisualStudio>
|
||||
<UserProperties project_1json__JSONSchema="http://www.asp.net/media/4878834/project.json" />
|
||||
</VisualStudio>
|
||||
</ProjectExtensions>
|
||||
</Project>
|
||||
|
|
@ -6,7 +6,9 @@
|
|||
"Microsoft.AspNet.FeatureModel": "1.0.0-*",
|
||||
"Microsoft.AspNet.Http": "1.0.0-*",
|
||||
"Microsoft.AspNet.HttpFeature": "1.0.0-*",
|
||||
"Microsoft.AspNet.WebUtilities": "1.0.0-*"
|
||||
"Microsoft.AspNet.WebUtilities": "1.0.0-*",
|
||||
"Microsoft.Net.Http.Headers": "1.0.0-*"
|
||||
|
||||
},
|
||||
"frameworks": {
|
||||
"aspnet50": {},
|
||||
|
|
|
|||
|
|
@ -26,10 +26,9 @@ namespace Microsoft.AspNet.WebUtilities
|
|||
_reader = new StringReader(data);
|
||||
}
|
||||
|
||||
// TODO: Encoding
|
||||
public FormReader([NotNull] Stream stream)
|
||||
public FormReader([NotNull] Stream stream, [NotNull] Encoding encoding)
|
||||
{
|
||||
_reader = new StreamReader(stream, Encoding.UTF8, detectEncodingFromByteOrderMarks: true, bufferSize: 1024 * 2, leaveOpen: true);
|
||||
_reader = new StreamReader(stream, encoding, detectEncodingFromByteOrderMarks: true, bufferSize: 1024 * 2, leaveOpen: true);
|
||||
}
|
||||
|
||||
// Format: key1=value1&key2=value2
|
||||
|
|
@ -168,11 +167,21 @@ namespace Microsoft.AspNet.WebUtilities
|
|||
/// <summary>
|
||||
/// Parses an HTTP form body.
|
||||
/// </summary>
|
||||
/// <param name="text">The HTTP form body to parse.</param>
|
||||
/// <param name="stream">The HTTP form body to parse.</param>
|
||||
/// <returns>The collection containing the parsed HTTP form body.</returns>
|
||||
public static async Task<IDictionary<string, string[]>> ReadFormAsync(Stream stream, CancellationToken cancellationToken = new CancellationToken())
|
||||
public static Task<IDictionary<string, string[]>> ReadFormAsync(Stream stream, CancellationToken cancellationToken = new CancellationToken())
|
||||
{
|
||||
var reader = new FormReader(stream);
|
||||
return ReadFormAsync(stream, Encoding.UTF8, cancellationToken);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parses an HTTP form body.
|
||||
/// </summary>
|
||||
/// <param name="stream">The HTTP form body to parse.</param>
|
||||
/// <returns>The collection containing the parsed HTTP form body.</returns>
|
||||
public static async Task<IDictionary<string, string[]>> ReadFormAsync(Stream stream, Encoding encoding, CancellationToken cancellationToken = new CancellationToken())
|
||||
{
|
||||
var reader = new FormReader(stream, encoding);
|
||||
|
||||
var accumulator = new KeyValueAccumulator<string, string>(StringComparer.OrdinalIgnoreCase);
|
||||
var pair = await reader.ReadNextPairAsync(cancellationToken);
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<Project ToolsVersion="14.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
|
||||
<PropertyGroup>
|
||||
<VisualStudioVersion Condition="'$(VisualStudioVersion)' == ''">14.0</VisualStudioVersion>
|
||||
|
|
@ -14,4 +14,9 @@
|
|||
<SchemaVersion>2.0</SchemaVersion>
|
||||
</PropertyGroup>
|
||||
<Import Project="$(VSToolsPath)\AspNet\Microsoft.Web.AspNet.targets" Condition="'$(VSToolsPath)' != ''" />
|
||||
</Project>
|
||||
<ProjectExtensions>
|
||||
<VisualStudio>
|
||||
<UserProperties project_1json__JSONSchema="http://www.asp.net/media/4878834/project.json" />
|
||||
</VisualStudio>
|
||||
</ProjectExtensions>
|
||||
</Project>
|
||||
|
|
@ -2,15 +2,17 @@
|
|||
"version": "1.0.0-*",
|
||||
"description": "ASP.NET 5 common helper methods such as URL encoding.",
|
||||
"dependencies": {
|
||||
"Microsoft.AspNet.Http": "1.0.0-*"
|
||||
},
|
||||
"frameworks": {
|
||||
"aspnet50": { },
|
||||
"aspnetcore50": {
|
||||
"dependencies": {
|
||||
"System.Collections": "4.0.10-beta-*",
|
||||
"System.Diagnostics.Debug": "4.0.10-beta-*",
|
||||
"System.IO": "4.0.10-beta-*",
|
||||
"System.IO.FileSystem": "4.0.0-beta-*",
|
||||
"System.Runtime": "4.0.20-beta-*"
|
||||
"System.Runtime": "4.0.20-beta-*",
|
||||
"System.Runtime.Extensions": "4.0.10-beta-*"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,70 @@
|
|||
// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved.
|
||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
namespace Microsoft.Net.Http.Headers
|
||||
{
|
||||
internal abstract class BaseHeaderParser<T> : HttpHeaderParser<T>
|
||||
{
|
||||
protected BaseHeaderParser(bool supportsMultipleValues)
|
||||
: base(supportsMultipleValues)
|
||||
{
|
||||
}
|
||||
|
||||
protected abstract int GetParsedValueLength(string value, int startIndex, out T parsedValue);
|
||||
|
||||
public sealed override bool TryParseValue(string value, ref int index, out T parsedValue)
|
||||
{
|
||||
parsedValue = default(T);
|
||||
|
||||
// If multiple values are supported (i.e. list of values), then accept an empty string: The header may
|
||||
// be added multiple times to the request/response message. E.g.
|
||||
// Accept: text/xml; q=1
|
||||
// Accept:
|
||||
// Accept: text/plain; q=0.2
|
||||
if (string.IsNullOrEmpty(value) || (index == value.Length))
|
||||
{
|
||||
return SupportsMultipleValues;
|
||||
}
|
||||
|
||||
var separatorFound = false;
|
||||
var current = HeaderUtilities.GetNextNonEmptyOrWhitespaceIndex(value, index, SupportsMultipleValues,
|
||||
out separatorFound);
|
||||
|
||||
if (separatorFound && !SupportsMultipleValues)
|
||||
{
|
||||
return false; // leading separators not allowed if we don't support multiple values.
|
||||
}
|
||||
|
||||
if (current == value.Length)
|
||||
{
|
||||
if (SupportsMultipleValues)
|
||||
{
|
||||
index = current;
|
||||
}
|
||||
return SupportsMultipleValues;
|
||||
}
|
||||
|
||||
T result = default(T);
|
||||
var length = GetParsedValueLength(value, current, out result);
|
||||
|
||||
if (length == 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
current = current + length;
|
||||
current = HeaderUtilities.GetNextNonEmptyOrWhitespaceIndex(value, current, SupportsMultipleValues,
|
||||
out separatorFound);
|
||||
|
||||
// If we support multiple values and we've not reached the end of the string, then we must have a separator.
|
||||
if ((separatorFound && !SupportsMultipleValues) || (!separatorFound && (current < value.Length)))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
index = current;
|
||||
parsedValue = result;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,603 @@
|
|||
// Copyright (c) Microsoft Open Technologies, Inc. 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.Collections.Generic;
|
||||
using System.Diagnostics.Contracts;
|
||||
using System.Globalization;
|
||||
using System.Text;
|
||||
|
||||
namespace Microsoft.Net.Http.Headers
|
||||
{
|
||||
public class CacheControlHeaderValue
|
||||
{
|
||||
private const string MaxAgeString = "max-age";
|
||||
private const string MaxStaleString = "max-stale";
|
||||
private const string MinFreshString = "min-fresh";
|
||||
private const string MustRevalidateString = "must-revalidate";
|
||||
private const string NoCacheString = "no-cache";
|
||||
private const string NoStoreString = "no-store";
|
||||
private const string NoTransformString = "no-transform";
|
||||
private const string OnlyIfCachedString = "only-if-cached";
|
||||
private const string PrivateString = "private";
|
||||
private const string ProxyRevalidateString = "proxy-revalidate";
|
||||
private const string PublicString = "public";
|
||||
private const string SharedMaxAgeString = "s-maxage";
|
||||
|
||||
// The Cache-Control header is special: It is a header supporting a list of values, but we represent the list
|
||||
// as _one_ instance of CacheControlHeaderValue. I.e we set 'SupportsMultipleValues' to 'true' since it is
|
||||
// OK to have multiple Cache-Control headers in a request/response message. However, after parsing all
|
||||
// Cache-Control headers, only one instance of CacheControlHeaderValue is created (if all headers contain valid
|
||||
// values, otherwise we may have multiple strings containing the invalid values).
|
||||
private static readonly HttpHeaderParser<CacheControlHeaderValue> Parser
|
||||
= new GenericHeaderParser<CacheControlHeaderValue>(true, GetCacheControlLength);
|
||||
|
||||
private static readonly Action<string> CheckIsValidTokenAction = CheckIsValidToken;
|
||||
|
||||
private bool _noCache;
|
||||
private ICollection<string> _noCacheHeaders;
|
||||
private bool _noStore;
|
||||
private TimeSpan? _maxAge;
|
||||
private TimeSpan? _sharedMaxAge;
|
||||
private bool _maxStale;
|
||||
private TimeSpan? _maxStaleLimit;
|
||||
private TimeSpan? _minFresh;
|
||||
private bool _noTransform;
|
||||
private bool _onlyIfCached;
|
||||
private bool _public;
|
||||
private bool _private;
|
||||
private ICollection<string> _privateHeaders;
|
||||
private bool _mustRevalidate;
|
||||
private bool _proxyRevalidate;
|
||||
private ICollection<NameValueHeaderValue> _extensions;
|
||||
|
||||
public CacheControlHeaderValue()
|
||||
{
|
||||
// This type is unique in that there is no single required parameter.
|
||||
}
|
||||
|
||||
public bool NoCache
|
||||
{
|
||||
get { return _noCache; }
|
||||
set { _noCache = value; }
|
||||
}
|
||||
|
||||
public ICollection<string> NoCacheHeaders
|
||||
{
|
||||
get
|
||||
{
|
||||
if (_noCacheHeaders == null)
|
||||
{
|
||||
_noCacheHeaders = new ObjectCollection<string>(CheckIsValidTokenAction);
|
||||
}
|
||||
return _noCacheHeaders;
|
||||
}
|
||||
}
|
||||
|
||||
public bool NoStore
|
||||
{
|
||||
get { return _noStore; }
|
||||
set { _noStore = value; }
|
||||
}
|
||||
|
||||
public TimeSpan? MaxAge
|
||||
{
|
||||
get { return _maxAge; }
|
||||
set { _maxAge = value; }
|
||||
}
|
||||
|
||||
public TimeSpan? SharedMaxAge
|
||||
{
|
||||
get { return _sharedMaxAge; }
|
||||
set { _sharedMaxAge = value; }
|
||||
}
|
||||
|
||||
public bool MaxStale
|
||||
{
|
||||
get { return _maxStale; }
|
||||
set { _maxStale = value; }
|
||||
}
|
||||
|
||||
public TimeSpan? MaxStaleLimit
|
||||
{
|
||||
get { return _maxStaleLimit; }
|
||||
set { _maxStaleLimit = value; }
|
||||
}
|
||||
|
||||
public TimeSpan? MinFresh
|
||||
{
|
||||
get { return _minFresh; }
|
||||
set { _minFresh = value; }
|
||||
}
|
||||
|
||||
public bool NoTransform
|
||||
{
|
||||
get { return _noTransform; }
|
||||
set { _noTransform = value; }
|
||||
}
|
||||
|
||||
public bool OnlyIfCached
|
||||
{
|
||||
get { return _onlyIfCached; }
|
||||
set { _onlyIfCached = value; }
|
||||
}
|
||||
|
||||
public bool Public
|
||||
{
|
||||
get { return _public; }
|
||||
set { _public = value; }
|
||||
}
|
||||
|
||||
public bool Private
|
||||
{
|
||||
get { return _private; }
|
||||
set { _private = value; }
|
||||
}
|
||||
|
||||
public ICollection<string> PrivateHeaders
|
||||
{
|
||||
get
|
||||
{
|
||||
if (_privateHeaders == null)
|
||||
{
|
||||
_privateHeaders = new ObjectCollection<string>(CheckIsValidTokenAction);
|
||||
}
|
||||
return _privateHeaders;
|
||||
}
|
||||
}
|
||||
|
||||
public bool MustRevalidate
|
||||
{
|
||||
get { return _mustRevalidate; }
|
||||
set { _mustRevalidate = value; }
|
||||
}
|
||||
|
||||
public bool ProxyRevalidate
|
||||
{
|
||||
get { return _proxyRevalidate; }
|
||||
set { _proxyRevalidate = value; }
|
||||
}
|
||||
|
||||
public ICollection<NameValueHeaderValue> Extensions
|
||||
{
|
||||
get
|
||||
{
|
||||
if (_extensions == null)
|
||||
{
|
||||
_extensions = new ObjectCollection<NameValueHeaderValue>();
|
||||
}
|
||||
return _extensions;
|
||||
}
|
||||
}
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
var sb = new StringBuilder();
|
||||
|
||||
AppendValueIfRequired(sb, _noStore, NoStoreString);
|
||||
AppendValueIfRequired(sb, _noTransform, NoTransformString);
|
||||
AppendValueIfRequired(sb, _onlyIfCached, OnlyIfCachedString);
|
||||
AppendValueIfRequired(sb, _public, PublicString);
|
||||
AppendValueIfRequired(sb, _mustRevalidate, MustRevalidateString);
|
||||
AppendValueIfRequired(sb, _proxyRevalidate, ProxyRevalidateString);
|
||||
|
||||
if (_noCache)
|
||||
{
|
||||
AppendValueWithSeparatorIfRequired(sb, NoCacheString);
|
||||
if ((_noCacheHeaders != null) && (_noCacheHeaders.Count > 0))
|
||||
{
|
||||
sb.Append("=\"");
|
||||
AppendValues(sb, _noCacheHeaders);
|
||||
sb.Append('\"');
|
||||
}
|
||||
}
|
||||
|
||||
if (_maxAge.HasValue)
|
||||
{
|
||||
AppendValueWithSeparatorIfRequired(sb, MaxAgeString);
|
||||
sb.Append('=');
|
||||
sb.Append(((int)_maxAge.Value.TotalSeconds).ToString(NumberFormatInfo.InvariantInfo));
|
||||
}
|
||||
|
||||
if (_sharedMaxAge.HasValue)
|
||||
{
|
||||
AppendValueWithSeparatorIfRequired(sb, SharedMaxAgeString);
|
||||
sb.Append('=');
|
||||
sb.Append(((int)_sharedMaxAge.Value.TotalSeconds).ToString(NumberFormatInfo.InvariantInfo));
|
||||
}
|
||||
|
||||
if (_maxStale)
|
||||
{
|
||||
AppendValueWithSeparatorIfRequired(sb, MaxStaleString);
|
||||
if (_maxStaleLimit.HasValue)
|
||||
{
|
||||
sb.Append('=');
|
||||
sb.Append(((int)_maxStaleLimit.Value.TotalSeconds).ToString(NumberFormatInfo.InvariantInfo));
|
||||
}
|
||||
}
|
||||
|
||||
if (_minFresh.HasValue)
|
||||
{
|
||||
AppendValueWithSeparatorIfRequired(sb, MinFreshString);
|
||||
sb.Append('=');
|
||||
sb.Append(((int)_minFresh.Value.TotalSeconds).ToString(NumberFormatInfo.InvariantInfo));
|
||||
}
|
||||
|
||||
if (_private)
|
||||
{
|
||||
AppendValueWithSeparatorIfRequired(sb, PrivateString);
|
||||
if ((_privateHeaders != null) && (_privateHeaders.Count > 0))
|
||||
{
|
||||
sb.Append("=\"");
|
||||
AppendValues(sb, _privateHeaders);
|
||||
sb.Append('\"');
|
||||
}
|
||||
}
|
||||
|
||||
NameValueHeaderValue.ToString(_extensions, ',', false, sb);
|
||||
|
||||
return sb.ToString();
|
||||
}
|
||||
|
||||
public override bool Equals(object obj)
|
||||
{
|
||||
var other = obj as CacheControlHeaderValue;
|
||||
|
||||
if (other == null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if ((_noCache != other._noCache) || (_noStore != other._noStore) || (_maxAge != other._maxAge) ||
|
||||
(_sharedMaxAge != other._sharedMaxAge) || (_maxStale != other._maxStale) ||
|
||||
(_maxStaleLimit != other._maxStaleLimit) || (_minFresh != other._minFresh) ||
|
||||
(_noTransform != other._noTransform) || (_onlyIfCached != other._onlyIfCached) ||
|
||||
(_public != other._public) || (_private != other._private) ||
|
||||
(_mustRevalidate != other._mustRevalidate) || (_proxyRevalidate != other._proxyRevalidate))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!HeaderUtilities.AreEqualCollections(_noCacheHeaders, other._noCacheHeaders,
|
||||
StringComparer.OrdinalIgnoreCase))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!HeaderUtilities.AreEqualCollections(_privateHeaders, other._privateHeaders,
|
||||
StringComparer.OrdinalIgnoreCase))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!HeaderUtilities.AreEqualCollections(_extensions, other._extensions))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public override int GetHashCode()
|
||||
{
|
||||
// Use a different bit for bool fields: bool.GetHashCode() will return 0 (false) or 1 (true). So we would
|
||||
// end up having the same hash code for e.g. two instances where one has only noCache set and the other
|
||||
// only noStore.
|
||||
int result = _noCache.GetHashCode() ^ (_noStore.GetHashCode() << 1) ^ (_maxStale.GetHashCode() << 2) ^
|
||||
(_noTransform.GetHashCode() << 3) ^ (_onlyIfCached.GetHashCode() << 4) ^
|
||||
(_public.GetHashCode() << 5) ^ (_private.GetHashCode() << 6) ^
|
||||
(_mustRevalidate.GetHashCode() << 7) ^ (_proxyRevalidate.GetHashCode() << 8);
|
||||
|
||||
// XOR the hashcode of timespan values with different numbers to make sure two instances with the same
|
||||
// timespan set on different fields result in different hashcodes.
|
||||
result = result ^ (_maxAge.HasValue ? _maxAge.Value.GetHashCode() ^ 1 : 0) ^
|
||||
(_sharedMaxAge.HasValue ? _sharedMaxAge.Value.GetHashCode() ^ 2 : 0) ^
|
||||
(_maxStaleLimit.HasValue ? _maxStaleLimit.Value.GetHashCode() ^ 4 : 0) ^
|
||||
(_minFresh.HasValue ? _minFresh.Value.GetHashCode() ^ 8 : 0);
|
||||
|
||||
if ((_noCacheHeaders != null) && (_noCacheHeaders.Count > 0))
|
||||
{
|
||||
foreach (var noCacheHeader in _noCacheHeaders)
|
||||
{
|
||||
result = result ^ StringComparer.OrdinalIgnoreCase.GetHashCode(noCacheHeader);
|
||||
}
|
||||
}
|
||||
|
||||
if ((_privateHeaders != null) && (_privateHeaders.Count > 0))
|
||||
{
|
||||
foreach (var privateHeader in _privateHeaders)
|
||||
{
|
||||
result = result ^ StringComparer.OrdinalIgnoreCase.GetHashCode(privateHeader);
|
||||
}
|
||||
}
|
||||
|
||||
if ((_extensions != null) && (_extensions.Count > 0))
|
||||
{
|
||||
foreach (var extension in _extensions)
|
||||
{
|
||||
result = result ^ extension.GetHashCode();
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
public static CacheControlHeaderValue Parse(string input)
|
||||
{
|
||||
int index = 0;
|
||||
// Cache-Control is unusual because there are no required values so the parser will succeed for an empty string, but still return null.
|
||||
var result = Parser.ParseValue(input, ref index);
|
||||
if (result == null)
|
||||
{
|
||||
throw new FormatException("No cache directives found.");
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
public static bool TryParse(string input, out CacheControlHeaderValue parsedValue)
|
||||
{
|
||||
int index = 0;
|
||||
// Cache-Control is unusual because there are no required values so the parser will succeed for an empty string, but still return null.
|
||||
if (Parser.TryParseValue(input, ref index, out parsedValue) && parsedValue != null)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
parsedValue = null;
|
||||
return false;
|
||||
}
|
||||
|
||||
private static int GetCacheControlLength(string input, int startIndex, out CacheControlHeaderValue parsedValue)
|
||||
{
|
||||
Contract.Requires(startIndex >= 0);
|
||||
|
||||
parsedValue = null;
|
||||
|
||||
if (string.IsNullOrEmpty(input) || (startIndex >= input.Length))
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Cache-Control header consists of a list of name/value pairs, where the value is optional. So use an
|
||||
// instance of NameValueHeaderParser to parse the string.
|
||||
var current = startIndex;
|
||||
NameValueHeaderValue nameValue = null;
|
||||
var nameValueList = new List<NameValueHeaderValue>();
|
||||
while (current < input.Length)
|
||||
{
|
||||
if (!NameValueHeaderValue.MultipleValueParser.TryParseValue(input, ref current, out nameValue))
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
nameValueList.Add(nameValue as NameValueHeaderValue);
|
||||
}
|
||||
|
||||
// If we get here, we were able to successfully parse the string as list of name/value pairs. Now analyze
|
||||
// the name/value pairs.
|
||||
|
||||
// Cache-Control is a header supporting lists of values. However, expose the header as an instance of
|
||||
// CacheControlHeaderValue.
|
||||
var result = new CacheControlHeaderValue();
|
||||
|
||||
if (!TrySetCacheControlValues(result, nameValueList))
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
parsedValue = result;
|
||||
|
||||
// If we get here we successfully parsed the whole string.
|
||||
return input.Length - startIndex;
|
||||
}
|
||||
|
||||
private static bool TrySetCacheControlValues(CacheControlHeaderValue cc,
|
||||
List<NameValueHeaderValue> nameValueList)
|
||||
{
|
||||
foreach (NameValueHeaderValue nameValue in nameValueList)
|
||||
{
|
||||
var success = true;
|
||||
string name = nameValue.Name.ToLowerInvariant();
|
||||
|
||||
switch (name)
|
||||
{
|
||||
case NoCacheString:
|
||||
success = TrySetOptionalTokenList(nameValue, ref cc._noCache, ref cc._noCacheHeaders);
|
||||
break;
|
||||
|
||||
case NoStoreString:
|
||||
success = TrySetTokenOnlyValue(nameValue, ref cc._noStore);
|
||||
break;
|
||||
|
||||
case MaxAgeString:
|
||||
success = TrySetTimeSpan(nameValue, ref cc._maxAge);
|
||||
break;
|
||||
|
||||
case MaxStaleString:
|
||||
success = ((nameValue.Value == null) || TrySetTimeSpan(nameValue, ref cc._maxStaleLimit));
|
||||
if (success)
|
||||
{
|
||||
cc._maxStale = true;
|
||||
}
|
||||
break;
|
||||
|
||||
case MinFreshString:
|
||||
success = TrySetTimeSpan(nameValue, ref cc._minFresh);
|
||||
break;
|
||||
|
||||
case NoTransformString:
|
||||
success = TrySetTokenOnlyValue(nameValue, ref cc._noTransform);
|
||||
break;
|
||||
|
||||
case OnlyIfCachedString:
|
||||
success = TrySetTokenOnlyValue(nameValue, ref cc._onlyIfCached);
|
||||
break;
|
||||
|
||||
case PublicString:
|
||||
success = TrySetTokenOnlyValue(nameValue, ref cc._public);
|
||||
break;
|
||||
|
||||
case PrivateString:
|
||||
success = TrySetOptionalTokenList(nameValue, ref cc._private, ref cc._privateHeaders);
|
||||
break;
|
||||
|
||||
case MustRevalidateString:
|
||||
success = TrySetTokenOnlyValue(nameValue, ref cc._mustRevalidate);
|
||||
break;
|
||||
|
||||
case ProxyRevalidateString:
|
||||
success = TrySetTokenOnlyValue(nameValue, ref cc._proxyRevalidate);
|
||||
break;
|
||||
|
||||
case SharedMaxAgeString:
|
||||
success = TrySetTimeSpan(nameValue, ref cc._sharedMaxAge);
|
||||
break;
|
||||
|
||||
default:
|
||||
cc.Extensions.Add(nameValue); // success is always true
|
||||
break;
|
||||
}
|
||||
|
||||
if (!success)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private static bool TrySetTokenOnlyValue(NameValueHeaderValue nameValue, ref bool boolField)
|
||||
{
|
||||
if (nameValue.Value != null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
boolField = true;
|
||||
return true;
|
||||
}
|
||||
|
||||
private static bool TrySetOptionalTokenList(NameValueHeaderValue nameValue, ref bool boolField,
|
||||
ref ICollection<string> destination)
|
||||
{
|
||||
Contract.Requires(nameValue != null);
|
||||
|
||||
if (nameValue.Value == null)
|
||||
{
|
||||
boolField = true;
|
||||
return true;
|
||||
}
|
||||
|
||||
// We need the string to be at least 3 chars long: 2x quotes and at least 1 character. Also make sure we
|
||||
// have a quoted string. Note that NameValueHeaderValue will never have leading/trailing whitespaces.
|
||||
var valueString = nameValue.Value;
|
||||
if ((valueString.Length < 3) || (valueString[0] != '\"') || (valueString[valueString.Length - 1] != '\"'))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// We have a quoted string. Now verify that the string contains a list of valid tokens separated by ','.
|
||||
var current = 1; // skip the initial '"' character.
|
||||
var maxLength = valueString.Length - 1; // -1 because we don't want to parse the final '"'.
|
||||
var separatorFound = false;
|
||||
var originalValueCount = destination == null ? 0 : destination.Count;
|
||||
while (current < maxLength)
|
||||
{
|
||||
current = HeaderUtilities.GetNextNonEmptyOrWhitespaceIndex(valueString, current, true,
|
||||
out separatorFound);
|
||||
|
||||
if (current == maxLength)
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
var tokenLength = HttpRuleParser.GetTokenLength(valueString, current);
|
||||
|
||||
if (tokenLength == 0)
|
||||
{
|
||||
// We already skipped whitespaces and separators. If we don't have a token it must be an invalid
|
||||
// character.
|
||||
return false;
|
||||
}
|
||||
|
||||
if (destination == null)
|
||||
{
|
||||
destination = new ObjectCollection<string>(CheckIsValidTokenAction);
|
||||
}
|
||||
|
||||
destination.Add(valueString.Substring(current, tokenLength));
|
||||
|
||||
current = current + tokenLength;
|
||||
}
|
||||
|
||||
// After parsing a valid token list, we expect to have at least one value
|
||||
if ((destination != null) && (destination.Count > originalValueCount))
|
||||
{
|
||||
boolField = true;
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private static bool TrySetTimeSpan(NameValueHeaderValue nameValue, ref TimeSpan? timeSpan)
|
||||
{
|
||||
Contract.Requires(nameValue != null);
|
||||
|
||||
if (nameValue.Value == null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
int seconds;
|
||||
if (!HeaderUtilities.TryParseInt32(nameValue.Value, out seconds))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
timeSpan = new TimeSpan(0, 0, seconds);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private static void AppendValueIfRequired(StringBuilder sb, bool appendValue, string value)
|
||||
{
|
||||
if (appendValue)
|
||||
{
|
||||
AppendValueWithSeparatorIfRequired(sb, value);
|
||||
}
|
||||
}
|
||||
|
||||
private static void AppendValueWithSeparatorIfRequired(StringBuilder sb, string value)
|
||||
{
|
||||
if (sb.Length > 0)
|
||||
{
|
||||
sb.Append(", ");
|
||||
}
|
||||
sb.Append(value);
|
||||
}
|
||||
|
||||
private static void AppendValues(StringBuilder sb, IEnumerable<string> values)
|
||||
{
|
||||
var first = true;
|
||||
foreach (string value in values)
|
||||
{
|
||||
if (first)
|
||||
{
|
||||
first = false;
|
||||
}
|
||||
else
|
||||
{
|
||||
sb.Append(", ");
|
||||
}
|
||||
|
||||
sb.Append(value);
|
||||
}
|
||||
}
|
||||
|
||||
private static void CheckIsValidToken(string item)
|
||||
{
|
||||
HeaderUtilities.CheckValidToken(item, "item");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,702 @@
|
|||
// Copyright (c) Microsoft Open Technologies, Inc. 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.Collections.Generic;
|
||||
using System.Diagnostics.Contracts;
|
||||
using System.Globalization;
|
||||
using System.Text;
|
||||
|
||||
namespace Microsoft.Net.Http.Headers
|
||||
{
|
||||
// Note this is for use both in HTTP (https://tools.ietf.org/html/rfc6266) and MIME (https://tools.ietf.org/html/rfc2183)
|
||||
public class ContentDispositionHeaderValue
|
||||
{
|
||||
private const string FileNameString = "filename";
|
||||
private const string NameString = "name";
|
||||
private const string FileNameStarString = "filename*";
|
||||
private const string CreationDateString = "creation-date";
|
||||
private const string ModificationDateString = "modification-date";
|
||||
private const string ReadDateString = "read-date";
|
||||
private const string SizeString = "size";
|
||||
|
||||
private static readonly HttpHeaderParser<ContentDispositionHeaderValue> Parser
|
||||
= new GenericHeaderParser<ContentDispositionHeaderValue>(false, GetDispositionTypeLength);
|
||||
|
||||
// Use list instead of dictionary since we may have multiple parameters with the same name.
|
||||
private ICollection<NameValueHeaderValue> _parameters;
|
||||
private string _dispositionType;
|
||||
|
||||
private ContentDispositionHeaderValue()
|
||||
{
|
||||
// Used by the parser to create a new instance of this type.
|
||||
}
|
||||
|
||||
public ContentDispositionHeaderValue(string dispositionType)
|
||||
{
|
||||
CheckDispositionTypeFormat(dispositionType, "dispositionType");
|
||||
_dispositionType = dispositionType;
|
||||
}
|
||||
|
||||
public string DispositionType
|
||||
{
|
||||
get { return _dispositionType; }
|
||||
set
|
||||
{
|
||||
CheckDispositionTypeFormat(value, "value");
|
||||
_dispositionType = value;
|
||||
}
|
||||
}
|
||||
|
||||
public ICollection<NameValueHeaderValue> Parameters
|
||||
{
|
||||
get
|
||||
{
|
||||
if (_parameters == null)
|
||||
{
|
||||
_parameters = new ObjectCollection<NameValueHeaderValue>();
|
||||
}
|
||||
return _parameters;
|
||||
}
|
||||
}
|
||||
|
||||
// Helpers to access specific parameters in the list
|
||||
|
||||
public string Name
|
||||
{
|
||||
get { return GetName(NameString); }
|
||||
set { SetName(NameString, value); }
|
||||
}
|
||||
|
||||
public string FileName
|
||||
{
|
||||
get { return GetName(FileNameString); }
|
||||
set { SetName(FileNameString, value); }
|
||||
}
|
||||
|
||||
public string FileNameStar
|
||||
{
|
||||
get { return GetName(FileNameStarString); }
|
||||
set { SetName(FileNameStarString, value); }
|
||||
}
|
||||
|
||||
public DateTimeOffset? CreationDate
|
||||
{
|
||||
get { return GetDate(CreationDateString); }
|
||||
set { SetDate(CreationDateString, value); }
|
||||
}
|
||||
|
||||
public DateTimeOffset? ModificationDate
|
||||
{
|
||||
get { return GetDate(ModificationDateString); }
|
||||
set { SetDate(ModificationDateString, value); }
|
||||
}
|
||||
|
||||
public DateTimeOffset? ReadDate
|
||||
{
|
||||
get { return GetDate(ReadDateString); }
|
||||
set { SetDate(ReadDateString, value); }
|
||||
}
|
||||
|
||||
public long? Size
|
||||
{
|
||||
get
|
||||
{
|
||||
var sizeParameter = NameValueHeaderValue.Find(_parameters, SizeString);
|
||||
ulong value;
|
||||
if (sizeParameter != null)
|
||||
{
|
||||
string sizeString = sizeParameter.Value;
|
||||
if (UInt64.TryParse(sizeString, NumberStyles.Integer, CultureInfo.InvariantCulture, out value))
|
||||
{
|
||||
return (long)value;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
set
|
||||
{
|
||||
var sizeParameter = NameValueHeaderValue.Find(_parameters, SizeString);
|
||||
if (value == null)
|
||||
{
|
||||
// Remove parameter
|
||||
if (sizeParameter != null)
|
||||
{
|
||||
_parameters.Remove(sizeParameter);
|
||||
}
|
||||
}
|
||||
else if (value < 0)
|
||||
{
|
||||
throw new ArgumentOutOfRangeException("value");
|
||||
}
|
||||
else if (sizeParameter != null)
|
||||
{
|
||||
sizeParameter.Value = value.Value.ToString(CultureInfo.InvariantCulture);
|
||||
}
|
||||
else
|
||||
{
|
||||
string sizeString = value.Value.ToString(CultureInfo.InvariantCulture);
|
||||
_parameters.Add(new NameValueHeaderValue(SizeString, sizeString));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets both FileName and FileNameStar using encodings appropriate for HTTP headers.
|
||||
/// </summary>
|
||||
/// <param name="fileName"></param>
|
||||
public void SetHttpFileName(string fileName)
|
||||
{
|
||||
if (!string.IsNullOrEmpty(fileName))
|
||||
{
|
||||
FileName = Sanatize(fileName);
|
||||
}
|
||||
else
|
||||
{
|
||||
FileName = fileName;
|
||||
}
|
||||
FileNameStar = fileName;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets the FileName parameter using encodings appropriate for MIME headers.
|
||||
/// The FileNameStar paraemter is removed.
|
||||
/// </summary>
|
||||
/// <param name="fileName"></param>
|
||||
public void SetMimeFileName(string fileName)
|
||||
{
|
||||
FileNameStar = null;
|
||||
FileName = fileName;
|
||||
}
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
return _dispositionType + NameValueHeaderValue.ToString(_parameters, ';', true);
|
||||
}
|
||||
|
||||
public override bool Equals(object obj)
|
||||
{
|
||||
var other = obj as ContentDispositionHeaderValue;
|
||||
|
||||
if (other == null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return (string.Compare(_dispositionType, other._dispositionType, StringComparison.OrdinalIgnoreCase) == 0) &&
|
||||
HeaderUtilities.AreEqualCollections(_parameters, other._parameters);
|
||||
}
|
||||
|
||||
public override int GetHashCode()
|
||||
{
|
||||
// The dispositionType string is case-insensitive.
|
||||
return StringComparer.OrdinalIgnoreCase.GetHashCode(_dispositionType) ^ NameValueHeaderValue.GetHashCode(_parameters);
|
||||
}
|
||||
|
||||
public static ContentDispositionHeaderValue Parse(string input)
|
||||
{
|
||||
var index = 0;
|
||||
return Parser.ParseValue(input, ref index);
|
||||
}
|
||||
|
||||
public static bool TryParse(string input, out ContentDispositionHeaderValue parsedValue)
|
||||
{
|
||||
var index = 0;
|
||||
return Parser.TryParseValue(input, ref index, out parsedValue);
|
||||
}
|
||||
|
||||
private static int GetDispositionTypeLength(string input, int startIndex, out ContentDispositionHeaderValue parsedValue)
|
||||
{
|
||||
Contract.Requires(startIndex >= 0);
|
||||
|
||||
parsedValue = null;
|
||||
|
||||
if (string.IsNullOrEmpty(input) || (startIndex >= input.Length))
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Caller must remove leading whitespaces. If not, we'll return 0.
|
||||
string dispositionType = null;
|
||||
var dispositionTypeLength = GetDispositionTypeExpressionLength(input, startIndex, out dispositionType);
|
||||
|
||||
if (dispositionTypeLength == 0)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
var current = startIndex + dispositionTypeLength;
|
||||
current = current + HttpRuleParser.GetWhitespaceLength(input, current);
|
||||
var 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, ';',
|
||||
contentDispositionHeader.Parameters);
|
||||
|
||||
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)
|
||||
{
|
||||
Contract.Requires((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"
|
||||
var typeLength = HttpRuleParser.GetTokenLength(input, startIndex);
|
||||
|
||||
if (typeLength == 0)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
dispositionType = input.Substring(startIndex, typeLength);
|
||||
return typeLength;
|
||||
}
|
||||
|
||||
private static void CheckDispositionTypeFormat(string dispositionType, string parameterName)
|
||||
{
|
||||
if (string.IsNullOrEmpty(dispositionType))
|
||||
{
|
||||
throw new ArgumentException("An empty string is not allowed.", parameterName);
|
||||
}
|
||||
|
||||
// When adding values using strongly typed objects, no leading/trailing LWS (whitespaces) are allowed.
|
||||
string tempDispositionType;
|
||||
var dispositionTypeLength = GetDispositionTypeExpressionLength(dispositionType, 0, out tempDispositionType);
|
||||
if ((dispositionTypeLength == 0) || (tempDispositionType.Length != dispositionType.Length))
|
||||
{
|
||||
throw new FormatException(string.Format(CultureInfo.InvariantCulture,
|
||||
"Invalid disposition type '{0}'.", dispositionType));
|
||||
}
|
||||
}
|
||||
|
||||
// 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)
|
||||
{
|
||||
var dateParameter = NameValueHeaderValue.Find(_parameters, parameter);
|
||||
if (dateParameter != null)
|
||||
{
|
||||
string dateString = dateParameter.Value;
|
||||
// Should have quotes, remove them.
|
||||
if (IsQuoted(dateString))
|
||||
{
|
||||
dateString = dateString.Substring(1, dateString.Length - 2);
|
||||
}
|
||||
DateTimeOffset date;
|
||||
if (HttpRuleParser.TryStringToDate(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)
|
||||
{
|
||||
var dateParameter = NameValueHeaderValue.Find(_parameters, parameter);
|
||||
if (date == null)
|
||||
{
|
||||
// Remove parameter
|
||||
if (dateParameter != null)
|
||||
{
|
||||
_parameters.Remove(dateParameter);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// Must always be quoted
|
||||
var dateString = string.Format(CultureInfo.InvariantCulture, "\"{0}\"", HttpRuleParser.DateToString(date.Value));
|
||||
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)
|
||||
{
|
||||
var nameParameter = NameValueHeaderValue.Find(_parameters, parameter);
|
||||
if (nameParameter != null)
|
||||
{
|
||||
string result;
|
||||
// filename*=utf-8'lang'%7FMyString
|
||||
if (parameter.EndsWith("*", StringComparison.Ordinal))
|
||||
{
|
||||
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)
|
||||
{
|
||||
var nameParameter = NameValueHeaderValue.Find(_parameters, parameter);
|
||||
if (string.IsNullOrEmpty(value))
|
||||
{
|
||||
// Remove parameter
|
||||
if (nameParameter != null)
|
||||
{
|
||||
_parameters.Remove(nameParameter);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
var processedValue = string.Empty;
|
||||
if (parameter.EndsWith("*", StringComparison.Ordinal))
|
||||
{
|
||||
processedValue = 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 string EncodeAndQuoteMime(string input)
|
||||
{
|
||||
var result = input;
|
||||
var needsQuotes = false;
|
||||
// Remove bounding quotes, they'll get re-added later
|
||||
if (IsQuoted(result))
|
||||
{
|
||||
result = result.Substring(1, result.Length - 2);
|
||||
needsQuotes = true;
|
||||
}
|
||||
|
||||
if (RequiresEncoding(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)
|
||||
{
|
||||
// '\' and '"' must be escaped in a quoted string
|
||||
result = result.Replace(@"\", @"\\");
|
||||
result = result.Replace(@"""", @"\""");
|
||||
// Re-add quotes "value"
|
||||
result = string.Format(CultureInfo.InvariantCulture, "\"{0}\"", result);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
// Replaces characters not suitable for HTTP headers with '_' rather than MIME encoding them.
|
||||
private string Sanatize(string input)
|
||||
{
|
||||
var result = input;
|
||||
|
||||
if (RequiresEncoding(result))
|
||||
{
|
||||
var builder = new StringBuilder(result.Length);
|
||||
for (int i = 0; i < result.Length; i++)
|
||||
{
|
||||
var c = result[i];
|
||||
if ((int)c > 0x7f)
|
||||
{
|
||||
c = '_'; // Replace out-of-range characters
|
||||
}
|
||||
builder.Append(c);
|
||||
}
|
||||
result = builder.ToString();
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
// Returns true if the value starts and ends with a quote
|
||||
private bool IsQuoted(string value)
|
||||
{
|
||||
Contract.Assert(value != null);
|
||||
|
||||
return value.Length > 1 && value.StartsWith("\"", StringComparison.Ordinal)
|
||||
&& value.EndsWith("\"", StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
// tspecials are required to be in a quoted string. Only non-ascii needs to be encoded.
|
||||
private bool RequiresEncoding(string input)
|
||||
{
|
||||
Contract.Assert(input != null);
|
||||
|
||||
foreach (char c in input)
|
||||
{
|
||||
if ((int)c > 0x7f)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// Encode using MIME encoding
|
||||
private string EncodeMime(string input)
|
||||
{
|
||||
var buffer = Encoding.UTF8.GetBytes(input);
|
||||
var encodedName = Convert.ToBase64String(buffer);
|
||||
return string.Format(CultureInfo.InvariantCulture, "=?utf-8?B?{0}?=", encodedName);
|
||||
}
|
||||
|
||||
// Attempt to decode MIME encoded strings
|
||||
private bool TryDecodeMime(string input, out string output)
|
||||
{
|
||||
Contract.Assert(input != null);
|
||||
|
||||
output = null;
|
||||
var processedInput = input;
|
||||
// Require quotes, min of "=?e?b??="
|
||||
if (!IsQuoted(processedInput) || processedInput.Length < 10)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
var parts = processedInput.Split('?');
|
||||
// "=, encodingName, encodingType, encodedData, ="
|
||||
if (parts.Length != 5 || parts[0] != "\"=" || parts[4] != "=\"" || parts[2].ToLowerInvariant() != "b")
|
||||
{
|
||||
// Not encoded.
|
||||
// This does not support multi-line encoding.
|
||||
// Only base64 encoding is supported, not quoted printable
|
||||
return false;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var encoding = Encoding.GetEncoding(parts[1]);
|
||||
var bytes = Convert.FromBase64String(parts[3]);
|
||||
output = encoding.GetString(bytes);
|
||||
return true;
|
||||
}
|
||||
catch (ArgumentException)
|
||||
{
|
||||
// Unknown encoding or bad characters
|
||||
}
|
||||
catch (FormatException)
|
||||
{
|
||||
// Bad base64 decoding
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// Encode a string using RFC 5987 encoding
|
||||
// encoding'lang'PercentEncodedSpecials
|
||||
private string Encode5987(string input)
|
||||
{
|
||||
var builder = new StringBuilder("UTF-8\'\'");
|
||||
foreach (char c in input)
|
||||
{
|
||||
// attr-char = ALPHA / DIGIT / "!" / "#" / "$" / "&" / "+" / "-" / "." / "^" / "_" / "`" / "|" / "~"
|
||||
// ; token except ( "*" / "'" / "%" )
|
||||
if (c > 0x7F) // Encodes as multiple utf-8 bytes
|
||||
{
|
||||
var bytes = Encoding.UTF8.GetBytes(c.ToString());
|
||||
foreach (byte b in bytes)
|
||||
{
|
||||
HexEscape(builder, (char)b);
|
||||
}
|
||||
}
|
||||
else if (!HttpRuleParser.IsTokenChar(c) || c == '*' || c == '\'' || c == '%')
|
||||
{
|
||||
// ASCII - Only one encoded byte
|
||||
HexEscape(builder, c);
|
||||
}
|
||||
else
|
||||
{
|
||||
builder.Append(c);
|
||||
}
|
||||
}
|
||||
return builder.ToString();
|
||||
}
|
||||
|
||||
private static readonly char[] HexUpperChars = {
|
||||
'0', '1', '2', '3', '4', '5', '6', '7',
|
||||
'8', '9', 'A', 'B', 'C', 'D', 'E', 'F' };
|
||||
|
||||
private static void HexEscape(StringBuilder builder, char c)
|
||||
{
|
||||
builder.Append('%');
|
||||
builder.Append(HexUpperChars[(c & 0xf0) >> 4]);
|
||||
builder.Append(HexUpperChars[c & 0xf]);
|
||||
}
|
||||
|
||||
// Attempt to decode using RFC 5987 encoding.
|
||||
// encoding'language'my%20string
|
||||
private bool TryDecode5987(string input, out string output)
|
||||
{
|
||||
output = null;
|
||||
var parts = input.Split('\'');
|
||||
if (parts.Length != 3)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var decoded = new StringBuilder();
|
||||
try
|
||||
{
|
||||
var encoding = Encoding.GetEncoding(parts[0]);
|
||||
|
||||
var dataString = parts[2];
|
||||
var unescapedBytes = new byte[dataString.Length];
|
||||
var unescapedBytesCount = 0;
|
||||
for (var index = 0; index < dataString.Length; index++)
|
||||
{
|
||||
if (IsHexEncoding(dataString, index)) // %FF
|
||||
{
|
||||
// Unescape and cache bytes, multi-byte characters must be decoded all at once
|
||||
unescapedBytes[unescapedBytesCount++] = 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;
|
||||
}
|
||||
|
||||
private static bool IsHexEncoding(string pattern, int index)
|
||||
{
|
||||
if ((pattern.Length - index) < 3)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
if ((pattern[index] == '%') && IsEscapedAscii(pattern[index + 1], pattern[index + 2]))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private static bool IsEscapedAscii(char digit, char next)
|
||||
{
|
||||
if (!(((digit >= '0') && (digit <= '9'))
|
||||
|| ((digit >= 'A') && (digit <= 'F'))
|
||||
|| ((digit >= 'a') && (digit <= 'f'))))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!(((next >= '0') && (next <= '9'))
|
||||
|| ((next >= 'A') && (next <= 'F'))
|
||||
|| ((next >= 'a') && (next <= 'f'))))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private static byte HexUnescape(string pattern, ref int index)
|
||||
{
|
||||
if ((index < 0) || (index >= pattern.Length))
|
||||
{
|
||||
throw new ArgumentOutOfRangeException("index");
|
||||
}
|
||||
if ((pattern[index] == '%')
|
||||
&& (pattern.Length - index >= 3))
|
||||
{
|
||||
var ret = UnEscapeAscii(pattern[index + 1], pattern[index + 2]);
|
||||
index += 3;
|
||||
return ret;
|
||||
}
|
||||
return (byte)pattern[index++];
|
||||
}
|
||||
|
||||
internal static byte UnEscapeAscii(char digit, char next)
|
||||
{
|
||||
if (!(((digit >= '0') && (digit <= '9'))
|
||||
|| ((digit >= 'A') && (digit <= 'F'))
|
||||
|| ((digit >= 'a') && (digit <= 'f'))))
|
||||
{
|
||||
throw new ArgumentException();
|
||||
}
|
||||
|
||||
var res = (digit <= '9')
|
||||
? ((int)digit - (int)'0')
|
||||
: (((digit <= 'F')
|
||||
? ((int)digit - (int)'A')
|
||||
: ((int)digit - (int)'a'))
|
||||
+ 10);
|
||||
|
||||
if (!(((next >= '0') && (next <= '9'))
|
||||
|| ((next >= 'A') && (next <= 'F'))
|
||||
|| ((next >= 'a') && (next <= 'f'))))
|
||||
{
|
||||
throw new ArgumentException();
|
||||
}
|
||||
|
||||
return (byte)((res << 4) + ((next <= '9')
|
||||
? ((int)next - (int)'0')
|
||||
: (((next <= 'F')
|
||||
? ((int)next - (int)'A')
|
||||
: ((int)next - (int)'a'))
|
||||
+ 10)));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,397 @@
|
|||
// Copyright (c) Microsoft Open Technologies, Inc. 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.Diagnostics.Contracts;
|
||||
using System.Globalization;
|
||||
using System.Text;
|
||||
|
||||
namespace Microsoft.Net.Http.Headers
|
||||
{
|
||||
public class ContentRangeHeaderValue
|
||||
{
|
||||
private static readonly HttpHeaderParser<ContentRangeHeaderValue> Parser
|
||||
= new GenericHeaderParser<ContentRangeHeaderValue>(false, GetContentRangeLength);
|
||||
|
||||
private string _unit;
|
||||
private long? _from;
|
||||
private long? _to;
|
||||
private long? _length;
|
||||
|
||||
private ContentRangeHeaderValue()
|
||||
{
|
||||
// Used by the parser to create a new instance of this type.
|
||||
}
|
||||
|
||||
public ContentRangeHeaderValue(long from, long to, long length)
|
||||
{
|
||||
// Scenario: "Content-Range: bytes 12-34/5678"
|
||||
|
||||
if (length < 0)
|
||||
{
|
||||
throw new ArgumentOutOfRangeException("length");
|
||||
}
|
||||
if ((to < 0) || (to > length))
|
||||
{
|
||||
throw new ArgumentOutOfRangeException("to");
|
||||
}
|
||||
if ((from < 0) || (from > to))
|
||||
{
|
||||
throw new ArgumentOutOfRangeException("from");
|
||||
}
|
||||
|
||||
_from = from;
|
||||
_to = to;
|
||||
_length = length;
|
||||
_unit = HeaderUtilities.BytesUnit;
|
||||
}
|
||||
|
||||
public ContentRangeHeaderValue(long length)
|
||||
{
|
||||
// Scenario: "Content-Range: bytes */1234"
|
||||
|
||||
if (length < 0)
|
||||
{
|
||||
throw new ArgumentOutOfRangeException("length");
|
||||
}
|
||||
|
||||
_length = length;
|
||||
_unit = HeaderUtilities.BytesUnit;
|
||||
}
|
||||
|
||||
public ContentRangeHeaderValue(long from, long to)
|
||||
{
|
||||
// Scenario: "Content-Range: bytes 12-34/*"
|
||||
|
||||
if (to < 0)
|
||||
{
|
||||
throw new ArgumentOutOfRangeException("to");
|
||||
}
|
||||
if ((from < 0) || (from > to))
|
||||
{
|
||||
throw new ArgumentOutOfRangeException("from");
|
||||
}
|
||||
|
||||
_from = from;
|
||||
_to = to;
|
||||
_unit = HeaderUtilities.BytesUnit;
|
||||
}
|
||||
|
||||
public string Unit
|
||||
{
|
||||
get { return _unit; }
|
||||
set
|
||||
{
|
||||
HeaderUtilities.CheckValidToken(value, "value");
|
||||
_unit = value;
|
||||
}
|
||||
}
|
||||
|
||||
public long? From
|
||||
{
|
||||
get { return _from; }
|
||||
}
|
||||
|
||||
public long? To
|
||||
{
|
||||
get { return _to; }
|
||||
}
|
||||
|
||||
public long? Length
|
||||
{
|
||||
get { return _length; }
|
||||
}
|
||||
|
||||
public bool HasLength // e.g. "Content-Range: bytes 12-34/*"
|
||||
{
|
||||
get { return _length != null; }
|
||||
}
|
||||
|
||||
public bool HasRange // e.g. "Content-Range: bytes */1234"
|
||||
{
|
||||
get { return _from != null; }
|
||||
}
|
||||
|
||||
public override bool Equals(object obj)
|
||||
{
|
||||
var other = obj as ContentRangeHeaderValue;
|
||||
|
||||
if (other == null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return ((_from == other._from) && (_to == other._to) && (_length == other._length) &&
|
||||
(string.Compare(_unit, other._unit, StringComparison.OrdinalIgnoreCase) == 0));
|
||||
}
|
||||
|
||||
public override int GetHashCode()
|
||||
{
|
||||
var result = StringComparer.OrdinalIgnoreCase.GetHashCode(_unit);
|
||||
|
||||
if (HasRange)
|
||||
{
|
||||
result = result ^ _from.GetHashCode() ^ _to.GetHashCode();
|
||||
}
|
||||
|
||||
if (HasLength)
|
||||
{
|
||||
result = result ^ _length.GetHashCode();
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
var sb = new StringBuilder(_unit);
|
||||
sb.Append(' ');
|
||||
|
||||
if (HasRange)
|
||||
{
|
||||
sb.Append(_from.Value.ToString(NumberFormatInfo.InvariantInfo));
|
||||
sb.Append('-');
|
||||
sb.Append(_to.Value.ToString(NumberFormatInfo.InvariantInfo));
|
||||
}
|
||||
else
|
||||
{
|
||||
sb.Append('*');
|
||||
}
|
||||
|
||||
sb.Append('/');
|
||||
if (HasLength)
|
||||
{
|
||||
sb.Append(_length.Value.ToString(NumberFormatInfo.InvariantInfo));
|
||||
}
|
||||
else
|
||||
{
|
||||
sb.Append('*');
|
||||
}
|
||||
|
||||
return sb.ToString();
|
||||
}
|
||||
|
||||
public static ContentRangeHeaderValue Parse(string input)
|
||||
{
|
||||
var index = 0;
|
||||
return Parser.ParseValue(input, ref index);
|
||||
}
|
||||
|
||||
public static bool TryParse(string input, out ContentRangeHeaderValue parsedValue)
|
||||
{
|
||||
var index = 0;
|
||||
return Parser.TryParseValue(input, ref index, out parsedValue);
|
||||
}
|
||||
|
||||
private static int GetContentRangeLength(string input, int startIndex, out ContentRangeHeaderValue parsedValue)
|
||||
{
|
||||
Contract.Requires(startIndex >= 0);
|
||||
|
||||
parsedValue = null;
|
||||
|
||||
if (string.IsNullOrEmpty(input) || (startIndex >= input.Length))
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Parse the unit string: <unit> in '<unit> <from>-<to>/<length>'
|
||||
var unitLength = HttpRuleParser.GetTokenLength(input, startIndex);
|
||||
|
||||
if (unitLength == 0)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
var unit = input.Substring(startIndex, unitLength);
|
||||
var current = startIndex + unitLength;
|
||||
var separatorLength = HttpRuleParser.GetWhitespaceLength(input, current);
|
||||
|
||||
if (separatorLength == 0)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
current = current + separatorLength;
|
||||
|
||||
if (current == input.Length)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Read range values <from> and <to> in '<unit> <from>-<to>/<length>'
|
||||
var fromStartIndex = current;
|
||||
var fromLength = 0;
|
||||
var toStartIndex = 0;
|
||||
var toLength = 0;
|
||||
if (!TryGetRangeLength(input, ref current, out fromLength, out toStartIndex, out toLength))
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
// After the range is read we expect the length separator '/'
|
||||
if ((current == input.Length) || (input[current] != '/'))
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
current++; // Skip '/' separator
|
||||
current = current + HttpRuleParser.GetWhitespaceLength(input, current);
|
||||
|
||||
if (current == input.Length)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
// We may not have a length (e.g. 'bytes 1-2/*'). But if we do, parse the length now.
|
||||
var lengthStartIndex = current;
|
||||
var lengthLength = 0;
|
||||
if (!TryGetLengthLength(input, ref current, out lengthLength))
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (!TryCreateContentRange(input, unit, fromStartIndex, fromLength, toStartIndex, toLength,
|
||||
lengthStartIndex, lengthLength, out parsedValue))
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
return current - startIndex;
|
||||
}
|
||||
|
||||
private static bool TryGetLengthLength(string input, ref int current, out int lengthLength)
|
||||
{
|
||||
lengthLength = 0;
|
||||
|
||||
if (input[current] == '*')
|
||||
{
|
||||
current++;
|
||||
}
|
||||
else
|
||||
{
|
||||
// Parse length value: <length> in '<unit> <from>-<to>/<length>'
|
||||
lengthLength = HttpRuleParser.GetNumberLength(input, current, false);
|
||||
|
||||
if ((lengthLength == 0) || (lengthLength > HttpRuleParser.MaxInt64Digits))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
current = current + lengthLength;
|
||||
}
|
||||
|
||||
current = current + HttpRuleParser.GetWhitespaceLength(input, current);
|
||||
return true;
|
||||
}
|
||||
|
||||
private static bool TryGetRangeLength(string input, ref int current, out int fromLength, out int toStartIndex, out int toLength)
|
||||
{
|
||||
fromLength = 0;
|
||||
toStartIndex = 0;
|
||||
toLength = 0;
|
||||
|
||||
// Check if we have a value like 'bytes */133'. If yes, skip the range part and continue parsing the
|
||||
// length separator '/'.
|
||||
if (input[current] == '*')
|
||||
{
|
||||
current++;
|
||||
}
|
||||
else
|
||||
{
|
||||
// Parse first range value: <from> in '<unit> <from>-<to>/<length>'
|
||||
fromLength = HttpRuleParser.GetNumberLength(input, current, false);
|
||||
|
||||
if ((fromLength == 0) || (fromLength > HttpRuleParser.MaxInt64Digits))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
current = current + fromLength;
|
||||
current = current + HttpRuleParser.GetWhitespaceLength(input, current);
|
||||
|
||||
// After the first value, the '-' character must follow.
|
||||
if ((current == input.Length) || (input[current] != '-'))
|
||||
{
|
||||
// We need a '-' character otherwise this can't be a valid range.
|
||||
return false;
|
||||
}
|
||||
|
||||
current++; // skip the '-' character
|
||||
current = current + HttpRuleParser.GetWhitespaceLength(input, current);
|
||||
|
||||
if (current == input.Length)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// Parse second range value: <to> in '<unit> <from>-<to>/<length>'
|
||||
toStartIndex = current;
|
||||
toLength = HttpRuleParser.GetNumberLength(input, current, false);
|
||||
|
||||
if ((toLength == 0) || (toLength > HttpRuleParser.MaxInt64Digits))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
current = current + toLength;
|
||||
}
|
||||
|
||||
current = current + HttpRuleParser.GetWhitespaceLength(input, current);
|
||||
return true;
|
||||
}
|
||||
|
||||
private static bool TryCreateContentRange(string input, string unit, int fromStartIndex, int fromLength,
|
||||
int toStartIndex, int toLength, int lengthStartIndex, int lengthLength, out ContentRangeHeaderValue parsedValue)
|
||||
{
|
||||
parsedValue = null;
|
||||
|
||||
long from = 0;
|
||||
if ((fromLength > 0) && !HeaderUtilities.TryParseInt64(input.Substring(fromStartIndex, fromLength), out from))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
long to = 0;
|
||||
if ((toLength > 0) && !HeaderUtilities.TryParseInt64(input.Substring(toStartIndex, toLength), out to))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// 'from' must not be greater than 'to'
|
||||
if ((fromLength > 0) && (toLength > 0) && (from > to))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
long length = 0;
|
||||
if ((lengthLength > 0) && !HeaderUtilities.TryParseInt64(input.Substring(lengthStartIndex, lengthLength),
|
||||
out length))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// 'from' and 'to' must be less than 'length'
|
||||
if ((toLength > 0) && (lengthLength > 0) && (to >= length))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var result = new ContentRangeHeaderValue();
|
||||
result._unit = unit;
|
||||
|
||||
if (fromLength > 0)
|
||||
{
|
||||
result._from = from;
|
||||
result._to = to;
|
||||
}
|
||||
|
||||
if (lengthLength > 0)
|
||||
{
|
||||
result._length = length;
|
||||
}
|
||||
|
||||
parsedValue = result;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,104 @@
|
|||
// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved.
|
||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
using System.Diagnostics.Contracts;
|
||||
|
||||
namespace Microsoft.Net.Http.Headers
|
||||
{
|
||||
internal class CookieHeaderParser : HttpHeaderParser<CookieHeaderValue>
|
||||
{
|
||||
// The Cache-Control header is special: It is a header supporting a list of values, but we represent the list
|
||||
// as _one_ instance of CacheControlHeaderValue. I.e we set 'SupportsMultipleValues' to 'true' since it is
|
||||
// OK to have multiple Cache-Control headers in a request/response message. However, after parsing all
|
||||
// Cache-Control headers, only one instance of CacheControlHeaderValue is created (if all headers contain valid
|
||||
// values, otherwise we may have multiple strings containing the invalid values).
|
||||
internal CookieHeaderParser(bool supportsMultipleValues)
|
||||
: base(supportsMultipleValues)
|
||||
{
|
||||
}
|
||||
|
||||
public sealed override bool TryParseValue(string value, ref int index, out CookieHeaderValue parsedValue)
|
||||
{
|
||||
parsedValue = null;
|
||||
|
||||
// If multiple values are supported (i.e. list of values), then accept an empty string: The header may
|
||||
// be added multiple times to the request/response message. E.g.
|
||||
// Accept: text/xml; q=1
|
||||
// Accept:
|
||||
// Accept: text/plain; q=0.2
|
||||
if (string.IsNullOrEmpty(value) || (index == value.Length))
|
||||
{
|
||||
return SupportsMultipleValues;
|
||||
}
|
||||
|
||||
var separatorFound = false;
|
||||
var current = GetNextNonEmptyOrWhitespaceIndex(value, index, SupportsMultipleValues, out separatorFound);
|
||||
|
||||
if (separatorFound && !SupportsMultipleValues)
|
||||
{
|
||||
return false; // leading separators not allowed if we don't support multiple values.
|
||||
}
|
||||
|
||||
if (current == value.Length)
|
||||
{
|
||||
if (SupportsMultipleValues)
|
||||
{
|
||||
index = current;
|
||||
}
|
||||
return SupportsMultipleValues;
|
||||
}
|
||||
|
||||
CookieHeaderValue result = null;
|
||||
int length = CookieHeaderValue.GetCookieLength(value, current, out result);
|
||||
|
||||
if (length == 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
current = current + length;
|
||||
current = GetNextNonEmptyOrWhitespaceIndex(value, current, SupportsMultipleValues, out separatorFound);
|
||||
|
||||
// If we support multiple values and we've not reached the end of the string, then we must have a separator.
|
||||
if ((separatorFound && !SupportsMultipleValues) || (!separatorFound && (current < value.Length)))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
index = current;
|
||||
parsedValue = result;
|
||||
return true;
|
||||
}
|
||||
|
||||
private static int GetNextNonEmptyOrWhitespaceIndex(string input, int startIndex, bool skipEmptyValues, out bool separatorFound)
|
||||
{
|
||||
Contract.Requires(input != null);
|
||||
Contract.Requires(startIndex <= input.Length); // it's OK if index == value.Length.
|
||||
|
||||
separatorFound = false;
|
||||
var current = startIndex + HttpRuleParser.GetWhitespaceLength(input, startIndex);
|
||||
|
||||
if ((current == input.Length) || (input[current] != ',' && input[current] != ';'))
|
||||
{
|
||||
return current;
|
||||
}
|
||||
|
||||
// If we have a separator, skip the separator and all following whitespaces. If we support
|
||||
// empty values, continue until the current character is neither a separator nor a whitespace.
|
||||
separatorFound = true;
|
||||
current++; // skip delimiter.
|
||||
current = current + HttpRuleParser.GetWhitespaceLength(input, current);
|
||||
|
||||
if (skipEmptyValues)
|
||||
{
|
||||
while ((current < input.Length) && (input[current] == ',') && (input[current] == ';'))
|
||||
{
|
||||
current++; // skip delimiter.
|
||||
current = current + HttpRuleParser.GetWhitespaceLength(input, current);
|
||||
}
|
||||
}
|
||||
|
||||
return current;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,256 @@
|
|||
// Copyright (c) Microsoft Open Technologies, Inc. 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.Collections.Generic;
|
||||
using System.Diagnostics.Contracts;
|
||||
using System.Text;
|
||||
|
||||
namespace Microsoft.Net.Http.Headers
|
||||
{
|
||||
// http://tools.ietf.org/html/rfc6265
|
||||
public class CookieHeaderValue
|
||||
{
|
||||
private static readonly CookieHeaderParser SingleValueParser = new CookieHeaderParser(supportsMultipleValues: false);
|
||||
private static readonly CookieHeaderParser MultipleValueParser = new CookieHeaderParser(supportsMultipleValues: true);
|
||||
|
||||
private string _name;
|
||||
private string _value;
|
||||
|
||||
private CookieHeaderValue()
|
||||
{
|
||||
// Used by the parser to create a new instance of this type.
|
||||
}
|
||||
|
||||
public CookieHeaderValue([NotNull] string name)
|
||||
: this(name, string.Empty)
|
||||
{
|
||||
}
|
||||
|
||||
public CookieHeaderValue([NotNull] string name, [NotNull] string value)
|
||||
{
|
||||
Name = name;
|
||||
Value = value;
|
||||
}
|
||||
|
||||
public string Name
|
||||
{
|
||||
get { return _name; }
|
||||
set
|
||||
{
|
||||
CheckNameFormat(value, "name");
|
||||
_name = value;
|
||||
}
|
||||
}
|
||||
|
||||
public string Value
|
||||
{
|
||||
get { return _value; }
|
||||
set
|
||||
{
|
||||
CheckValueFormat(value, "value");
|
||||
_value = value;
|
||||
}
|
||||
}
|
||||
|
||||
// name="val ue";
|
||||
public override string ToString()
|
||||
{
|
||||
var header = new StringBuilder();
|
||||
|
||||
header.Append(_name);
|
||||
header.Append("=");
|
||||
header.Append(_value);
|
||||
|
||||
return header.ToString();
|
||||
}
|
||||
|
||||
private static void AppendSegment(StringBuilder builder, string name, string value)
|
||||
{
|
||||
builder.Append("; ");
|
||||
builder.Append(name);
|
||||
if (value != null)
|
||||
{
|
||||
builder.Append("=");
|
||||
builder.Append(value);
|
||||
}
|
||||
}
|
||||
|
||||
public static CookieHeaderValue Parse(string input)
|
||||
{
|
||||
var index = 0;
|
||||
return SingleValueParser.ParseValue(input, ref index);
|
||||
}
|
||||
|
||||
public static bool TryParse(string input, out CookieHeaderValue parsedValue)
|
||||
{
|
||||
var index = 0;
|
||||
return SingleValueParser.TryParseValue(input, ref index, out parsedValue);
|
||||
}
|
||||
|
||||
public static IList<CookieHeaderValue> ParseList(IList<string> inputs)
|
||||
{
|
||||
return MultipleValueParser.ParseValues(inputs);
|
||||
}
|
||||
|
||||
public static bool TryParseList(IList<string> inputs, out IList<CookieHeaderValue> parsedValues)
|
||||
{
|
||||
return MultipleValueParser.TryParseValues(inputs, out parsedValues);
|
||||
}
|
||||
|
||||
// name=value; name="value"
|
||||
internal static int GetCookieLength(string input, int startIndex, out CookieHeaderValue parsedValue)
|
||||
{
|
||||
Contract.Requires(startIndex >= 0);
|
||||
var offset = startIndex;
|
||||
|
||||
parsedValue = null;
|
||||
|
||||
if (string.IsNullOrEmpty(input) || (offset >= input.Length))
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
var result = new CookieHeaderValue();
|
||||
|
||||
// The caller should have already consumed any leading whitespace, commas, etc..
|
||||
|
||||
// Name=value;
|
||||
|
||||
// Name
|
||||
var itemLength = HttpRuleParser.GetTokenLength(input, offset);
|
||||
if (itemLength == 0)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
result._name = input.Substring(offset, itemLength);
|
||||
offset += itemLength;
|
||||
|
||||
// = (no spaces)
|
||||
if (!ReadEqualsSign(input, ref offset))
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
string value;
|
||||
// value or "quoted value"
|
||||
itemLength = GetCookieValueLength(input, offset, out value);
|
||||
// The value may be empty
|
||||
result._value = input.Substring(offset, itemLength);
|
||||
offset += itemLength;
|
||||
|
||||
parsedValue = result;
|
||||
return offset - startIndex;
|
||||
}
|
||||
|
||||
// cookie-value = *cookie-octet / ( DQUOTE* cookie-octet DQUOTE )
|
||||
// cookie-octet = %x21 / %x23-2B / %x2D-3A / %x3C-5B / %x5D-7E
|
||||
// ; US-ASCII characters excluding CTLs, whitespace DQUOTE, comma, semicolon, and backslash
|
||||
internal static int GetCookieValueLength(string input, int startIndex, out string value)
|
||||
{
|
||||
Contract.Requires(input != null);
|
||||
Contract.Requires(startIndex >= 0);
|
||||
Contract.Ensures((Contract.Result<int>() >= 0) && (Contract.Result<int>() <= (input.Length - startIndex)));
|
||||
|
||||
value = null;
|
||||
if (startIndex >= input.Length)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
var inQuotes = false;
|
||||
var offset = startIndex;
|
||||
|
||||
if (input[offset] == '"')
|
||||
{
|
||||
inQuotes = true;
|
||||
offset++;
|
||||
}
|
||||
|
||||
while (offset < input.Length)
|
||||
{
|
||||
var c = input[offset];
|
||||
if (!IsCookieValueChar(c))
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
offset++;
|
||||
}
|
||||
|
||||
if (inQuotes)
|
||||
{
|
||||
if (offset == input.Length || input[offset] != '"')
|
||||
{
|
||||
return 0; // Missing final quote
|
||||
}
|
||||
offset++;
|
||||
}
|
||||
|
||||
int length = offset - startIndex;
|
||||
if (length == 0)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
value = input.Substring(startIndex, length);
|
||||
return length;
|
||||
}
|
||||
|
||||
private static bool ReadEqualsSign(string input, ref int offset)
|
||||
{
|
||||
// = (no spaces)
|
||||
if (offset >= input.Length || input[offset] != '=')
|
||||
{
|
||||
return false;
|
||||
}
|
||||
offset++;
|
||||
return true;
|
||||
}
|
||||
|
||||
// cookie-octet = %x21 / %x23-2B / %x2D-3A / %x3C-5B / %x5D-7E
|
||||
// ; US-ASCII characters excluding CTLs, whitespace DQUOTE, comma, semicolon, and backslash
|
||||
private static bool IsCookieValueChar(char c)
|
||||
{
|
||||
if (c < 0x21 || c > 0x7E)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
return !(c == '"' || c == ',' || c == ';' || c == '\\');
|
||||
}
|
||||
|
||||
internal static void CheckNameFormat([NotNull] string name, string parameterName)
|
||||
{
|
||||
if (HttpRuleParser.GetTokenLength(name, 0) != name.Length)
|
||||
{
|
||||
throw new ArgumentException("Invalid cookie name: " + name, parameterName);
|
||||
}
|
||||
}
|
||||
|
||||
internal static void CheckValueFormat([NotNull] string value, string parameterName)
|
||||
{
|
||||
string temp;
|
||||
if (GetCookieValueLength(value, 0, out temp) != value.Length)
|
||||
{
|
||||
throw new ArgumentException("Invalid cookie value: " + value, parameterName);
|
||||
}
|
||||
}
|
||||
|
||||
public override bool Equals(object obj)
|
||||
{
|
||||
var other = obj as CookieHeaderValue;
|
||||
|
||||
if (other == null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return string.Equals(_name, other._name, StringComparison.OrdinalIgnoreCase)
|
||||
&& string.Equals(_value, other._value, StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
public override int GetHashCode()
|
||||
{
|
||||
return _name.GetHashCode() ^ _value.GetHashCode();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,204 @@
|
|||
// Copyright (c) Microsoft Open Technologies, Inc. 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.Collections.Generic;
|
||||
using System.Diagnostics.Contracts;
|
||||
|
||||
namespace Microsoft.Net.Http.Headers
|
||||
{
|
||||
public class EntityTagHeaderValue
|
||||
{
|
||||
// Note that the ETag header does not allow a * but we're not that strict: We allow both '*' and ETag values in a single value.
|
||||
// We can't guarantee that a single parsed value will be used directly in an ETag header.
|
||||
private static readonly HttpHeaderParser<EntityTagHeaderValue> SingleValueParser
|
||||
= new GenericHeaderParser<EntityTagHeaderValue>(false, GetEntityTagLength);
|
||||
// Note that if multiple ETag values are allowed (e.g. 'If-Match', 'If-None-Match'), according to the RFC
|
||||
// the value must either be '*' or a list of ETag values. It's not allowed to have both '*' and a list of
|
||||
// ETag values. We're not that strict: We allow both '*' and ETag values in a list. If the server sends such
|
||||
// an invalid list, we want to be able to represent it using the corresponding header property.
|
||||
private static readonly HttpHeaderParser<EntityTagHeaderValue> MultipleValueParser
|
||||
= new GenericHeaderParser<EntityTagHeaderValue>(true, GetEntityTagLength);
|
||||
|
||||
private static EntityTagHeaderValue AnyType;
|
||||
|
||||
private string _tag;
|
||||
private bool _isWeak;
|
||||
|
||||
private EntityTagHeaderValue()
|
||||
{
|
||||
// Used by the parser to create a new instance of this type.
|
||||
}
|
||||
|
||||
public EntityTagHeaderValue(string tag)
|
||||
: this(tag, false)
|
||||
{
|
||||
}
|
||||
|
||||
public EntityTagHeaderValue(string tag, bool isWeak)
|
||||
{
|
||||
if (string.IsNullOrEmpty(tag))
|
||||
{
|
||||
throw new ArgumentException("An empty string is not allowed.", "tag");
|
||||
}
|
||||
|
||||
int length = 0;
|
||||
if (!isWeak && string.Equals(tag, "*", StringComparison.Ordinal))
|
||||
{
|
||||
// * is valid, but W/* isn't.
|
||||
_tag = tag;
|
||||
}
|
||||
else if ((HttpRuleParser.GetQuotedStringLength(tag, 0, out length) != HttpParseResult.Parsed) ||
|
||||
(length != tag.Length))
|
||||
{
|
||||
// Note that we don't allow 'W/' prefixes for weak ETags in the 'tag' parameter. If the user wants to
|
||||
// add a weak ETag, he can set 'isWeak' to true.
|
||||
throw new FormatException("Invalid ETag name");
|
||||
}
|
||||
|
||||
_tag = tag;
|
||||
_isWeak = isWeak;
|
||||
}
|
||||
|
||||
public static EntityTagHeaderValue Any
|
||||
{
|
||||
get
|
||||
{
|
||||
if (AnyType == null)
|
||||
{
|
||||
AnyType = new EntityTagHeaderValue();
|
||||
AnyType._tag = "*";
|
||||
AnyType._isWeak = false;
|
||||
}
|
||||
return AnyType;
|
||||
}
|
||||
}
|
||||
|
||||
public string Tag
|
||||
{
|
||||
get { return _tag; }
|
||||
}
|
||||
|
||||
public bool IsWeak
|
||||
{
|
||||
get { return _isWeak; }
|
||||
}
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
if (_isWeak)
|
||||
{
|
||||
return "W/" + _tag;
|
||||
}
|
||||
return _tag;
|
||||
}
|
||||
|
||||
public override bool Equals(object obj)
|
||||
{
|
||||
var other = obj as EntityTagHeaderValue;
|
||||
|
||||
if (other == null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// Since the tag is a quoted-string we treat it case-sensitive.
|
||||
return ((_isWeak == other._isWeak) && (string.CompareOrdinal(_tag, other._tag) == 0));
|
||||
}
|
||||
|
||||
public override int GetHashCode()
|
||||
{
|
||||
// Since the tag is a quoted-string we treat it case-sensitive.
|
||||
return _tag.GetHashCode() ^ _isWeak.GetHashCode();
|
||||
}
|
||||
|
||||
public static EntityTagHeaderValue Parse(string input)
|
||||
{
|
||||
var index = 0;
|
||||
return SingleValueParser.ParseValue(input, ref index);
|
||||
}
|
||||
|
||||
public static bool TryParse(string input, out EntityTagHeaderValue parsedValue)
|
||||
{
|
||||
var index = 0;
|
||||
return SingleValueParser.TryParseValue(input, ref index, out parsedValue);
|
||||
}
|
||||
|
||||
public static IList<EntityTagHeaderValue> ParseList(IList<string> inputs)
|
||||
{
|
||||
return MultipleValueParser.ParseValues(inputs);
|
||||
}
|
||||
|
||||
public static bool TryParseList(IList<string> inputs, out IList<EntityTagHeaderValue> parsedValues)
|
||||
{
|
||||
return MultipleValueParser.TryParseValues(inputs, out parsedValues);
|
||||
}
|
||||
|
||||
internal static int GetEntityTagLength(string input, int startIndex, out EntityTagHeaderValue parsedValue)
|
||||
{
|
||||
Contract.Requires(startIndex >= 0);
|
||||
|
||||
parsedValue = null;
|
||||
|
||||
if (string.IsNullOrEmpty(input) || (startIndex >= input.Length))
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Caller must remove leading whitespaces. If not, we'll return 0.
|
||||
var isWeak = false;
|
||||
var current = startIndex;
|
||||
|
||||
var firstChar = input[startIndex];
|
||||
if (firstChar == '*')
|
||||
{
|
||||
// We have '*' value, indicating "any" ETag.
|
||||
parsedValue = Any;
|
||||
current++;
|
||||
}
|
||||
else
|
||||
{
|
||||
// The RFC defines 'W/' as prefix, but we'll be flexible and also accept lower-case 'w'.
|
||||
if ((firstChar == 'W') || (firstChar == 'w'))
|
||||
{
|
||||
current++;
|
||||
// We need at least 3 more chars: the '/' character followed by two quotes.
|
||||
if ((current + 2 >= input.Length) || (input[current] != '/'))
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
isWeak = true;
|
||||
current++; // we have a weak-entity tag.
|
||||
current = current + HttpRuleParser.GetWhitespaceLength(input, current);
|
||||
}
|
||||
|
||||
var tagStartIndex = current;
|
||||
var tagLength = 0;
|
||||
if (HttpRuleParser.GetQuotedStringLength(input, current, out tagLength) != HttpParseResult.Parsed)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
parsedValue = new EntityTagHeaderValue();
|
||||
if (tagLength == input.Length)
|
||||
{
|
||||
// Most of the time we'll have strong ETags without leading/trailing whitespaces.
|
||||
Contract.Assert(startIndex == 0);
|
||||
Contract.Assert(!isWeak);
|
||||
parsedValue._tag = input;
|
||||
parsedValue._isWeak = false;
|
||||
}
|
||||
else
|
||||
{
|
||||
parsedValue._tag = input.Substring(tagStartIndex, tagLength);
|
||||
parsedValue._isWeak = isWeak;
|
||||
}
|
||||
|
||||
current = current + tagLength;
|
||||
}
|
||||
current = current + HttpRuleParser.GetWhitespaceLength(input, current);
|
||||
|
||||
return current - startIndex;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,23 @@
|
|||
// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved.
|
||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
namespace Microsoft.Net.Http.Headers
|
||||
{
|
||||
internal sealed class GenericHeaderParser<T> : BaseHeaderParser<T>
|
||||
{
|
||||
internal delegate int GetParsedValueLengthDelegate(string value, int startIndex, out T parsedValue);
|
||||
|
||||
private GetParsedValueLengthDelegate _getParsedValueLength;
|
||||
|
||||
internal GenericHeaderParser(bool supportsMultipleValues, [NotNull] GetParsedValueLengthDelegate getParsedValueLength)
|
||||
: base(supportsMultipleValues)
|
||||
{
|
||||
_getParsedValueLength = getParsedValueLength;
|
||||
}
|
||||
|
||||
protected override int GetParsedValueLength(string value, int startIndex, out T parsedValue)
|
||||
{
|
||||
return _getParsedValueLength(value, startIndex, out parsedValue);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,59 @@
|
|||
// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved.
|
||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
namespace Microsoft.Net.Http.Headers
|
||||
{
|
||||
public static class HeaderNames
|
||||
{
|
||||
public const string Accept = "Accept";
|
||||
public const string AcceptCharset = "Accept-Charset";
|
||||
public const string AcceptEncoding = "Accept-Encoding";
|
||||
public const string AcceptLanguage = "Accept-Language";
|
||||
public const string AcceptRanges = "Accept-Ranges";
|
||||
public const string Age = "Age";
|
||||
public const string Allow = "Allow";
|
||||
public const string Authorization = "Authorization";
|
||||
public const string CacheControl = "Cache-Control";
|
||||
public const string Connection = "Connection";
|
||||
public const string ContentDisposition = "Content-Disposition";
|
||||
public const string ContentEncoding = "Content-Encoding";
|
||||
public const string ContentLanguage = "Content-Language";
|
||||
public const string ContentLength = "Content-Length";
|
||||
public const string ContentLocation = "Content-Location";
|
||||
public const string ContentMD5 = "ContentMD5";
|
||||
public const string ContentRange = "Content-Range";
|
||||
public const string ContentType = "Content-Type";
|
||||
public const string Cookie = "Cookie";
|
||||
public const string Date = "Date";
|
||||
public const string ETag = "ETag";
|
||||
public const string Expires = "Expires";
|
||||
public const string Expect = "Expect";
|
||||
public const string From = "From";
|
||||
public const string Host = "Host";
|
||||
public const string IfMatch = "If-Match";
|
||||
public const string IfModifiedSince = "If-Modified-Since";
|
||||
public const string IfNoneMatch = "If-None-Match";
|
||||
public const string IfRange = "If-Range";
|
||||
public const string IfUnmodifiedSince = "If-Unmodified-Since";
|
||||
public const string LastModified = "Last-Modified";
|
||||
public const string Location = "Location";
|
||||
public const string MaxForwards = "Max-Forwards";
|
||||
public const string Pragma = "Pragma";
|
||||
public const string ProxyAuthenticate = "Proxy-Authenticate";
|
||||
public const string ProxyAuthorization = "Proxy-Authorization";
|
||||
public const string Range = "Range";
|
||||
public const string Referer = "Referer";
|
||||
public const string RetryAfter = "Retry-After";
|
||||
public const string Server = "Server";
|
||||
public const string SetCookie = "Set-Cookie";
|
||||
public const string TE = "TE";
|
||||
public const string Trailer = "Trailer";
|
||||
public const string TransferEncoding = "Transfer-Encoding";
|
||||
public const string Upgrade = "Upgrade";
|
||||
public const string UserAgent = "User-Agent";
|
||||
public const string Vary = "Vary";
|
||||
public const string Via = "Via";
|
||||
public const string Warning = "Warning";
|
||||
public const string WWWAuthenticate = "WWW-Authenticate";
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,18 @@
|
|||
// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved.
|
||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
namespace Microsoft.Net.Http.Headers
|
||||
{
|
||||
public static class HeaderQuality
|
||||
{
|
||||
/// <summary>
|
||||
/// Quality factor to indicate a perfect match.
|
||||
/// </summary>
|
||||
public const double Match = 1.0;
|
||||
|
||||
/// <summary>
|
||||
/// Quality factor to indicate no match.
|
||||
/// </summary>
|
||||
public const double NoMatch = 0.0;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,232 @@
|
|||
// Copyright (c) Microsoft Open Technologies, Inc. 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.Collections.Generic;
|
||||
using System.Diagnostics.Contracts;
|
||||
using System.Globalization;
|
||||
|
||||
namespace Microsoft.Net.Http.Headers
|
||||
{
|
||||
public static class HeaderUtilities
|
||||
{
|
||||
private const string QualityName = "q";
|
||||
internal const string BytesUnit = "bytes";
|
||||
|
||||
internal static void SetQuality(ICollection<NameValueHeaderValue> parameters, double? value)
|
||||
{
|
||||
Contract.Requires(parameters != null);
|
||||
|
||||
var qualityParameter = NameValueHeaderValue.Find(parameters, QualityName);
|
||||
if (value.HasValue)
|
||||
{
|
||||
// Note that even if we check the value here, we can't prevent a user from adding an invalid quality
|
||||
// value using Parameters.Add(). Even if we would prevent the user from adding an invalid value
|
||||
// using Parameters.Add() he could always add invalid values using HttpHeaders.AddWithoutValidation().
|
||||
// So this check is really for convenience to show users that they're trying to add an invalid
|
||||
// value.
|
||||
if ((value < 0) || (value > 1))
|
||||
{
|
||||
throw new ArgumentOutOfRangeException("value");
|
||||
}
|
||||
|
||||
var qualityString = ((double)value).ToString("0.0##", NumberFormatInfo.InvariantInfo);
|
||||
if (qualityParameter != null)
|
||||
{
|
||||
qualityParameter.Value = qualityString;
|
||||
}
|
||||
else
|
||||
{
|
||||
parameters.Add(new NameValueHeaderValue(QualityName, qualityString));
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// Remove quality parameter
|
||||
if (qualityParameter != null)
|
||||
{
|
||||
parameters.Remove(qualityParameter);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
internal static double? GetQuality(ICollection<NameValueHeaderValue> parameters)
|
||||
{
|
||||
Contract.Requires(parameters != null);
|
||||
|
||||
var qualityParameter = NameValueHeaderValue.Find(parameters, QualityName);
|
||||
if (qualityParameter != null)
|
||||
{
|
||||
// Note that the RFC requires decimal '.' regardless of the culture. I.e. using ',' as decimal
|
||||
// separator is considered invalid (even if the current culture would allow it).
|
||||
double qualityValue;
|
||||
if (double.TryParse(qualityParameter.Value, NumberStyles.AllowDecimalPoint,
|
||||
NumberFormatInfo.InvariantInfo, out qualityValue))
|
||||
{
|
||||
return qualityValue;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
internal static void CheckValidToken(string value, string parameterName)
|
||||
{
|
||||
if (string.IsNullOrEmpty(value))
|
||||
{
|
||||
throw new ArgumentException("An empty string is not allowed.", parameterName);
|
||||
}
|
||||
|
||||
if (HttpRuleParser.GetTokenLength(value, 0) != value.Length)
|
||||
{
|
||||
throw new FormatException(string.Format(CultureInfo.InvariantCulture, "Invalid token '{0}.", value));
|
||||
}
|
||||
}
|
||||
|
||||
internal static void CheckValidQuotedString(string value, string parameterName)
|
||||
{
|
||||
if (string.IsNullOrEmpty(value))
|
||||
{
|
||||
throw new ArgumentException("An empty string is not allowed.", parameterName);
|
||||
}
|
||||
|
||||
int length;
|
||||
if ((HttpRuleParser.GetQuotedStringLength(value, 0, out length) != HttpParseResult.Parsed) ||
|
||||
(length != value.Length)) // no trailing spaces allowed
|
||||
{
|
||||
throw new FormatException(string.Format(CultureInfo.InvariantCulture, "Invalid quoted string '{0}'.", value));
|
||||
}
|
||||
}
|
||||
|
||||
internal static bool AreEqualCollections<T>(ICollection<T> x, ICollection<T> y)
|
||||
{
|
||||
return AreEqualCollections(x, y, null);
|
||||
}
|
||||
|
||||
internal static bool AreEqualCollections<T>(ICollection<T> x, ICollection<T> y, IEqualityComparer<T> comparer)
|
||||
{
|
||||
if (x == null)
|
||||
{
|
||||
return (y == null) || (y.Count == 0);
|
||||
}
|
||||
|
||||
if (y == null)
|
||||
{
|
||||
return (x.Count == 0);
|
||||
}
|
||||
|
||||
if (x.Count != y.Count)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (x.Count == 0)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
// We have two unordered lists. So comparison is an O(n*m) operation which is expensive. Usually
|
||||
// headers have 1-2 parameters (if any), so this comparison shouldn't be too expensive.
|
||||
var alreadyFound = new bool[x.Count];
|
||||
var i = 0;
|
||||
foreach (var xItem in x)
|
||||
{
|
||||
Contract.Assert(xItem != null);
|
||||
|
||||
i = 0;
|
||||
var found = false;
|
||||
foreach (var yItem in y)
|
||||
{
|
||||
if (!alreadyFound[i])
|
||||
{
|
||||
if (((comparer == null) && xItem.Equals(yItem)) ||
|
||||
((comparer != null) && comparer.Equals(xItem, yItem)))
|
||||
{
|
||||
alreadyFound[i] = true;
|
||||
found = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
i++;
|
||||
}
|
||||
|
||||
if (!found)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Since we never re-use a "found" value in 'y', we expecte 'alreadyFound' to have all fields set to 'true'.
|
||||
// Otherwise the two collections can't be equal and we should not get here.
|
||||
Contract.Assert(Contract.ForAll(alreadyFound, value => { return value; }),
|
||||
"Expected all values in 'alreadyFound' to be true since collections are considered equal.");
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
internal static int GetNextNonEmptyOrWhitespaceIndex(string input, int startIndex, bool skipEmptyValues,
|
||||
out bool separatorFound)
|
||||
{
|
||||
Contract.Requires(input != null);
|
||||
Contract.Requires(startIndex <= input.Length); // it's OK if index == value.Length.
|
||||
|
||||
separatorFound = false;
|
||||
var current = startIndex + HttpRuleParser.GetWhitespaceLength(input, startIndex);
|
||||
|
||||
if ((current == input.Length) || (input[current] != ','))
|
||||
{
|
||||
return current;
|
||||
}
|
||||
|
||||
// If we have a separator, skip the separator and all following whitespaces. If we support
|
||||
// empty values, continue until the current character is neither a separator nor a whitespace.
|
||||
separatorFound = true;
|
||||
current++; // skip delimiter.
|
||||
current = current + HttpRuleParser.GetWhitespaceLength(input, current);
|
||||
|
||||
if (skipEmptyValues)
|
||||
{
|
||||
while ((current < input.Length) && (input[current] == ','))
|
||||
{
|
||||
current++; // skip delimiter.
|
||||
current = current + HttpRuleParser.GetWhitespaceLength(input, current);
|
||||
}
|
||||
}
|
||||
|
||||
return current;
|
||||
}
|
||||
|
||||
internal static bool TryParseInt32(string value, out int result)
|
||||
{
|
||||
return int.TryParse(value, NumberStyles.None, NumberFormatInfo.InvariantInfo, out result);
|
||||
}
|
||||
|
||||
public static bool TryParseInt64(string value, out long result)
|
||||
{
|
||||
return long.TryParse(value, NumberStyles.None, NumberFormatInfo.InvariantInfo, out result);
|
||||
}
|
||||
|
||||
public static string FormatInt64(long value)
|
||||
{
|
||||
return value.ToString(CultureInfo.InvariantCulture);
|
||||
}
|
||||
|
||||
public static bool TryParseDate(string input, out DateTimeOffset result)
|
||||
{
|
||||
return HttpRuleParser.TryStringToDate(input, out result);
|
||||
}
|
||||
|
||||
public static string FormatDate(DateTimeOffset dateTime)
|
||||
{
|
||||
return HttpRuleParser.DateToString(dateTime);
|
||||
}
|
||||
|
||||
public static string RemoveQuotes(string input)
|
||||
{
|
||||
if (!string.IsNullOrEmpty(input) && input.Length >= 2 && input[0] == '"' && input[input.Length - 1] == '"')
|
||||
{
|
||||
input = input.Substring(1, input.Length - 2);
|
||||
}
|
||||
return input;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,137 @@
|
|||
// Copyright (c) Microsoft Open Technologies, Inc. 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.Collections;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics.Contracts;
|
||||
using System.Globalization;
|
||||
|
||||
namespace Microsoft.Net.Http.Headers
|
||||
{
|
||||
internal abstract class HttpHeaderParser<T>
|
||||
{
|
||||
private bool _supportsMultipleValues;
|
||||
|
||||
protected HttpHeaderParser(bool supportsMultipleValues)
|
||||
{
|
||||
_supportsMultipleValues = supportsMultipleValues;
|
||||
}
|
||||
|
||||
public bool SupportsMultipleValues
|
||||
{
|
||||
get { return _supportsMultipleValues; }
|
||||
}
|
||||
|
||||
// If a parser supports multiple values, a call to ParseValue/TryParseValue should return a value for 'index'
|
||||
// pointing to the next non-whitespace character after a delimiter. E.g. if called with a start index of 0
|
||||
// for string "value , second_value", then after the call completes, 'index' must point to 's', i.e. the first
|
||||
// non-whitespace after the separator ','.
|
||||
public abstract bool TryParseValue(string value, ref int index, out T parsedValue);
|
||||
|
||||
public T ParseValue(string value, ref int index)
|
||||
{
|
||||
// Index may be value.Length (e.g. both 0). This may be allowed for some headers (e.g. Accept but not
|
||||
// allowed by others (e.g. Content-Length). The parser has to decide if this is valid or not.
|
||||
Contract.Requires((value == null) || ((index >= 0) && (index <= value.Length)));
|
||||
|
||||
// If a parser returns 'null', it means there was no value, but that's valid (e.g. "Accept: "). The caller
|
||||
// can ignore the value.
|
||||
T result = default(T);
|
||||
if (!TryParseValue(value, ref index, out result))
|
||||
{
|
||||
throw new FormatException(string.Format(CultureInfo.InvariantCulture, "Invalid value '{0}'.",
|
||||
value == null ? "<null>" : value.Substring(index)));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
public virtual bool TryParseValues(IList<string> values, out IList<T> parsedValues)
|
||||
{
|
||||
Contract.Assert(_supportsMultipleValues);
|
||||
// If a parser returns an empty list, it means there was no value, but that's valid (e.g. "Accept: "). The caller
|
||||
// can ignore the value.
|
||||
parsedValues = null;
|
||||
var results = new List<T>();
|
||||
if (values == null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
foreach (var value in values)
|
||||
{
|
||||
int index = 0;
|
||||
|
||||
while (!string.IsNullOrEmpty(value) && index < value.Length)
|
||||
{
|
||||
T output;
|
||||
if (TryParseValue(value, ref index, out output))
|
||||
{
|
||||
// The entry may not contain an actual value, like " , "
|
||||
if (output != null)
|
||||
{
|
||||
results.Add(output);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (results.Count > 0)
|
||||
{
|
||||
parsedValues = results;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public IList<T> ParseValues(IList<string> values)
|
||||
{
|
||||
Contract.Assert(_supportsMultipleValues);
|
||||
// If a parser returns an empty list, it means there was no value, but that's valid (e.g. "Accept: "). The caller
|
||||
// can ignore the value.
|
||||
var parsedValues = new List<T>();
|
||||
if (values == null)
|
||||
{
|
||||
return parsedValues;
|
||||
}
|
||||
foreach (var value in values)
|
||||
{
|
||||
int index = 0;
|
||||
|
||||
while (!string.IsNullOrEmpty(value) && index < value.Length)
|
||||
{
|
||||
T output;
|
||||
if (TryParseValue(value, ref index, out output))
|
||||
{
|
||||
// The entry may not contain an actual value, like " , "
|
||||
if (output != null)
|
||||
{
|
||||
parsedValues.Add(output);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new FormatException(string.Format(CultureInfo.InvariantCulture, "Invalid values '{0}'.",
|
||||
values == null ? "<null>" : value.Substring(index)));
|
||||
}
|
||||
}
|
||||
}
|
||||
return parsedValues;
|
||||
}
|
||||
|
||||
// If ValueType is a custom header value type (e.g. NameValueHeaderValue) it implements ToString() correctly.
|
||||
// However for existing types like int, byte[], DateTimeOffset we can't override ToString(). Therefore the
|
||||
// parser provides a ToString() virtual method that can be overridden by derived types to correctly serialize
|
||||
// values (e.g. byte[] to Base64 encoded string).
|
||||
// The default implementation is to just call ToString() on the value itself which is the right thing to do
|
||||
// for most headers (custom types, string, etc.).
|
||||
public virtual string ToString(object value)
|
||||
{
|
||||
Contract.Requires(value != null);
|
||||
|
||||
return value.ToString();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved.
|
||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
namespace Microsoft.Net.Http.Headers
|
||||
{
|
||||
internal enum HttpParseResult
|
||||
{
|
||||
Parsed,
|
||||
NotParsed,
|
||||
InvalidFormat,
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,352 @@
|
|||
// Copyright (c) Microsoft Open Technologies, Inc. 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.Diagnostics.Contracts;
|
||||
using System.Globalization;
|
||||
using System.Text;
|
||||
|
||||
namespace Microsoft.Net.Http.Headers
|
||||
{
|
||||
internal static class HttpRuleParser
|
||||
{
|
||||
private static readonly bool[] TokenChars;
|
||||
private const int MaxNestedCount = 5;
|
||||
private static readonly string[] DateFormats = new string[] {
|
||||
// "r", // RFC 1123, required output format but too strict for input
|
||||
"ddd, d MMM yyyy H:m:s 'GMT'", // RFC 1123 (r, except it allows both 1 and 01 for date and time)
|
||||
"ddd, d MMM yyyy H:m:s", // RFC 1123, no zone - assume GMT
|
||||
"d MMM yyyy H:m:s 'GMT'", // RFC 1123, no day-of-week
|
||||
"d MMM yyyy H:m:s", // RFC 1123, no day-of-week, no zone
|
||||
"ddd, d MMM yy H:m:s 'GMT'", // RFC 1123, short year
|
||||
"ddd, d MMM yy H:m:s", // RFC 1123, short year, no zone
|
||||
"d MMM yy H:m:s 'GMT'", // RFC 1123, no day-of-week, short year
|
||||
"d MMM yy H:m:s", // RFC 1123, no day-of-week, short year, no zone
|
||||
|
||||
"dddd, d'-'MMM'-'yy H:m:s 'GMT'", // RFC 850
|
||||
"dddd, d'-'MMM'-'yy H:m:s", // RFC 850 no zone
|
||||
"ddd MMM d H:m:s yyyy", // ANSI C's asctime() format
|
||||
|
||||
"ddd, d MMM yyyy H:m:s zzz", // RFC 5322
|
||||
"ddd, d MMM yyyy H:m:s", // RFC 5322 no zone
|
||||
"d MMM yyyy H:m:s zzz", // RFC 5322 no day-of-week
|
||||
"d MMM yyyy H:m:s", // RFC 5322 no day-of-week, no zone
|
||||
};
|
||||
|
||||
internal const char CR = '\r';
|
||||
internal const char LF = '\n';
|
||||
internal const char SP = ' ';
|
||||
internal const char Tab = '\t';
|
||||
internal const int MaxInt64Digits = 19;
|
||||
internal const int MaxInt32Digits = 10;
|
||||
|
||||
// iso-8859-1, Western European (ISO)
|
||||
internal static readonly Encoding DefaultHttpEncoding = Encoding.GetEncoding(28591);
|
||||
|
||||
static HttpRuleParser()
|
||||
{
|
||||
// token = 1*<any CHAR except CTLs or separators>
|
||||
// CTL = <any US-ASCII control character (octets 0 - 31) and DEL (127)>
|
||||
|
||||
TokenChars = new bool[128]; // everything is false
|
||||
|
||||
for (int i = 33; i < 127; i++) // skip Space (32) & DEL (127)
|
||||
{
|
||||
TokenChars[i] = true;
|
||||
}
|
||||
|
||||
// remove separators: these are not valid token characters
|
||||
TokenChars[(byte)'('] = false;
|
||||
TokenChars[(byte)')'] = false;
|
||||
TokenChars[(byte)'<'] = false;
|
||||
TokenChars[(byte)'>'] = false;
|
||||
TokenChars[(byte)'@'] = false;
|
||||
TokenChars[(byte)','] = false;
|
||||
TokenChars[(byte)';'] = false;
|
||||
TokenChars[(byte)':'] = false;
|
||||
TokenChars[(byte)'\\'] = false;
|
||||
TokenChars[(byte)'"'] = false;
|
||||
TokenChars[(byte)'/'] = false;
|
||||
TokenChars[(byte)'['] = false;
|
||||
TokenChars[(byte)']'] = false;
|
||||
TokenChars[(byte)'?'] = false;
|
||||
TokenChars[(byte)'='] = false;
|
||||
TokenChars[(byte)'{'] = false;
|
||||
TokenChars[(byte)'}'] = false;
|
||||
}
|
||||
|
||||
internal static bool IsTokenChar(char character)
|
||||
{
|
||||
// Must be between 'space' (32) and 'DEL' (127)
|
||||
if (character > 127)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return TokenChars[character];
|
||||
}
|
||||
|
||||
[Pure]
|
||||
internal static int GetTokenLength(string input, int startIndex)
|
||||
{
|
||||
Contract.Requires(input != null);
|
||||
Contract.Ensures((Contract.Result<int>() >= 0) && (Contract.Result<int>() <= (input.Length - startIndex)));
|
||||
|
||||
if (startIndex >= input.Length)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
var current = startIndex;
|
||||
|
||||
while (current < input.Length)
|
||||
{
|
||||
if (!IsTokenChar(input[current]))
|
||||
{
|
||||
return current - startIndex;
|
||||
}
|
||||
current++;
|
||||
}
|
||||
return input.Length - startIndex;
|
||||
}
|
||||
|
||||
internal static int GetWhitespaceLength(string input, int startIndex)
|
||||
{
|
||||
Contract.Requires(input != null);
|
||||
Contract.Ensures((Contract.Result<int>() >= 0) && (Contract.Result<int>() <= (input.Length - startIndex)));
|
||||
|
||||
if (startIndex >= input.Length)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
var current = startIndex;
|
||||
|
||||
char c;
|
||||
while (current < input.Length)
|
||||
{
|
||||
c = input[current];
|
||||
|
||||
if ((c == SP) || (c == Tab))
|
||||
{
|
||||
current++;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (c == CR)
|
||||
{
|
||||
// If we have a #13 char, it must be followed by #10 and then at least one SP or HT.
|
||||
if ((current + 2 < input.Length) && (input[current + 1] == LF))
|
||||
{
|
||||
char spaceOrTab = input[current + 2];
|
||||
if ((spaceOrTab == SP) || (spaceOrTab == Tab))
|
||||
{
|
||||
current += 3;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return current - startIndex;
|
||||
}
|
||||
|
||||
// All characters between startIndex and the end of the string are LWS characters.
|
||||
return input.Length - startIndex;
|
||||
}
|
||||
|
||||
internal static int GetNumberLength(string input, int startIndex, bool allowDecimal)
|
||||
{
|
||||
Contract.Requires(input != null);
|
||||
Contract.Requires((startIndex >= 0) && (startIndex < input.Length));
|
||||
Contract.Ensures((Contract.Result<int>() >= 0) && (Contract.Result<int>() <= (input.Length - startIndex)));
|
||||
|
||||
var current = startIndex;
|
||||
char c;
|
||||
|
||||
// If decimal values are not allowed, we pretend to have read the '.' character already. I.e. if a dot is
|
||||
// found in the string, parsing will be aborted.
|
||||
var haveDot = !allowDecimal;
|
||||
|
||||
// The RFC doesn't allow decimal values starting with dot. I.e. value ".123" is invalid. It must be in the
|
||||
// form "0.123". Also, there are no negative values defined in the RFC. So we'll just parse non-negative
|
||||
// values.
|
||||
// The RFC only allows decimal dots not ',' characters as decimal separators. Therefore value "1,23" is
|
||||
// considered invalid and must be represented as "1.23".
|
||||
if (input[current] == '.')
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
while (current < input.Length)
|
||||
{
|
||||
c = input[current];
|
||||
if ((c >= '0') && (c <= '9'))
|
||||
{
|
||||
current++;
|
||||
}
|
||||
else if (!haveDot && (c == '.'))
|
||||
{
|
||||
// Note that value "1." is valid.
|
||||
haveDot = true;
|
||||
current++;
|
||||
}
|
||||
else
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return current - startIndex;
|
||||
}
|
||||
|
||||
internal static HttpParseResult GetQuotedStringLength(string input, int startIndex, out int length)
|
||||
{
|
||||
var nestedCount = 0;
|
||||
return GetExpressionLength(input, startIndex, '"', '"', false, ref nestedCount, out length);
|
||||
}
|
||||
|
||||
// quoted-pair = "\" CHAR
|
||||
// CHAR = <any US-ASCII character (octets 0 - 127)>
|
||||
internal static HttpParseResult GetQuotedPairLength(string input, int startIndex, out int length)
|
||||
{
|
||||
Contract.Requires(input != null);
|
||||
Contract.Requires((startIndex >= 0) && (startIndex < input.Length));
|
||||
Contract.Ensures((Contract.ValueAtReturn(out length) >= 0) &&
|
||||
(Contract.ValueAtReturn(out length) <= (input.Length - startIndex)));
|
||||
|
||||
length = 0;
|
||||
|
||||
if (input[startIndex] != '\\')
|
||||
{
|
||||
return HttpParseResult.NotParsed;
|
||||
}
|
||||
|
||||
// Quoted-char has 2 characters. Check wheter there are 2 chars left ('\' + char)
|
||||
// If so, check whether the character is in the range 0-127. If not, it's an invalid value.
|
||||
if ((startIndex + 2 > input.Length) || (input[startIndex + 1] > 127))
|
||||
{
|
||||
return HttpParseResult.InvalidFormat;
|
||||
}
|
||||
|
||||
// We don't care what the char next to '\' is.
|
||||
length = 2;
|
||||
return HttpParseResult.Parsed;
|
||||
}
|
||||
|
||||
internal static string DateToString(DateTimeOffset dateTime)
|
||||
{
|
||||
// Format according to RFC1123; 'r' uses invariant info (DateTimeFormatInfo.InvariantInfo)
|
||||
return dateTime.ToUniversalTime().ToString("r", CultureInfo.InvariantCulture);
|
||||
}
|
||||
|
||||
internal static bool TryStringToDate(string input, out DateTimeOffset result)
|
||||
{
|
||||
// Try the various date formats in the order listed above.
|
||||
// We should accept a wide verity of common formats, but only output RFC 1123 style dates.
|
||||
if (DateTimeOffset.TryParseExact(input, DateFormats, DateTimeFormatInfo.InvariantInfo,
|
||||
DateTimeStyles.AllowWhiteSpaces | DateTimeStyles.AssumeUniversal, out result))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
// TEXT = <any OCTET except CTLs, but including LWS>
|
||||
// LWS = [CRLF] 1*( SP | HT )
|
||||
// CTL = <any US-ASCII control character (octets 0 - 31) and DEL (127)>
|
||||
//
|
||||
// Since we don't really care about the content of a quoted string or comment, we're more tolerant and
|
||||
// allow these characters. We only want to find the delimiters ('"' for quoted string and '(', ')' for comment).
|
||||
//
|
||||
// 'nestedCount': Comments can be nested. We allow a depth of up to 5 nested comments, i.e. something like
|
||||
// "(((((comment)))))". If we wouldn't define a limit an attacker could send a comment with hundreds of nested
|
||||
// comments, resulting in a stack overflow exception. In addition having more than 1 nested comment (if any)
|
||||
// is unusual.
|
||||
private static HttpParseResult GetExpressionLength(string input, int startIndex, char openChar,
|
||||
char closeChar, bool supportsNesting, ref int nestedCount, out int length)
|
||||
{
|
||||
Contract.Requires(input != null);
|
||||
Contract.Requires((startIndex >= 0) && (startIndex < input.Length));
|
||||
Contract.Ensures((Contract.Result<HttpParseResult>() != HttpParseResult.Parsed) ||
|
||||
(Contract.ValueAtReturn<int>(out length) > 0));
|
||||
|
||||
length = 0;
|
||||
|
||||
if (input[startIndex] != openChar)
|
||||
{
|
||||
return HttpParseResult.NotParsed;
|
||||
}
|
||||
|
||||
var current = startIndex + 1; // Start parsing with the character next to the first open-char
|
||||
while (current < input.Length)
|
||||
{
|
||||
// Only check whether we have a quoted char, if we have at least 3 characters left to read (i.e.
|
||||
// quoted char + closing char). Otherwise the closing char may be considered part of the quoted char.
|
||||
var quotedPairLength = 0;
|
||||
if ((current + 2 < input.Length) &&
|
||||
(GetQuotedPairLength(input, current, out quotedPairLength) == HttpParseResult.Parsed))
|
||||
{
|
||||
// We ignore invalid quoted-pairs. Invalid quoted-pairs may mean that it looked like a quoted pair,
|
||||
// but we actually have a quoted-string: e.g. "\ü" ('\' followed by a char >127 - quoted-pair only
|
||||
// allows ASCII chars after '\'; qdtext allows both '\' and >127 chars).
|
||||
current = current + quotedPairLength;
|
||||
continue;
|
||||
}
|
||||
|
||||
// If we support nested expressions and we find an open-char, then parse the nested expressions.
|
||||
if (supportsNesting && (input[current] == openChar))
|
||||
{
|
||||
nestedCount++;
|
||||
try
|
||||
{
|
||||
// Check if we exceeded the number of nested calls.
|
||||
if (nestedCount > MaxNestedCount)
|
||||
{
|
||||
return HttpParseResult.InvalidFormat;
|
||||
}
|
||||
|
||||
var nestedLength = 0;
|
||||
HttpParseResult nestedResult = GetExpressionLength(input, current, openChar, closeChar,
|
||||
supportsNesting, ref nestedCount, out nestedLength);
|
||||
|
||||
switch (nestedResult)
|
||||
{
|
||||
case HttpParseResult.Parsed:
|
||||
current += nestedLength; // add the length of the nested expression and continue.
|
||||
break;
|
||||
|
||||
case HttpParseResult.NotParsed:
|
||||
Contract.Assert(false, "'NotParsed' is unexpected: We started nested expression " +
|
||||
"parsing, because we found the open-char. So either it's a valid nested " +
|
||||
"expression or it has invalid format.");
|
||||
break;
|
||||
|
||||
case HttpParseResult.InvalidFormat:
|
||||
// If the nested expression is invalid, we can't continue, so we fail with invalid format.
|
||||
return HttpParseResult.InvalidFormat;
|
||||
|
||||
default:
|
||||
Contract.Assert(false, "Unknown enum result: " + nestedResult);
|
||||
break;
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
nestedCount--;
|
||||
}
|
||||
}
|
||||
|
||||
if (input[current] == closeChar)
|
||||
{
|
||||
length = current - startIndex + 1;
|
||||
return HttpParseResult.Parsed;
|
||||
}
|
||||
current++;
|
||||
}
|
||||
|
||||
// We didn't see the final quote, therefore we have an invalid expression string.
|
||||
return HttpParseResult.InvalidFormat;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,408 @@
|
|||
// Copyright (c) Microsoft Open Technologies, Inc. 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.Collections.Generic;
|
||||
using System.Diagnostics.Contracts;
|
||||
using System.Globalization;
|
||||
using System.Text;
|
||||
|
||||
namespace Microsoft.Net.Http.Headers
|
||||
{
|
||||
public class MediaTypeHeaderValue
|
||||
{
|
||||
private const string CharsetString = "charset";
|
||||
private const string BoundaryString = "boundary";
|
||||
|
||||
private static readonly HttpHeaderParser<MediaTypeHeaderValue> SingleValueParser
|
||||
= new GenericHeaderParser<MediaTypeHeaderValue>(false, GetMediaTypeLength);
|
||||
private static readonly HttpHeaderParser<MediaTypeHeaderValue> MultipleValueParser
|
||||
= new GenericHeaderParser<MediaTypeHeaderValue>(true, GetMediaTypeLength);
|
||||
|
||||
// Use list instead of dictionary since we may have multiple parameters with the same name.
|
||||
private ICollection<NameValueHeaderValue> _parameters;
|
||||
private string _mediaType;
|
||||
|
||||
private MediaTypeHeaderValue()
|
||||
{
|
||||
// Used by the parser to create a new instance of this type.
|
||||
}
|
||||
|
||||
public MediaTypeHeaderValue(string mediaType)
|
||||
{
|
||||
CheckMediaTypeFormat(mediaType, "mediaType");
|
||||
_mediaType = mediaType;
|
||||
}
|
||||
|
||||
public MediaTypeHeaderValue(string mediaType, double quality)
|
||||
: this(mediaType)
|
||||
{
|
||||
Quality = quality;
|
||||
}
|
||||
|
||||
public string Charset
|
||||
{
|
||||
get
|
||||
{
|
||||
return NameValueHeaderValue.Find(_parameters, CharsetString)?.Value;
|
||||
}
|
||||
set
|
||||
{
|
||||
// We don't prevent a user from setting whitespace-only charsets. Like we can't prevent a user from
|
||||
// setting a non-existing charset.
|
||||
var charsetParameter = NameValueHeaderValue.Find(_parameters, CharsetString);
|
||||
if (string.IsNullOrEmpty(value))
|
||||
{
|
||||
// Remove charset parameter
|
||||
if (charsetParameter != null)
|
||||
{
|
||||
_parameters.Remove(charsetParameter);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if (charsetParameter != null)
|
||||
{
|
||||
charsetParameter.Value = value;
|
||||
}
|
||||
else
|
||||
{
|
||||
Parameters.Add(new NameValueHeaderValue(CharsetString, value));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public Encoding Encoding
|
||||
{
|
||||
get
|
||||
{
|
||||
var charset = Charset;
|
||||
if (!string.IsNullOrWhiteSpace(charset))
|
||||
{
|
||||
try
|
||||
{
|
||||
return Encoding.GetEncoding(charset);
|
||||
}
|
||||
catch (ArgumentException)
|
||||
{
|
||||
// Invalid or not supported
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
set
|
||||
{
|
||||
if (value == null)
|
||||
{
|
||||
Charset = null;
|
||||
}
|
||||
else
|
||||
{
|
||||
Charset = value.WebName;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public string Boundary
|
||||
{
|
||||
get
|
||||
{
|
||||
return NameValueHeaderValue.Find(_parameters, BoundaryString)?.Value;
|
||||
}
|
||||
set
|
||||
{
|
||||
var boundaryParameter = NameValueHeaderValue.Find(_parameters, BoundaryString);
|
||||
if (string.IsNullOrEmpty(value))
|
||||
{
|
||||
// Remove charset parameter
|
||||
if (boundaryParameter != null)
|
||||
{
|
||||
_parameters.Remove(boundaryParameter);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if (boundaryParameter != null)
|
||||
{
|
||||
boundaryParameter.Value = value;
|
||||
}
|
||||
else
|
||||
{
|
||||
Parameters.Add(new NameValueHeaderValue(BoundaryString, value));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public ICollection<NameValueHeaderValue> Parameters
|
||||
{
|
||||
get
|
||||
{
|
||||
if (_parameters == null)
|
||||
{
|
||||
_parameters = new ObjectCollection<NameValueHeaderValue>();
|
||||
}
|
||||
return _parameters;
|
||||
}
|
||||
}
|
||||
|
||||
public double? Quality
|
||||
{
|
||||
get { return HeaderUtilities.GetQuality(Parameters); }
|
||||
set { HeaderUtilities.SetQuality(Parameters, value); }
|
||||
}
|
||||
|
||||
public string MediaType
|
||||
{
|
||||
get { return _mediaType; }
|
||||
set
|
||||
{
|
||||
CheckMediaTypeFormat(value, "value");
|
||||
_mediaType = value;
|
||||
}
|
||||
}
|
||||
|
||||
public string Type
|
||||
{
|
||||
get
|
||||
{
|
||||
return _mediaType.Substring(0, _mediaType.IndexOf('/'));
|
||||
}
|
||||
}
|
||||
|
||||
public string SubType
|
||||
{
|
||||
get
|
||||
{
|
||||
return _mediaType.Substring(_mediaType.IndexOf('/') + 1);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// MediaType = "*/*"
|
||||
/// </summary>
|
||||
public bool MatchesAllTypes
|
||||
{
|
||||
get
|
||||
{
|
||||
return MediaType.Equals("*/*", StringComparison.Ordinal);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// SubType = "*"
|
||||
/// </summary>
|
||||
public bool MatchesAllSubTypes
|
||||
{
|
||||
get
|
||||
{
|
||||
return SubType.Equals("*", StringComparison.Ordinal);
|
||||
}
|
||||
}
|
||||
|
||||
public bool IsSubsetOf(MediaTypeHeaderValue otherMediaType)
|
||||
{
|
||||
if (otherMediaType == null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!Type.Equals(otherMediaType.Type, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
if (!otherMediaType.MatchesAllTypes)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
else if (!SubType.Equals(otherMediaType.SubType, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
if (!otherMediaType.MatchesAllSubTypes)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if (Parameters != null)
|
||||
{
|
||||
if (Parameters.Count != 0 && (otherMediaType.Parameters == null || otherMediaType.Parameters.Count == 0))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// Make sure all parameters listed locally are listed in the other one. The other one may have additional parameters.
|
||||
foreach (var param in _parameters)
|
||||
{
|
||||
var otherParam = NameValueHeaderValue.Find(otherMediaType._parameters, param.Name);
|
||||
if (otherParam == null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
if (!string.Equals(param.Value, otherParam.Value, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
return _mediaType + NameValueHeaderValue.ToString(_parameters, ';', true);
|
||||
}
|
||||
|
||||
public override bool Equals(object obj)
|
||||
{
|
||||
var other = obj as MediaTypeHeaderValue;
|
||||
|
||||
if (other == null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return (string.Compare(_mediaType, other._mediaType, StringComparison.OrdinalIgnoreCase) == 0) &&
|
||||
HeaderUtilities.AreEqualCollections(_parameters, other._parameters);
|
||||
}
|
||||
|
||||
public override int GetHashCode()
|
||||
{
|
||||
// The media-type string is case-insensitive.
|
||||
return StringComparer.OrdinalIgnoreCase.GetHashCode(_mediaType) ^ NameValueHeaderValue.GetHashCode(_parameters);
|
||||
}
|
||||
|
||||
public static MediaTypeHeaderValue Parse(string input)
|
||||
{
|
||||
var index = 0;
|
||||
return SingleValueParser.ParseValue(input, ref index);
|
||||
}
|
||||
|
||||
public static bool TryParse(string input, out MediaTypeHeaderValue parsedValue)
|
||||
{
|
||||
var index = 0;
|
||||
return SingleValueParser.TryParseValue(input, ref index, out parsedValue);
|
||||
}
|
||||
|
||||
public static IList<MediaTypeHeaderValue> ParseList(IList<string> inputs)
|
||||
{
|
||||
return MultipleValueParser.ParseValues(inputs);
|
||||
}
|
||||
|
||||
public static bool TryParseList(IList<string> inputs, out IList<MediaTypeHeaderValue> parsedValues)
|
||||
{
|
||||
return MultipleValueParser.TryParseValues(inputs, out parsedValues);
|
||||
}
|
||||
|
||||
private static int GetMediaTypeLength(string input, int startIndex, out MediaTypeHeaderValue parsedValue)
|
||||
{
|
||||
Contract.Requires(startIndex >= 0);
|
||||
|
||||
parsedValue = null;
|
||||
|
||||
if (string.IsNullOrEmpty(input) || (startIndex >= input.Length))
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Caller must remove leading whitespaces. If not, we'll return 0.
|
||||
string mediaType = null;
|
||||
var mediaTypeLength = MediaTypeHeaderValue.GetMediaTypeExpressionLength(input, startIndex, out mediaType);
|
||||
|
||||
if (mediaTypeLength == 0)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
var current = startIndex + mediaTypeLength;
|
||||
current = current + HttpRuleParser.GetWhitespaceLength(input, current);
|
||||
MediaTypeHeaderValue mediaTypeHeader = null;
|
||||
|
||||
// If we're not done and we have a parameter delimiter, then we have a list of parameters.
|
||||
if ((current < input.Length) && (input[current] == ';'))
|
||||
{
|
||||
mediaTypeHeader = new MediaTypeHeaderValue();
|
||||
mediaTypeHeader._mediaType = mediaType;
|
||||
|
||||
current++; // skip delimiter.
|
||||
var parameterLength = NameValueHeaderValue.GetNameValueListLength(input, current, ';',
|
||||
mediaTypeHeader.Parameters);
|
||||
|
||||
parsedValue = mediaTypeHeader;
|
||||
return current + parameterLength - startIndex;
|
||||
}
|
||||
|
||||
// We have a media type without parameters.
|
||||
mediaTypeHeader = new MediaTypeHeaderValue();
|
||||
mediaTypeHeader._mediaType = mediaType;
|
||||
parsedValue = mediaTypeHeader;
|
||||
return current - startIndex;
|
||||
}
|
||||
|
||||
private static int GetMediaTypeExpressionLength(string input, int startIndex, out string mediaType)
|
||||
{
|
||||
Contract.Requires((input != null) && (input.Length > 0) && (startIndex < input.Length));
|
||||
|
||||
// This method just parses the "type/subtype" string, it does not parse parameters.
|
||||
mediaType = null;
|
||||
|
||||
// Parse the type, i.e. <type> in media type string "<type>/<subtype>; param1=value1; param2=value2"
|
||||
var typeLength = HttpRuleParser.GetTokenLength(input, startIndex);
|
||||
|
||||
if (typeLength == 0)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
var current = startIndex + typeLength;
|
||||
current = current + HttpRuleParser.GetWhitespaceLength(input, current);
|
||||
|
||||
// Parse the separator between type and subtype
|
||||
if ((current >= input.Length) || (input[current] != '/'))
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
current++; // skip delimiter.
|
||||
current = current + HttpRuleParser.GetWhitespaceLength(input, current);
|
||||
|
||||
// Parse the subtype, i.e. <subtype> in media type string "<type>/<subtype>; param1=value1; param2=value2"
|
||||
var subtypeLength = HttpRuleParser.GetTokenLength(input, current);
|
||||
|
||||
if (subtypeLength == 0)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
// If there are no whitespaces between <type> and <subtype> in <type>/<subtype> get the media type using
|
||||
// one Substring call. Otherwise get substrings for <type> and <subtype> and combine them.
|
||||
var mediatTypeLength = current + subtypeLength - startIndex;
|
||||
if (typeLength + subtypeLength + 1 == mediatTypeLength)
|
||||
{
|
||||
mediaType = input.Substring(startIndex, mediatTypeLength);
|
||||
}
|
||||
else
|
||||
{
|
||||
mediaType = input.Substring(startIndex, typeLength) + "/" + input.Substring(current, subtypeLength);
|
||||
}
|
||||
|
||||
return mediatTypeLength;
|
||||
}
|
||||
|
||||
private static void CheckMediaTypeFormat(string mediaType, string parameterName)
|
||||
{
|
||||
if (string.IsNullOrEmpty(mediaType))
|
||||
{
|
||||
throw new ArgumentException("An empty string is not allowed.", parameterName);
|
||||
}
|
||||
|
||||
// When adding values using strongly typed objects, no leading/trailing LWS (whitespaces) are allowed.
|
||||
// Also no LWS between type and subtype are allowed.
|
||||
string tempMediaType;
|
||||
var mediaTypeLength = GetMediaTypeExpressionLength(mediaType, 0, out tempMediaType);
|
||||
if ((mediaTypeLength == 0) || (tempMediaType.Length != mediaType.Length))
|
||||
{
|
||||
throw new FormatException(string.Format(CultureInfo.InvariantCulture, "Invalid media type '{0}'.", mediaType));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,100 @@
|
|||
// Copyright (c) Microsoft Open Technologies, Inc. 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.Collections.Generic;
|
||||
|
||||
namespace Microsoft.Net.Http.Headers
|
||||
{
|
||||
/// <summary>
|
||||
/// Implementation of <see cref="IComparer{T}"/> that can compare accept media type header fields
|
||||
/// based on their quality values (a.k.a q-values).
|
||||
/// </summary>
|
||||
public class MediaTypeHeaderValueComparer : IComparer<MediaTypeHeaderValue>
|
||||
{
|
||||
private static readonly MediaTypeHeaderValueComparer _mediaTypeComparer =
|
||||
new MediaTypeHeaderValueComparer();
|
||||
|
||||
private MediaTypeHeaderValueComparer()
|
||||
{
|
||||
}
|
||||
|
||||
public static MediaTypeHeaderValueComparer QualityComparer
|
||||
{
|
||||
get { return _mediaTypeComparer; }
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
/// <remarks>
|
||||
/// Performs comparisons based on the arguments' quality values
|
||||
/// (aka their "q-value"). Values with identical q-values are considered equal (i.e. the result is 0)
|
||||
/// with the exception that subtype wildcards are considered less than specific media types and full
|
||||
/// wildcards are considered less than subtype wildcards. This allows callers to sort a sequence of
|
||||
/// <see cref="MediaTypeHeaderValue"/> following their q-values in the order of specific
|
||||
/// media types, subtype wildcards, and last any full wildcards.
|
||||
/// </remarks>
|
||||
public int Compare(MediaTypeHeaderValue mediaType1, MediaTypeHeaderValue mediaType2)
|
||||
{
|
||||
if (object.ReferenceEquals(mediaType1, mediaType2))
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
var returnValue = CompareBasedOnQualityFactor(mediaType1, mediaType2);
|
||||
|
||||
if (returnValue == 0)
|
||||
{
|
||||
if (!mediaType1.Type.Equals(mediaType2.Type, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
if (mediaType1.MatchesAllTypes)
|
||||
{
|
||||
return -1;
|
||||
}
|
||||
else if (mediaType2.MatchesAllTypes)
|
||||
{
|
||||
return 1;
|
||||
}
|
||||
else if (mediaType1.MatchesAllSubTypes && !mediaType2.MatchesAllSubTypes)
|
||||
{
|
||||
return -1;
|
||||
}
|
||||
else if (!mediaType1.MatchesAllSubTypes && mediaType2.MatchesAllSubTypes)
|
||||
{
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
else if (!mediaType1.SubType.Equals(mediaType2.SubType, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
if (mediaType1.MatchesAllSubTypes)
|
||||
{
|
||||
return -1;
|
||||
}
|
||||
else if (mediaType2.MatchesAllSubTypes)
|
||||
{
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return returnValue;
|
||||
}
|
||||
|
||||
private static int CompareBasedOnQualityFactor(MediaTypeHeaderValue mediaType1,
|
||||
MediaTypeHeaderValue mediaType2)
|
||||
{
|
||||
var mediaType1Quality = mediaType1.Quality ?? HeaderQuality.Match;
|
||||
var mediaType2Quality = mediaType2.Quality ?? HeaderQuality.Match;
|
||||
var qualityDifference = mediaType1Quality - mediaType2Quality;
|
||||
if (qualityDifference < 0)
|
||||
{
|
||||
return -1;
|
||||
}
|
||||
else if (qualityDifference > 0)
|
||||
{
|
||||
return 1;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,23 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<Project ToolsVersion="14.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
|
||||
<PropertyGroup>
|
||||
<VisualStudioVersion Condition="'$(VisualStudioVersion)' == ''">14.0</VisualStudioVersion>
|
||||
<VSToolsPath Condition="'$(VSToolsPath)' == ''">$(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion)</VSToolsPath>
|
||||
</PropertyGroup>
|
||||
<Import Project="$(VSToolsPath)\AspNet\Microsoft.Web.AspNet.Props" Condition="'$(VSToolsPath)' != ''" />
|
||||
<PropertyGroup Label="Globals">
|
||||
<ProjectGuid>60aa2fdb-8121-4826-8d00-9a143fefaf66</ProjectGuid>
|
||||
<RootNamespace>Microsoft.Net.Http.Headers</RootNamespace>
|
||||
<BaseIntermediateOutputPath Condition="'$(BaseIntermediateOutputPath)'=='' ">..\..\artifacts\obj\$(MSBuildProjectName)</BaseIntermediateOutputPath>
|
||||
<OutputPath Condition="'$(OutputPath)'=='' ">..\..\artifacts\bin\$(MSBuildProjectName)\</OutputPath>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup>
|
||||
<SchemaVersion>2.0</SchemaVersion>
|
||||
</PropertyGroup>
|
||||
<Import Project="$(VSToolsPath)\AspNet\Microsoft.Web.AspNet.targets" Condition="'$(VSToolsPath)' != ''" />
|
||||
<ProjectExtensions>
|
||||
<VisualStudio>
|
||||
<UserProperties project_1json__JSONSchema="http://www.asp.net/media/4878834/project.json" />
|
||||
</VisualStudio>
|
||||
</ProjectExtensions>
|
||||
</Project>
|
||||
|
|
@ -0,0 +1,347 @@
|
|||
// Copyright (c) Microsoft Open Technologies, Inc. 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.Collections.Generic;
|
||||
using System.Diagnostics.Contracts;
|
||||
using System.Text;
|
||||
|
||||
namespace Microsoft.Net.Http.Headers
|
||||
{
|
||||
// According to the RFC, in places where a "parameter" is required, the value is mandatory
|
||||
// (e.g. Media-Type, Accept). However, we don't introduce a dedicated type for it. So NameValueHeaderValue supports
|
||||
// name-only values in addition to name/value pairs.
|
||||
public class NameValueHeaderValue
|
||||
{
|
||||
private static readonly HttpHeaderParser<NameValueHeaderValue> SingleValueParser
|
||||
= new GenericHeaderParser<NameValueHeaderValue>(false, GetNameValueLength);
|
||||
internal static readonly HttpHeaderParser<NameValueHeaderValue> MultipleValueParser
|
||||
= new GenericHeaderParser<NameValueHeaderValue>(true, GetNameValueLength);
|
||||
|
||||
private string _name;
|
||||
private string _value;
|
||||
|
||||
private NameValueHeaderValue()
|
||||
{
|
||||
// Used by the parser to create a new instance of this type.
|
||||
}
|
||||
|
||||
public NameValueHeaderValue(string name)
|
||||
: this(name, null)
|
||||
{
|
||||
}
|
||||
|
||||
public NameValueHeaderValue(string name, string value)
|
||||
{
|
||||
CheckNameValueFormat(name, value);
|
||||
|
||||
_name = name;
|
||||
_value = value;
|
||||
}
|
||||
|
||||
public string Name
|
||||
{
|
||||
get { return _name; }
|
||||
}
|
||||
|
||||
public string Value
|
||||
{
|
||||
get { return _value; }
|
||||
set
|
||||
{
|
||||
CheckValueFormat(value);
|
||||
_value = value;
|
||||
}
|
||||
}
|
||||
|
||||
public override int GetHashCode()
|
||||
{
|
||||
Contract.Assert(_name != null);
|
||||
|
||||
var nameHashCode = StringComparer.OrdinalIgnoreCase.GetHashCode(_name);
|
||||
|
||||
if (!string.IsNullOrEmpty(_value))
|
||||
{
|
||||
// If we have a quoted-string, then just use the hash code. If we have a token, convert to lowercase
|
||||
// and retrieve the hash code.
|
||||
if (_value[0] == '"')
|
||||
{
|
||||
return nameHashCode ^ _value.GetHashCode();
|
||||
}
|
||||
|
||||
return nameHashCode ^ StringComparer.OrdinalIgnoreCase.GetHashCode(_value);
|
||||
}
|
||||
|
||||
return nameHashCode;
|
||||
}
|
||||
|
||||
public override bool Equals(object obj)
|
||||
{
|
||||
var other = obj as NameValueHeaderValue;
|
||||
|
||||
if (other == null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (string.Compare(_name, other._name, StringComparison.OrdinalIgnoreCase) != 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// RFC2616: 14.20: unquoted tokens should use case-INsensitive comparison; quoted-strings should use
|
||||
// case-sensitive comparison. The RFC doesn't mention how to compare quoted-strings outside the "Expect"
|
||||
// header. We treat all quoted-strings the same: case-sensitive comparison.
|
||||
|
||||
if (string.IsNullOrEmpty(_value))
|
||||
{
|
||||
return string.IsNullOrEmpty(other._value);
|
||||
}
|
||||
|
||||
if (_value[0] == '"')
|
||||
{
|
||||
// We have a quoted string, so we need to do case-sensitive comparison.
|
||||
return (string.CompareOrdinal(_value, other._value) == 0);
|
||||
}
|
||||
else
|
||||
{
|
||||
return (string.Compare(_value, other._value, StringComparison.OrdinalIgnoreCase) == 0);
|
||||
}
|
||||
}
|
||||
|
||||
public static NameValueHeaderValue Parse(string input)
|
||||
{
|
||||
var index = 0;
|
||||
return SingleValueParser.ParseValue(input, ref index);
|
||||
}
|
||||
|
||||
public static bool TryParse(string input, out NameValueHeaderValue parsedValue)
|
||||
{
|
||||
var index = 0;
|
||||
return SingleValueParser.TryParseValue(input, ref index, out parsedValue);
|
||||
}
|
||||
|
||||
public static IList<NameValueHeaderValue> ParseList(IList<string> input)
|
||||
{
|
||||
return MultipleValueParser.ParseValues(input);
|
||||
}
|
||||
|
||||
public static bool TryParseList(IList<string> input, out IList<NameValueHeaderValue> parsedValues)
|
||||
{
|
||||
return MultipleValueParser.TryParseValues(input, out parsedValues);
|
||||
}
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
if (!string.IsNullOrEmpty(_value))
|
||||
{
|
||||
return _name + "=" + _value;
|
||||
}
|
||||
return _name;
|
||||
}
|
||||
|
||||
internal static void ToString(ICollection<NameValueHeaderValue> values, char separator, bool leadingSeparator,
|
||||
StringBuilder destination)
|
||||
{
|
||||
Contract.Assert(destination != null);
|
||||
|
||||
if ((values == null) || (values.Count == 0))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
foreach (var value in values)
|
||||
{
|
||||
if (leadingSeparator || (destination.Length > 0))
|
||||
{
|
||||
destination.Append(separator);
|
||||
destination.Append(' ');
|
||||
}
|
||||
destination.Append(value.ToString());
|
||||
}
|
||||
}
|
||||
|
||||
internal static string ToString(ICollection<NameValueHeaderValue> values, char separator, bool leadingSeparator)
|
||||
{
|
||||
if ((values == null) || (values.Count == 0))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var sb = new StringBuilder();
|
||||
|
||||
ToString(values, separator, leadingSeparator, sb);
|
||||
|
||||
return sb.ToString();
|
||||
}
|
||||
|
||||
internal static int GetHashCode(ICollection<NameValueHeaderValue> values)
|
||||
{
|
||||
if ((values == null) || (values.Count == 0))
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
var result = 0;
|
||||
foreach (var value in values)
|
||||
{
|
||||
result = result ^ value.GetHashCode();
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private static int GetNameValueLength(string input, int startIndex, out NameValueHeaderValue parsedValue)
|
||||
{
|
||||
Contract.Requires(input != null);
|
||||
Contract.Requires(startIndex >= 0);
|
||||
|
||||
parsedValue = null;
|
||||
|
||||
if (string.IsNullOrEmpty(input) || (startIndex >= input.Length))
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Parse the name, i.e. <name> in name/value string "<name>=<value>". Caller must remove
|
||||
// leading whitespaces.
|
||||
var nameLength = HttpRuleParser.GetTokenLength(input, startIndex);
|
||||
|
||||
if (nameLength == 0)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
var name = input.Substring(startIndex, nameLength);
|
||||
var current = startIndex + nameLength;
|
||||
current = current + HttpRuleParser.GetWhitespaceLength(input, current);
|
||||
|
||||
// Parse the separator between name and value
|
||||
if ((current == input.Length) || (input[current] != '='))
|
||||
{
|
||||
// We only have a name and that's OK. Return.
|
||||
parsedValue = new NameValueHeaderValue();
|
||||
parsedValue._name = name;
|
||||
current = current + HttpRuleParser.GetWhitespaceLength(input, current); // skip whitespaces
|
||||
return current - startIndex;
|
||||
}
|
||||
|
||||
current++; // skip delimiter.
|
||||
current = current + HttpRuleParser.GetWhitespaceLength(input, current);
|
||||
|
||||
// Parse the value, i.e. <value> in name/value string "<name>=<value>"
|
||||
int valueLength = GetValueLength(input, current);
|
||||
|
||||
// Value after the '=' may be empty
|
||||
// Use parameterless ctor to avoid double-parsing of name and value, i.e. skip public ctor validation.
|
||||
parsedValue = new NameValueHeaderValue();
|
||||
parsedValue._name = name;
|
||||
parsedValue._value = input.Substring(current, valueLength);
|
||||
current = current + valueLength;
|
||||
current = current + HttpRuleParser.GetWhitespaceLength(input, current); // skip whitespaces
|
||||
return current - startIndex;
|
||||
}
|
||||
|
||||
// Returns the length of a name/value list, separated by 'delimiter'. E.g. "a=b, c=d, e=f" adds 3
|
||||
// name/value pairs to 'nameValueCollection' if 'delimiter' equals ','.
|
||||
internal static int GetNameValueListLength(string input, int startIndex, char delimiter,
|
||||
ICollection<NameValueHeaderValue> nameValueCollection)
|
||||
{
|
||||
Contract.Requires(nameValueCollection != null);
|
||||
Contract.Requires(startIndex >= 0);
|
||||
|
||||
if ((string.IsNullOrEmpty(input)) || (startIndex >= input.Length))
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
var current = startIndex + HttpRuleParser.GetWhitespaceLength(input, startIndex);
|
||||
while (true)
|
||||
{
|
||||
NameValueHeaderValue parameter = null;
|
||||
var nameValueLength = GetNameValueLength(input, current, out parameter);
|
||||
|
||||
if (nameValueLength == 0)
|
||||
{
|
||||
// There may be a trailing ';'
|
||||
return current - startIndex;
|
||||
}
|
||||
|
||||
nameValueCollection.Add(parameter);
|
||||
current = current + nameValueLength;
|
||||
current = current + HttpRuleParser.GetWhitespaceLength(input, current);
|
||||
|
||||
if ((current == input.Length) || (input[current] != delimiter))
|
||||
{
|
||||
// We're done and we have at least one valid name/value pair.
|
||||
return current - startIndex;
|
||||
}
|
||||
|
||||
// input[current] is 'delimiter'. Skip the delimiter and whitespaces and try to parse again.
|
||||
current++; // skip delimiter.
|
||||
current = current + HttpRuleParser.GetWhitespaceLength(input, current);
|
||||
}
|
||||
}
|
||||
|
||||
public static NameValueHeaderValue Find(ICollection<NameValueHeaderValue> values, string name)
|
||||
{
|
||||
Contract.Requires((name != null) && (name.Length > 0));
|
||||
|
||||
if ((values == null) || (values.Count == 0))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
foreach (var value in values)
|
||||
{
|
||||
if (string.Compare(value.Name, name, StringComparison.OrdinalIgnoreCase) == 0)
|
||||
{
|
||||
return value;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
internal static int GetValueLength(string input, int startIndex)
|
||||
{
|
||||
Contract.Requires(input != null);
|
||||
|
||||
if (startIndex >= input.Length)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
var valueLength = HttpRuleParser.GetTokenLength(input, startIndex);
|
||||
|
||||
if (valueLength == 0)
|
||||
{
|
||||
// A value can either be a token or a quoted string. Check if it is a quoted string.
|
||||
if (HttpRuleParser.GetQuotedStringLength(input, startIndex, out valueLength) != HttpParseResult.Parsed)
|
||||
{
|
||||
// We have an invalid value. Reset the name and return.
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
return valueLength;
|
||||
}
|
||||
|
||||
private static void CheckNameValueFormat(string name, string value)
|
||||
{
|
||||
HeaderUtilities.CheckValidToken(name, "name");
|
||||
CheckValueFormat(value);
|
||||
}
|
||||
|
||||
private static void CheckValueFormat(string value)
|
||||
{
|
||||
// Either value is null/empty or a valid token/quoted string
|
||||
if (!(string.IsNullOrEmpty(value) || (GetValueLength(value, 0) == value.Length)))
|
||||
{
|
||||
throw new FormatException(string.Format(System.Globalization.CultureInfo.InvariantCulture, "The header value is invalid: '{0}'", value));
|
||||
}
|
||||
}
|
||||
|
||||
private static NameValueHeaderValue CreateNameValue()
|
||||
{
|
||||
return new NameValueHeaderValue();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved.
|
||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
using System;
|
||||
|
||||
namespace Microsoft.Net.Http.Headers
|
||||
{
|
||||
[AttributeUsage(AttributeTargets.Parameter, AllowMultiple = false)]
|
||||
internal sealed class NotNullAttribute : Attribute
|
||||
{
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,56 @@
|
|||
// Copyright (c) Microsoft Open Technologies, Inc. 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.Collections.ObjectModel;
|
||||
|
||||
namespace Microsoft.Net.Http.Headers
|
||||
{
|
||||
// List<T> allows 'null' values to be added. This is not what we want so we use a custom Collection<T> derived
|
||||
// type to throw if 'null' gets added. Collection<T> internally uses List<T> which comes at some cost. In addition
|
||||
// Collection<T>.Add() calls List<T>.InsertItem() which is an O(n) operation (compared to O(1) for List<T>.Add()).
|
||||
// This type is only used for very small collections (1-2 items) to keep the impact of using Collection<T> small.
|
||||
internal class ObjectCollection<T> : Collection<T> where T : class
|
||||
{
|
||||
private static readonly Action<T> DefaultValidator = CheckNotNull;
|
||||
|
||||
private Action<T> _validator;
|
||||
|
||||
public ObjectCollection()
|
||||
: this(DefaultValidator)
|
||||
{
|
||||
}
|
||||
|
||||
public ObjectCollection(Action<T> validator)
|
||||
{
|
||||
_validator = validator;
|
||||
}
|
||||
|
||||
protected override void InsertItem(int index, T item)
|
||||
{
|
||||
if (_validator != null)
|
||||
{
|
||||
_validator(item);
|
||||
}
|
||||
base.InsertItem(index, item);
|
||||
}
|
||||
|
||||
protected override void SetItem(int index, T item)
|
||||
{
|
||||
if (_validator != null)
|
||||
{
|
||||
_validator(item);
|
||||
}
|
||||
base.SetItem(index, item);
|
||||
}
|
||||
|
||||
private static void CheckNotNull(T item)
|
||||
{
|
||||
// null values cannot be added to the collection.
|
||||
if (item == null)
|
||||
{
|
||||
throw new ArgumentNullException("item");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,166 @@
|
|||
// Copyright (c) Microsoft Open Technologies, Inc. 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.Diagnostics.Contracts;
|
||||
|
||||
namespace Microsoft.Net.Http.Headers
|
||||
{
|
||||
public class RangeConditionHeaderValue
|
||||
{
|
||||
private static readonly HttpHeaderParser<RangeConditionHeaderValue> Parser
|
||||
= new GenericHeaderParser<RangeConditionHeaderValue>(false, GetRangeConditionLength);
|
||||
|
||||
private DateTimeOffset? _lastModified;
|
||||
private EntityTagHeaderValue _entityTag;
|
||||
|
||||
private RangeConditionHeaderValue()
|
||||
{
|
||||
// Used by the parser to create a new instance of this type.
|
||||
}
|
||||
|
||||
public RangeConditionHeaderValue(DateTimeOffset lastModified)
|
||||
{
|
||||
_lastModified = lastModified;
|
||||
}
|
||||
|
||||
public RangeConditionHeaderValue(EntityTagHeaderValue entityTag)
|
||||
{
|
||||
if (entityTag == null)
|
||||
{
|
||||
throw new ArgumentNullException("entityTag");
|
||||
}
|
||||
|
||||
_entityTag = entityTag;
|
||||
}
|
||||
|
||||
public RangeConditionHeaderValue(string entityTag)
|
||||
: this(new EntityTagHeaderValue(entityTag))
|
||||
{
|
||||
}
|
||||
|
||||
public DateTimeOffset? LastModified
|
||||
{
|
||||
get { return _lastModified; }
|
||||
}
|
||||
|
||||
public EntityTagHeaderValue EntityTag
|
||||
{
|
||||
get { return _entityTag; }
|
||||
}
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
if (_entityTag == null)
|
||||
{
|
||||
return HttpRuleParser.DateToString(_lastModified.Value);
|
||||
}
|
||||
return _entityTag.ToString();
|
||||
}
|
||||
|
||||
public override bool Equals(object obj)
|
||||
{
|
||||
var other = obj as RangeConditionHeaderValue;
|
||||
|
||||
if (other == null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (_entityTag == null)
|
||||
{
|
||||
return (other._lastModified != null) && (_lastModified.Value == other._lastModified.Value);
|
||||
}
|
||||
|
||||
return _entityTag.Equals(other._entityTag);
|
||||
}
|
||||
|
||||
public override int GetHashCode()
|
||||
{
|
||||
if (_entityTag == null)
|
||||
{
|
||||
return _lastModified.Value.GetHashCode();
|
||||
}
|
||||
|
||||
return _entityTag.GetHashCode();
|
||||
}
|
||||
|
||||
public static RangeConditionHeaderValue Parse(string input)
|
||||
{
|
||||
var index = 0;
|
||||
return Parser.ParseValue(input, ref index);
|
||||
}
|
||||
|
||||
public static bool TryParse(string input, out RangeConditionHeaderValue parsedValue)
|
||||
{
|
||||
var index = 0;
|
||||
return Parser.TryParseValue(input, ref index, out parsedValue);
|
||||
}
|
||||
|
||||
private static int GetRangeConditionLength(string input, int startIndex, out RangeConditionHeaderValue parsedValue)
|
||||
{
|
||||
Contract.Requires(startIndex >= 0);
|
||||
|
||||
parsedValue = null;
|
||||
|
||||
// Make sure we have at least 2 characters
|
||||
if (string.IsNullOrEmpty(input) || (startIndex + 1 >= input.Length))
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
var current = startIndex;
|
||||
|
||||
// Caller must remove leading whitespaces.
|
||||
DateTimeOffset date = DateTimeOffset.MinValue;
|
||||
EntityTagHeaderValue entityTag = null;
|
||||
|
||||
// Entity tags are quoted strings optionally preceded by "W/". By looking at the first two character we
|
||||
// can determine whether the string is en entity tag or a date.
|
||||
var firstChar = input[current];
|
||||
var secondChar = input[current + 1];
|
||||
|
||||
if ((firstChar == '\"') || (((firstChar == 'w') || (firstChar == 'W')) && (secondChar == '/')))
|
||||
{
|
||||
// trailing whitespaces are removed by GetEntityTagLength()
|
||||
var entityTagLength = EntityTagHeaderValue.GetEntityTagLength(input, current, out entityTag);
|
||||
|
||||
if (entityTagLength == 0)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
current = current + entityTagLength;
|
||||
|
||||
// RangeConditionHeaderValue only allows 1 value. There must be no delimiter/other chars after an
|
||||
// entity tag.
|
||||
if (current != input.Length)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if (!HttpRuleParser.TryStringToDate(input.Substring(current), out date))
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
// If we got a valid date, then the parser consumed the whole string (incl. trailing whitespaces).
|
||||
current = input.Length;
|
||||
}
|
||||
|
||||
parsedValue = new RangeConditionHeaderValue();
|
||||
if (entityTag == null)
|
||||
{
|
||||
parsedValue._lastModified = date;
|
||||
}
|
||||
else
|
||||
{
|
||||
parsedValue._entityTag = entityTag;
|
||||
}
|
||||
|
||||
return current - startIndex;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,161 @@
|
|||
// Copyright (c) Microsoft Open Technologies, Inc. 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.Collections.Generic;
|
||||
using System.Diagnostics.Contracts;
|
||||
using System.Text;
|
||||
|
||||
namespace Microsoft.Net.Http.Headers
|
||||
{
|
||||
public class RangeHeaderValue
|
||||
{
|
||||
private static readonly HttpHeaderParser<RangeHeaderValue> Parser
|
||||
= new GenericHeaderParser<RangeHeaderValue>(false, GetRangeLength);
|
||||
|
||||
private string _unit;
|
||||
private ICollection<RangeItemHeaderValue> _ranges;
|
||||
|
||||
public RangeHeaderValue()
|
||||
{
|
||||
_unit = HeaderUtilities.BytesUnit;
|
||||
}
|
||||
|
||||
public RangeHeaderValue(long? from, long? to)
|
||||
{
|
||||
// convenience ctor: "Range: bytes=from-to"
|
||||
_unit = HeaderUtilities.BytesUnit;
|
||||
Ranges.Add(new RangeItemHeaderValue(from, to));
|
||||
}
|
||||
|
||||
public string Unit
|
||||
{
|
||||
get { return _unit; }
|
||||
set
|
||||
{
|
||||
HeaderUtilities.CheckValidToken(value, "value");
|
||||
_unit = value;
|
||||
}
|
||||
}
|
||||
|
||||
public ICollection<RangeItemHeaderValue> Ranges
|
||||
{
|
||||
get
|
||||
{
|
||||
if (_ranges == null)
|
||||
{
|
||||
_ranges = new ObjectCollection<RangeItemHeaderValue>();
|
||||
}
|
||||
return _ranges;
|
||||
}
|
||||
}
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
var sb = new StringBuilder(_unit);
|
||||
sb.Append('=');
|
||||
|
||||
var first = true;
|
||||
foreach (var range in Ranges)
|
||||
{
|
||||
if (first)
|
||||
{
|
||||
first = false;
|
||||
}
|
||||
else
|
||||
{
|
||||
sb.Append(", ");
|
||||
}
|
||||
|
||||
sb.Append(range.From);
|
||||
sb.Append('-');
|
||||
sb.Append(range.To);
|
||||
}
|
||||
|
||||
return sb.ToString();
|
||||
}
|
||||
|
||||
public override bool Equals(object obj)
|
||||
{
|
||||
var other = obj as RangeHeaderValue;
|
||||
|
||||
if (other == null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return (string.Compare(_unit, other._unit, StringComparison.OrdinalIgnoreCase) == 0) &&
|
||||
HeaderUtilities.AreEqualCollections(Ranges, other.Ranges);
|
||||
}
|
||||
|
||||
public override int GetHashCode()
|
||||
{
|
||||
var result = StringComparer.OrdinalIgnoreCase.GetHashCode(_unit);
|
||||
|
||||
foreach (var range in Ranges)
|
||||
{
|
||||
result = result ^ range.GetHashCode();
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
public static RangeHeaderValue Parse(string input)
|
||||
{
|
||||
var index = 0;
|
||||
return Parser.ParseValue(input, ref index);
|
||||
}
|
||||
|
||||
public static bool TryParse(string input, out RangeHeaderValue parsedValue)
|
||||
{
|
||||
var index = 0;
|
||||
return Parser.TryParseValue(input, ref index, out parsedValue);
|
||||
}
|
||||
|
||||
private static int GetRangeLength(string input, int startIndex, out RangeHeaderValue parsedValue)
|
||||
{
|
||||
Contract.Requires(startIndex >= 0);
|
||||
|
||||
parsedValue = null;
|
||||
|
||||
if (string.IsNullOrEmpty(input) || (startIndex >= input.Length))
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Parse the unit string: <unit> in '<unit>=<from1>-<to1>, <from2>-<to2>'
|
||||
var unitLength = HttpRuleParser.GetTokenLength(input, startIndex);
|
||||
|
||||
if (unitLength == 0)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
RangeHeaderValue result = new RangeHeaderValue();
|
||||
result._unit = input.Substring(startIndex, unitLength);
|
||||
var current = startIndex + unitLength;
|
||||
current = current + HttpRuleParser.GetWhitespaceLength(input, current);
|
||||
|
||||
if ((current == input.Length) || (input[current] != '='))
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
current++; // skip '=' separator
|
||||
current = current + HttpRuleParser.GetWhitespaceLength(input, current);
|
||||
|
||||
var rangesLength = RangeItemHeaderValue.GetRangeItemListLength(input, current, result.Ranges);
|
||||
|
||||
if (rangesLength == 0)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
current = current + rangesLength;
|
||||
Contract.Assert(current == input.Length, "GetRangeItemListLength() should consume the whole string or fail.");
|
||||
|
||||
parsedValue = result;
|
||||
return current - startIndex;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,226 @@
|
|||
// Copyright (c) Microsoft Open Technologies, Inc. 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.Collections.Generic;
|
||||
using System.Diagnostics.Contracts;
|
||||
using System.Globalization;
|
||||
|
||||
namespace Microsoft.Net.Http.Headers
|
||||
{
|
||||
public class RangeItemHeaderValue
|
||||
{
|
||||
private long? _from;
|
||||
private long? _to;
|
||||
|
||||
public RangeItemHeaderValue(long? from, long? to)
|
||||
{
|
||||
if (!from.HasValue && !to.HasValue)
|
||||
{
|
||||
throw new ArgumentException("Invalid header range.");
|
||||
}
|
||||
if (from.HasValue && (from.Value < 0))
|
||||
{
|
||||
throw new ArgumentOutOfRangeException("from");
|
||||
}
|
||||
if (to.HasValue && (to.Value < 0))
|
||||
{
|
||||
throw new ArgumentOutOfRangeException("to");
|
||||
}
|
||||
if (from.HasValue && to.HasValue && (from.Value > to.Value))
|
||||
{
|
||||
throw new ArgumentOutOfRangeException("from");
|
||||
}
|
||||
|
||||
_from = from;
|
||||
_to = to;
|
||||
}
|
||||
|
||||
public long? From
|
||||
{
|
||||
get { return _from; }
|
||||
}
|
||||
|
||||
public long? To
|
||||
{
|
||||
get { return _to; }
|
||||
}
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
if (!_from.HasValue)
|
||||
{
|
||||
return "-" + _to.Value.ToString(NumberFormatInfo.InvariantInfo);
|
||||
}
|
||||
else if (!_to.HasValue)
|
||||
{
|
||||
return _from.Value.ToString(NumberFormatInfo.InvariantInfo) + "-";
|
||||
}
|
||||
return _from.Value.ToString(NumberFormatInfo.InvariantInfo) + "-" +
|
||||
_to.Value.ToString(NumberFormatInfo.InvariantInfo);
|
||||
}
|
||||
|
||||
public override bool Equals(object obj)
|
||||
{
|
||||
var other = obj as RangeItemHeaderValue;
|
||||
|
||||
if (other == null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
return ((_from == other._from) && (_to == other._to));
|
||||
}
|
||||
|
||||
public override int GetHashCode()
|
||||
{
|
||||
if (!_from.HasValue)
|
||||
{
|
||||
return _to.GetHashCode();
|
||||
}
|
||||
else if (!_to.HasValue)
|
||||
{
|
||||
return _from.GetHashCode();
|
||||
}
|
||||
return _from.GetHashCode() ^ _to.GetHashCode();
|
||||
}
|
||||
|
||||
// Returns the length of a range list. E.g. "1-2, 3-4, 5-6" adds 3 ranges to 'rangeCollection'. Note that empty
|
||||
// list segments are allowed, e.g. ",1-2, , 3-4,,".
|
||||
internal static int GetRangeItemListLength(string input, int startIndex,
|
||||
ICollection<RangeItemHeaderValue> rangeCollection)
|
||||
{
|
||||
Contract.Requires(rangeCollection != null);
|
||||
Contract.Requires(startIndex >= 0);
|
||||
Contract.Ensures((Contract.Result<int>() == 0) || (rangeCollection.Count > 0),
|
||||
"If we can parse the string, then we expect to have at least one range item.");
|
||||
|
||||
if ((string.IsNullOrEmpty(input)) || (startIndex >= input.Length))
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Empty segments are allowed, so skip all delimiter-only segments (e.g. ", ,").
|
||||
var separatorFound = false;
|
||||
var current = HeaderUtilities.GetNextNonEmptyOrWhitespaceIndex(input, startIndex, true, out separatorFound);
|
||||
// It's OK if we didn't find leading separator characters. Ignore 'separatorFound'.
|
||||
|
||||
if (current == input.Length)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
RangeItemHeaderValue range = null;
|
||||
while (true)
|
||||
{
|
||||
var rangeLength = GetRangeItemLength(input, current, out range);
|
||||
|
||||
if (rangeLength == 0)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
rangeCollection.Add(range);
|
||||
|
||||
current = current + rangeLength;
|
||||
current = HeaderUtilities.GetNextNonEmptyOrWhitespaceIndex(input, current, true, out separatorFound);
|
||||
|
||||
// If the string is not consumed, we must have a delimiter, otherwise the string is not a valid
|
||||
// range list.
|
||||
if ((current < input.Length) && !separatorFound)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (current == input.Length)
|
||||
{
|
||||
return current - startIndex;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
internal static int GetRangeItemLength(string input, int startIndex, out RangeItemHeaderValue parsedValue)
|
||||
{
|
||||
Contract.Requires(startIndex >= 0);
|
||||
|
||||
// This parser parses number ranges: e.g. '1-2', '1-', '-2'.
|
||||
|
||||
parsedValue = null;
|
||||
|
||||
if (string.IsNullOrEmpty(input) || (startIndex >= input.Length))
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Caller must remove leading whitespaces. If not, we'll return 0.
|
||||
var current = startIndex;
|
||||
|
||||
// Try parse the first value of a value pair.
|
||||
var fromStartIndex = current;
|
||||
var fromLength = HttpRuleParser.GetNumberLength(input, current, false);
|
||||
|
||||
if (fromLength > HttpRuleParser.MaxInt64Digits)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
current = current + fromLength;
|
||||
current = current + HttpRuleParser.GetWhitespaceLength(input, current);
|
||||
|
||||
// Afer the first value, the '-' character must follow.
|
||||
if ((current == input.Length) || (input[current] != '-'))
|
||||
{
|
||||
// We need a '-' character otherwise this can't be a valid range.
|
||||
return 0;
|
||||
}
|
||||
|
||||
current++; // skip the '-' character
|
||||
current = current + HttpRuleParser.GetWhitespaceLength(input, current);
|
||||
|
||||
var toStartIndex = current;
|
||||
var toLength = 0;
|
||||
|
||||
// If we didn't reach the end of the string, try parse the second value of the range.
|
||||
if (current < input.Length)
|
||||
{
|
||||
toLength = HttpRuleParser.GetNumberLength(input, current, false);
|
||||
|
||||
if (toLength > HttpRuleParser.MaxInt64Digits)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
current = current + toLength;
|
||||
current = current + HttpRuleParser.GetWhitespaceLength(input, current);
|
||||
}
|
||||
|
||||
if ((fromLength == 0) && (toLength == 0))
|
||||
{
|
||||
return 0; // At least one value must be provided in order to be a valid range.
|
||||
}
|
||||
|
||||
// Try convert first value to int64
|
||||
long from = 0;
|
||||
if ((fromLength > 0) && !HeaderUtilities.TryParseInt64(input.Substring(fromStartIndex, fromLength), out from))
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Try convert second value to int64
|
||||
long to = 0;
|
||||
if ((toLength > 0) && !HeaderUtilities.TryParseInt64(input.Substring(toStartIndex, toLength), out to))
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
// 'from' must not be greater than 'to'
|
||||
if ((fromLength > 0) && (toLength > 0) && (from > to))
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
parsedValue = new RangeItemHeaderValue((fromLength == 0 ? (long?)null : (long?)from),
|
||||
(toLength == 0 ? (long?)null : (long?)to));
|
||||
return current - startIndex;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,364 @@
|
|||
// Copyright (c) Microsoft Open Technologies, Inc. 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.Collections.Generic;
|
||||
using System.Diagnostics.Contracts;
|
||||
using System.Text;
|
||||
|
||||
namespace Microsoft.Net.Http.Headers
|
||||
{
|
||||
// http://tools.ietf.org/html/rfc6265
|
||||
public class SetCookieHeaderValue
|
||||
{
|
||||
private const string ExpiresToken = "expires";
|
||||
private const string MaxAgeToken = "max-age";
|
||||
private const string DomainToken = "domain";
|
||||
private const string PathToken = "path";
|
||||
private const string SecureToken = "secure";
|
||||
private const string HttpOnlyToken = "httponly";
|
||||
private const string DefaultPath = "/"; // TODO: Used?
|
||||
|
||||
private static readonly HttpHeaderParser<SetCookieHeaderValue> SingleValueParser
|
||||
= new GenericHeaderParser<SetCookieHeaderValue>(false, GetSetCookieLength);
|
||||
private static readonly HttpHeaderParser<SetCookieHeaderValue> MultipleValueParser
|
||||
= new GenericHeaderParser<SetCookieHeaderValue>(true, GetSetCookieLength);
|
||||
|
||||
private string _name;
|
||||
private string _value;
|
||||
|
||||
private SetCookieHeaderValue()
|
||||
{
|
||||
// Used by the parser to create a new instance of this type.
|
||||
}
|
||||
|
||||
public SetCookieHeaderValue([NotNull] string name)
|
||||
: this(name, string.Empty)
|
||||
{
|
||||
}
|
||||
|
||||
public SetCookieHeaderValue([NotNull] string name, [NotNull] string value)
|
||||
{
|
||||
Name = name;
|
||||
Value = value;
|
||||
}
|
||||
|
||||
public string Name
|
||||
{
|
||||
get { return _name; }
|
||||
set
|
||||
{
|
||||
CookieHeaderValue.CheckNameFormat(value, "name");
|
||||
_name = value;
|
||||
}
|
||||
}
|
||||
|
||||
public string Value
|
||||
{
|
||||
get { return _value; }
|
||||
set
|
||||
{
|
||||
CookieHeaderValue.CheckValueFormat(value, "value");
|
||||
_value = value;
|
||||
}
|
||||
}
|
||||
|
||||
public DateTimeOffset? Expires { get; set; }
|
||||
|
||||
public TimeSpan? MaxAge { get; set; }
|
||||
|
||||
public string Domain { get; set; }
|
||||
|
||||
// TODO: PathString?
|
||||
public string Path { get; set; }
|
||||
|
||||
public bool Secure { get; set; }
|
||||
|
||||
public bool HttpOnly { get; set; }
|
||||
|
||||
// name="val ue"; expires=Sun, 06 Nov 1994 08:49:37 GMT; max-age=86400; domain=domain1; path=path1; secure; httponly
|
||||
public override string ToString()
|
||||
{
|
||||
StringBuilder header = new StringBuilder();
|
||||
|
||||
header.Append(_name);
|
||||
header.Append("=");
|
||||
header.Append(_value);
|
||||
|
||||
if (Expires.HasValue)
|
||||
{
|
||||
AppendSegment(header, ExpiresToken, HeaderUtilities.FormatDate(Expires.Value));
|
||||
}
|
||||
|
||||
if (MaxAge.HasValue)
|
||||
{
|
||||
AppendSegment(header, MaxAgeToken, HeaderUtilities.FormatInt64((long)MaxAge.Value.TotalSeconds));
|
||||
}
|
||||
|
||||
if (Domain != null)
|
||||
{
|
||||
AppendSegment(header, DomainToken, Domain);
|
||||
}
|
||||
|
||||
if (Path != null)
|
||||
{
|
||||
AppendSegment(header, PathToken, Path);
|
||||
}
|
||||
|
||||
if (Secure)
|
||||
{
|
||||
AppendSegment(header, SecureToken, null);
|
||||
}
|
||||
|
||||
if (HttpOnly)
|
||||
{
|
||||
AppendSegment(header, HttpOnlyToken, null);
|
||||
}
|
||||
|
||||
return header.ToString();
|
||||
}
|
||||
|
||||
private static void AppendSegment(StringBuilder builder, string name, string value)
|
||||
{
|
||||
builder.Append("; ");
|
||||
builder.Append(name);
|
||||
if (value != null)
|
||||
{
|
||||
builder.Append("=");
|
||||
builder.Append(value);
|
||||
}
|
||||
}
|
||||
|
||||
public static SetCookieHeaderValue Parse(string input)
|
||||
{
|
||||
var index = 0;
|
||||
return SingleValueParser.ParseValue(input, ref index);
|
||||
}
|
||||
|
||||
public static bool TryParse(string input, out SetCookieHeaderValue parsedValue)
|
||||
{
|
||||
var index = 0;
|
||||
return SingleValueParser.TryParseValue(input, ref index, out parsedValue);
|
||||
}
|
||||
|
||||
public static IList<SetCookieHeaderValue> ParseList(IList<string> inputs)
|
||||
{
|
||||
return MultipleValueParser.ParseValues(inputs);
|
||||
}
|
||||
|
||||
public static bool TryParseList(IList<string> inputs, out IList<SetCookieHeaderValue> parsedValues)
|
||||
{
|
||||
return MultipleValueParser.TryParseValues(inputs, out parsedValues);
|
||||
}
|
||||
|
||||
// name=value; expires=Sun, 06 Nov 1994 08:49:37 GMT; max-age=86400; domain=domain1; path=path1; secure; httponly
|
||||
private static int GetSetCookieLength(string input, int startIndex, out SetCookieHeaderValue parsedValue)
|
||||
{
|
||||
Contract.Requires(startIndex >= 0);
|
||||
var offset = startIndex;
|
||||
|
||||
parsedValue = null;
|
||||
|
||||
if (string.IsNullOrEmpty(input) || (offset >= input.Length))
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
var result = new SetCookieHeaderValue();
|
||||
|
||||
// The caller should have already consumed any leading whitespace, commas, etc..
|
||||
|
||||
// Name=value;
|
||||
|
||||
// Name
|
||||
var itemLength = HttpRuleParser.GetTokenLength(input, offset);
|
||||
if (itemLength == 0)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
result._name = input.Substring(offset, itemLength);
|
||||
offset += itemLength;
|
||||
|
||||
// = (no spaces)
|
||||
if (!ReadEqualsSign(input, ref offset))
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
string value;
|
||||
// value or "quoted value"
|
||||
itemLength = CookieHeaderValue.GetCookieValueLength(input, offset, out value);
|
||||
// The value may be empty
|
||||
result._value = input.Substring(offset, itemLength);
|
||||
offset += itemLength;
|
||||
|
||||
// *(';' SP cookie-av)
|
||||
while (offset < input.Length)
|
||||
{
|
||||
if (input[offset] == ',')
|
||||
{
|
||||
// Divider between headers
|
||||
break;
|
||||
}
|
||||
if (input[offset] != ';')
|
||||
{
|
||||
// Expecting a ';' between parameters
|
||||
return 0;
|
||||
}
|
||||
offset++;
|
||||
|
||||
offset += HttpRuleParser.GetWhitespaceLength(input, offset);
|
||||
|
||||
// cookie-av = expires-av / max-age-av / domain-av / path-av / secure-av / httponly-av / extension-av
|
||||
itemLength = HttpRuleParser.GetTokenLength(input, offset);
|
||||
if (itemLength == 0)
|
||||
{
|
||||
// Trailing ';' or leading into garbage. Let the next parser fail.
|
||||
break;
|
||||
}
|
||||
var token = input.Substring(offset, itemLength);
|
||||
offset += itemLength;
|
||||
|
||||
// expires-av = "Expires=" sane-cookie-date
|
||||
if (string.Equals(token, ExpiresToken, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
// = (no spaces)
|
||||
if (!ReadEqualsSign(input, ref offset))
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
var dateString = ReadToSemicolonOrEnd(input, ref offset);
|
||||
DateTimeOffset expirationDate;
|
||||
if (!HttpRuleParser.TryStringToDate(dateString, out expirationDate))
|
||||
{
|
||||
// Invalid expiration date, abort
|
||||
return 0;
|
||||
}
|
||||
result.Expires = expirationDate;
|
||||
}
|
||||
// max-age-av = "Max-Age=" non-zero-digit *DIGIT
|
||||
else if (string.Equals(token, MaxAgeToken, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
// = (no spaces)
|
||||
if (!ReadEqualsSign(input, ref offset))
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
itemLength = HttpRuleParser.GetNumberLength(input, offset, allowDecimal: false);
|
||||
if (itemLength == 0)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
var numberString = input.Substring(offset, itemLength);
|
||||
long maxAge;
|
||||
if (!HeaderUtilities.TryParseInt64(numberString, out maxAge))
|
||||
{
|
||||
// Invalid expiration date, abort
|
||||
return 0;
|
||||
}
|
||||
result.MaxAge = TimeSpan.FromSeconds(maxAge);
|
||||
offset += itemLength;
|
||||
}
|
||||
// domain-av = "Domain=" domain-value
|
||||
// domain-value = <subdomain> ; defined in [RFC1034], Section 3.5, as enhanced by [RFC1123], Section 2.1
|
||||
else if (string.Equals(token, DomainToken, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
// = (no spaces)
|
||||
if (!ReadEqualsSign(input, ref offset))
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
// We don't do any detailed validation on the domain.
|
||||
result.Domain = ReadToSemicolonOrEnd(input, ref offset);
|
||||
}
|
||||
// path-av = "Path=" path-value
|
||||
// path-value = <any CHAR except CTLs or ";">
|
||||
else if (string.Equals(token, PathToken, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
// = (no spaces)
|
||||
if (!ReadEqualsSign(input, ref offset))
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
// We don't do any detailed validation on the path.
|
||||
result.Path = ReadToSemicolonOrEnd(input, ref offset);
|
||||
}
|
||||
// secure-av = "Secure"
|
||||
else if (string.Equals(token, SecureToken, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
result.Secure = true;
|
||||
}
|
||||
// httponly-av = "HttpOnly"
|
||||
else if (string.Equals(token, HttpOnlyToken, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
result.HttpOnly = true;
|
||||
}
|
||||
// extension-av = <any CHAR except CTLs or ";">
|
||||
else
|
||||
{
|
||||
// TODO: skip it? Store it in a list?
|
||||
}
|
||||
}
|
||||
|
||||
parsedValue = result;
|
||||
return offset - startIndex;
|
||||
}
|
||||
|
||||
private static bool ReadEqualsSign(string input, ref int offset)
|
||||
{
|
||||
// = (no spaces)
|
||||
if (offset >= input.Length || input[offset] != '=')
|
||||
{
|
||||
return false;
|
||||
}
|
||||
offset++;
|
||||
return true;
|
||||
}
|
||||
|
||||
private static string ReadToSemicolonOrEnd(string input, ref int offset)
|
||||
{
|
||||
var end = input.IndexOf(';', offset);
|
||||
if (end < 0)
|
||||
{
|
||||
// Remainder of the string
|
||||
end = input.Length;
|
||||
}
|
||||
var itemLength = end - offset;
|
||||
var result = input.Substring(offset, itemLength);
|
||||
offset += itemLength;
|
||||
return result;
|
||||
}
|
||||
|
||||
public override bool Equals(object obj)
|
||||
{
|
||||
var other = obj as SetCookieHeaderValue;
|
||||
|
||||
if (other == null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return string.Equals(_name, other._name, StringComparison.OrdinalIgnoreCase)
|
||||
&& string.Equals(_value, other._value, StringComparison.OrdinalIgnoreCase)
|
||||
&& Expires.Equals(other.Expires)
|
||||
&& MaxAge.Equals(other.MaxAge)
|
||||
&& string.Equals(Domain, other.Domain, StringComparison.OrdinalIgnoreCase)
|
||||
&& string.Equals(Path, other.Path, StringComparison.OrdinalIgnoreCase)
|
||||
&& Secure == other.Secure
|
||||
&& HttpOnly == other.HttpOnly;
|
||||
}
|
||||
|
||||
public override int GetHashCode()
|
||||
{
|
||||
return StringComparer.OrdinalIgnoreCase.GetHashCode(_name)
|
||||
^ StringComparer.OrdinalIgnoreCase.GetHashCode(_value)
|
||||
^ (Expires.HasValue ? Expires.GetHashCode() : 0)
|
||||
^ (MaxAge.HasValue ? MaxAge.GetHashCode() : 0)
|
||||
^ (Domain != null ? StringComparer.OrdinalIgnoreCase.GetHashCode(Domain) : 0)
|
||||
^ (Path != null ? StringComparer.OrdinalIgnoreCase.GetHashCode(Path) : 0)
|
||||
^ Secure.GetHashCode()
|
||||
^ HttpOnly.GetHashCode();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,225 @@
|
|||
// Copyright (c) Microsoft Open Technologies, Inc. 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.Collections.Generic;
|
||||
using System.Diagnostics.Contracts;
|
||||
using System.Globalization;
|
||||
|
||||
namespace Microsoft.Net.Http.Headers
|
||||
{
|
||||
public class StringWithQualityHeaderValue
|
||||
{
|
||||
private static readonly HttpHeaderParser<StringWithQualityHeaderValue> SingleValueParser
|
||||
= new GenericHeaderParser<StringWithQualityHeaderValue>(false, GetStringWithQualityLength);
|
||||
private static readonly HttpHeaderParser<StringWithQualityHeaderValue> MultipleValueParser
|
||||
= new GenericHeaderParser<StringWithQualityHeaderValue>(true, GetStringWithQualityLength);
|
||||
|
||||
private string _value;
|
||||
private double? _quality;
|
||||
|
||||
private StringWithQualityHeaderValue()
|
||||
{
|
||||
// Used by the parser to create a new instance of this type.
|
||||
}
|
||||
|
||||
public StringWithQualityHeaderValue(string value)
|
||||
{
|
||||
HeaderUtilities.CheckValidToken(value, "value");
|
||||
|
||||
_value = value;
|
||||
}
|
||||
|
||||
public StringWithQualityHeaderValue(string value, double quality)
|
||||
{
|
||||
HeaderUtilities.CheckValidToken(value, "value");
|
||||
|
||||
if ((quality < 0) || (quality > 1))
|
||||
{
|
||||
throw new ArgumentOutOfRangeException("quality");
|
||||
}
|
||||
|
||||
_value = value;
|
||||
_quality = quality;
|
||||
}
|
||||
|
||||
public string Value
|
||||
{
|
||||
get { return _value; }
|
||||
}
|
||||
|
||||
public double? Quality
|
||||
{
|
||||
get { return _quality; }
|
||||
}
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
if (_quality.HasValue)
|
||||
{
|
||||
return _value + "; q=" + _quality.Value.ToString("0.0##", NumberFormatInfo.InvariantInfo);
|
||||
}
|
||||
|
||||
return _value;
|
||||
}
|
||||
|
||||
public override bool Equals(object obj)
|
||||
{
|
||||
var other = obj as StringWithQualityHeaderValue;
|
||||
|
||||
if (other == null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (string.Compare(_value, other._value, StringComparison.OrdinalIgnoreCase) != 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (_quality.HasValue)
|
||||
{
|
||||
// Note that we don't consider double.Epsilon here. We really consider two values equal if they're
|
||||
// actually equal. This makes sure that we also get the same hashcode for two values considered equal
|
||||
// by Equals().
|
||||
return other._quality.HasValue && (_quality.Value == other._quality.Value);
|
||||
}
|
||||
|
||||
// If we don't have a quality value, then 'other' must also have no quality assigned in order to be
|
||||
// considered equal.
|
||||
return !other._quality.HasValue;
|
||||
}
|
||||
|
||||
public override int GetHashCode()
|
||||
{
|
||||
var result = StringComparer.OrdinalIgnoreCase.GetHashCode(_value);
|
||||
|
||||
if (_quality.HasValue)
|
||||
{
|
||||
result = result ^ _quality.Value.GetHashCode();
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
public static StringWithQualityHeaderValue Parse(string input)
|
||||
{
|
||||
var index = 0;
|
||||
return SingleValueParser.ParseValue(input, ref index);
|
||||
}
|
||||
|
||||
public static bool TryParse(string input, out StringWithQualityHeaderValue parsedValue)
|
||||
{
|
||||
var index = 0;
|
||||
return SingleValueParser.TryParseValue(input, ref index, out parsedValue);
|
||||
}
|
||||
|
||||
public static IList<StringWithQualityHeaderValue> ParseList(IList<string> input)
|
||||
{
|
||||
return MultipleValueParser.ParseValues(input);
|
||||
}
|
||||
|
||||
public static bool TryParseList(IList<string> input, out IList<StringWithQualityHeaderValue> parsedValues)
|
||||
{
|
||||
return MultipleValueParser.TryParseValues(input, out parsedValues);
|
||||
}
|
||||
|
||||
private static int GetStringWithQualityLength(string input, int startIndex, out StringWithQualityHeaderValue parsedValue)
|
||||
{
|
||||
Contract.Requires(startIndex >= 0);
|
||||
|
||||
parsedValue = null;
|
||||
|
||||
if (string.IsNullOrEmpty(input) || (startIndex >= input.Length))
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Parse the value string: <value> in '<value>; q=<quality>'
|
||||
var valueLength = HttpRuleParser.GetTokenLength(input, startIndex);
|
||||
|
||||
if (valueLength == 0)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
StringWithQualityHeaderValue result = new StringWithQualityHeaderValue();
|
||||
result._value = input.Substring(startIndex, valueLength);
|
||||
var current = startIndex + valueLength;
|
||||
current = current + HttpRuleParser.GetWhitespaceLength(input, current);
|
||||
|
||||
if ((current == input.Length) || (input[current] != ';'))
|
||||
{
|
||||
parsedValue = result;
|
||||
return current - startIndex; // we have a valid token, but no quality.
|
||||
}
|
||||
|
||||
current++; // skip ';' separator
|
||||
current = current + HttpRuleParser.GetWhitespaceLength(input, current);
|
||||
|
||||
// If we found a ';' separator, it must be followed by a quality information
|
||||
if (!TryReadQuality(input, result, ref current))
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
parsedValue = result;
|
||||
return current - startIndex;
|
||||
}
|
||||
|
||||
private static bool TryReadQuality(string input, StringWithQualityHeaderValue result, ref int index)
|
||||
{
|
||||
var current = index;
|
||||
|
||||
// See if we have a quality value by looking for "q"
|
||||
if ((current == input.Length) || ((input[current] != 'q') && (input[current] != 'Q')))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
current++; // skip 'q' identifier
|
||||
current = current + HttpRuleParser.GetWhitespaceLength(input, current);
|
||||
|
||||
// If we found "q" it must be followed by "="
|
||||
if ((current == input.Length) || (input[current] != '='))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
current++; // skip '=' separator
|
||||
current = current + HttpRuleParser.GetWhitespaceLength(input, current);
|
||||
|
||||
if (current == input.Length)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
int qualityLength = HttpRuleParser.GetNumberLength(input, current, true);
|
||||
|
||||
if (qualityLength == 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
double quality;
|
||||
if (!double.TryParse(input.Substring(current, qualityLength), NumberStyles.AllowDecimalPoint,
|
||||
NumberFormatInfo.InvariantInfo, out quality))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if ((quality < 0) || (quality > 1))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
result._quality = quality;
|
||||
|
||||
current = current + qualityLength;
|
||||
current = current + HttpRuleParser.GetWhitespaceLength(input, current);
|
||||
|
||||
index = current;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,71 @@
|
|||
// Copyright (c) Microsoft Open Technologies, Inc. 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.Collections.Generic;
|
||||
|
||||
namespace Microsoft.Net.Http.Headers
|
||||
{
|
||||
/// <summary>
|
||||
/// Implementation of <see cref="IComparer{T}"/> that can compare content negotiation header fields
|
||||
/// based on their quality values (a.k.a q-values). This applies to values used in accept-charset,
|
||||
/// accept-encoding, accept-language and related header fields with similar syntax rules. See
|
||||
/// <see cref="MediaTypeHeaderValueComparer"/> for a comparer for media type
|
||||
/// q-values.
|
||||
/// </summary>
|
||||
public class StringWithQualityHeaderValueComparer : IComparer<StringWithQualityHeaderValue>
|
||||
{
|
||||
private static readonly StringWithQualityHeaderValueComparer _qualityComparer =
|
||||
new StringWithQualityHeaderValueComparer();
|
||||
|
||||
private StringWithQualityHeaderValueComparer()
|
||||
{
|
||||
}
|
||||
|
||||
public static StringWithQualityHeaderValueComparer QualityComparer
|
||||
{
|
||||
get { return _qualityComparer; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Compares two <see cref="StringWithQualityHeaderValue"/> based on their quality value
|
||||
/// (a.k.a their "q-value").
|
||||
/// Values with identical q-values are considered equal (i.e the result is 0) with the exception of wild-card
|
||||
/// values (i.e. a value of "*") which are considered less than non-wild-card values. This allows to sort
|
||||
/// a sequence of <see cref="StringWithQualityHeaderValue"/> following their q-values ending up with any
|
||||
/// wild-cards at the end.
|
||||
/// </summary>
|
||||
/// <param name="stringWithQuality1">The first value to compare.</param>
|
||||
/// <param name="stringWithQuality2">The second value to compare</param>
|
||||
/// <returns>The result of the comparison.</returns>
|
||||
public int Compare([NotNull] StringWithQualityHeaderValue stringWithQuality1,
|
||||
[NotNull] StringWithQualityHeaderValue stringWithQuality2)
|
||||
{
|
||||
var quality1 = stringWithQuality1.Quality ?? HeaderQuality.Match;
|
||||
var quality2 = stringWithQuality2.Quality ?? HeaderQuality.Match;
|
||||
var qualityDifference = quality1 - quality2;
|
||||
if (qualityDifference < 0)
|
||||
{
|
||||
return -1;
|
||||
}
|
||||
else if (qualityDifference > 0)
|
||||
{
|
||||
return 1;
|
||||
}
|
||||
|
||||
if (!String.Equals(stringWithQuality1.Value, stringWithQuality2.Value, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
if (String.Equals(stringWithQuality1.Value, "*", StringComparison.Ordinal))
|
||||
{
|
||||
return -1;
|
||||
}
|
||||
else if (String.Equals(stringWithQuality2.Value, "*", StringComparison.Ordinal))
|
||||
{
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,19 @@
|
|||
{
|
||||
"version": "1.0.0-*",
|
||||
"dependencies": {
|
||||
},
|
||||
"frameworks" : {
|
||||
"net45" : { },
|
||||
"aspnet50" : { },
|
||||
"aspnetcore50" : {
|
||||
"dependencies": {
|
||||
"System.Collections": "4.0.10-beta-*",
|
||||
"System.Diagnostics.Contracts": "4.0.0-beta-*",
|
||||
"System.Globalization": "4.0.10-beta-*",
|
||||
"System.Globalization.Extensions": "4.0.0-beta-*",
|
||||
"System.Text.Encoding": "4.0.10-beta-*",
|
||||
"System.Runtime": "4.0.20-beta-*"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,206 @@
|
|||
// Copyright (c) Microsoft Open Technologies, Inc. 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.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Microsoft.AspNet.PipelineCore.Collections;
|
||||
using Microsoft.Net.Http.Headers;
|
||||
using Xunit;
|
||||
|
||||
namespace Microsoft.AspNet.Http.Headers
|
||||
{
|
||||
public class HeaderDictionaryTypeExtensionsTest
|
||||
{
|
||||
[Fact]
|
||||
public void GetT_KnownTypeWithValidValue_Success()
|
||||
{
|
||||
var headers = new HeaderDictionary();
|
||||
headers[HeaderNames.ContentType] = "text/plain";
|
||||
|
||||
var result = headers.Get<MediaTypeHeaderValue>(HeaderNames.ContentType);
|
||||
|
||||
var expected = new MediaTypeHeaderValue("text/plain");
|
||||
Assert.Equal(expected, result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetT_KnownTypeWithMissingValue_Null()
|
||||
{
|
||||
var headers = new HeaderDictionary();
|
||||
|
||||
var result = headers.Get<MediaTypeHeaderValue>(HeaderNames.ContentType);
|
||||
|
||||
Assert.Null(result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetT_KnownTypeWithInvalidValue_Null()
|
||||
{
|
||||
var headers = new HeaderDictionary();
|
||||
headers[HeaderNames.ContentType] = "invalid";
|
||||
|
||||
var result = headers.Get<MediaTypeHeaderValue>(HeaderNames.ContentType);
|
||||
|
||||
Assert.Null(result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetT_UnknownTypeWithTryParseAndValidValue_Success()
|
||||
{
|
||||
var headers = new HeaderDictionary();
|
||||
headers["custom"] = "valid";
|
||||
|
||||
var result = headers.Get<TestHeaderValue>("custom");
|
||||
Assert.NotNull(result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetT_UnknownTypeWithTryParseAndInvalidValue_Null()
|
||||
{
|
||||
var headers = new HeaderDictionary();
|
||||
headers["custom"] = "invalid";
|
||||
|
||||
var result = headers.Get<TestHeaderValue>("custom");
|
||||
Assert.Null(result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetT_UnknownTypeWithTryParseAndMissingValue_Null()
|
||||
{
|
||||
var headers = new HeaderDictionary();
|
||||
|
||||
var result = headers.Get<TestHeaderValue>("custom");
|
||||
Assert.Null(result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetT_UnknownTypeWithoutTryParse_Throws()
|
||||
{
|
||||
var headers = new HeaderDictionary();
|
||||
headers["custom"] = "valid";
|
||||
|
||||
Assert.Throws<NotSupportedException>(() => headers.Get<object>("custom"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetListT_KnownTypeWithValidValue_Success()
|
||||
{
|
||||
var headers = new HeaderDictionary();
|
||||
headers[HeaderNames.Accept] = "text/plain; q=0.9, text/other, */*";
|
||||
|
||||
var result = headers.GetList<MediaTypeHeaderValue>(HeaderNames.Accept);
|
||||
|
||||
var expected = new[] {
|
||||
new MediaTypeHeaderValue("text/plain", 0.9),
|
||||
new MediaTypeHeaderValue("text/other"),
|
||||
new MediaTypeHeaderValue("*/*"),
|
||||
}.ToList();
|
||||
Assert.Equal(expected, result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetListT_KnownTypeWithMissingValue_Null()
|
||||
{
|
||||
var headers = new HeaderDictionary();
|
||||
|
||||
var result = headers.GetList<MediaTypeHeaderValue>(HeaderNames.Accept);
|
||||
|
||||
Assert.Null(result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetListT_KnownTypeWithInvalidValue_Null()
|
||||
{
|
||||
var headers = new HeaderDictionary();
|
||||
headers[HeaderNames.Accept] = "invalid";
|
||||
|
||||
var result = headers.GetList<MediaTypeHeaderValue>(HeaderNames.Accept);
|
||||
|
||||
Assert.Null(result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetListT_UnknownTypeWithTryParseListAndValidValue_Success()
|
||||
{
|
||||
var headers = new HeaderDictionary();
|
||||
headers["custom"] = "valid";
|
||||
|
||||
var results = headers.GetList<TestHeaderValue>("custom");
|
||||
Assert.NotNull(results);
|
||||
Assert.Equal(new[] { new TestHeaderValue() }.ToList(), results);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetListT_UnknownTypeWithTryParseListAndInvalidValue_Null()
|
||||
{
|
||||
var headers = new HeaderDictionary();
|
||||
headers["custom"] = "invalid";
|
||||
|
||||
var results = headers.GetList<TestHeaderValue>("custom");
|
||||
Assert.Null(results);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetListT_UnknownTypeWithTryParseListAndMissingValue_Null()
|
||||
{
|
||||
var headers = new HeaderDictionary();
|
||||
|
||||
var results = headers.GetList<TestHeaderValue>("custom");
|
||||
Assert.Null(results);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetListT_UnknownTypeWithoutTryParseList_Throws()
|
||||
{
|
||||
var headers = new HeaderDictionary();
|
||||
headers["custom"] = "valid";
|
||||
|
||||
Assert.Throws<NotSupportedException>(() => headers.GetList<object>("custom"));
|
||||
}
|
||||
|
||||
public class TestHeaderValue
|
||||
{
|
||||
public static bool TryParse(string value, out TestHeaderValue result)
|
||||
{
|
||||
if (string.Equals("valid", value))
|
||||
{
|
||||
result = new TestHeaderValue();
|
||||
return true;
|
||||
}
|
||||
result = null;
|
||||
return false;
|
||||
}
|
||||
|
||||
public static bool TryParseList(IList<string> values, out IList<TestHeaderValue> result)
|
||||
{
|
||||
var results = new List<TestHeaderValue>();
|
||||
foreach (var value in values)
|
||||
{
|
||||
if (string.Equals("valid", value))
|
||||
{
|
||||
results.Add(new TestHeaderValue());
|
||||
}
|
||||
}
|
||||
if (results.Count > 0)
|
||||
{
|
||||
result = results;
|
||||
return true;
|
||||
}
|
||||
result = null;
|
||||
return false;
|
||||
}
|
||||
|
||||
public override bool Equals(object obj)
|
||||
{
|
||||
var other = obj as TestHeaderValue;
|
||||
return other != null;
|
||||
}
|
||||
|
||||
public override int GetHashCode()
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<Project ToolsVersion="14.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
|
||||
<PropertyGroup>
|
||||
<VisualStudioVersion Condition="'$(VisualStudioVersion)' == ''">14.0</VisualStudioVersion>
|
||||
|
|
@ -14,4 +14,9 @@
|
|||
<SchemaVersion>2.0</SchemaVersion>
|
||||
</PropertyGroup>
|
||||
<Import Project="$(VSToolsPath)\AspNet\Microsoft.Web.AspNet.targets" Condition="'$(VSToolsPath)' != ''" />
|
||||
</Project>
|
||||
<ProjectExtensions>
|
||||
<VisualStudio>
|
||||
<UserProperties project_1json__JSONSchema="http://www.asp.net/media/4878834/project.json" />
|
||||
</VisualStudio>
|
||||
</ProjectExtensions>
|
||||
</Project>
|
||||
|
|
@ -5,7 +5,7 @@ using System;
|
|||
using System.Collections.Generic;
|
||||
using Xunit;
|
||||
|
||||
namespace Microsoft.AspNet.WebUtilities
|
||||
namespace Microsoft.AspNet.Http.Extensions
|
||||
{
|
||||
public class QueryBuilderTests
|
||||
{
|
||||
|
|
@ -124,7 +124,7 @@ namespace Microsoft.AspNet.Http.Extensions.Tests
|
|||
{
|
||||
}
|
||||
|
||||
public class TestMiddleware
|
||||
public class TestMiddleware
|
||||
{
|
||||
RequestDelegate _next;
|
||||
|
||||
|
|
@ -133,9 +133,10 @@ namespace Microsoft.AspNet.Http.Extensions.Tests
|
|||
_next = next;
|
||||
}
|
||||
|
||||
public async Task Invoke(HttpContext context, ITestService testService)
|
||||
public Task Invoke(HttpContext context, ITestService testService)
|
||||
{
|
||||
context.Items[typeof(ITestService)] = testService;
|
||||
return Task.FromResult(0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -59,26 +59,6 @@ namespace Microsoft.AspNet.PipelineCore.Tests
|
|||
Assert.Null(request.ContentType);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetAcceptHeader_ReturnsNullIfHeaderDoesNotExist()
|
||||
{
|
||||
// Arrange
|
||||
var request = GetRequestWithAcceptHeader(acceptHeader: null);
|
||||
|
||||
// Act and Assert
|
||||
Assert.Null(request.Accept);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetAcceptCharsetHeader_ReturnsNullIfHeaderDoesNotExist()
|
||||
{
|
||||
// Arrange
|
||||
var request = GetRequestWithAcceptCharsetHeader(acceptCharset: null);
|
||||
|
||||
// Act and Assert
|
||||
Assert.Null(request.AcceptCharset);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Host_GetsHostFromHeaders()
|
||||
{
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<Project ToolsVersion="14.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
|
||||
<PropertyGroup>
|
||||
<VisualStudioVersion Condition="'$(VisualStudioVersion)' == ''">14.0</VisualStudioVersion>
|
||||
|
|
@ -14,4 +14,9 @@
|
|||
<SchemaVersion>2.0</SchemaVersion>
|
||||
</PropertyGroup>
|
||||
<Import Project="$(VSToolsPath)\AspNet\Microsoft.Web.AspNet.targets" Condition="'$(VSToolsPath)' != ''" />
|
||||
</Project>
|
||||
<ProjectExtensions>
|
||||
<VisualStudio>
|
||||
<UserProperties project_1json__JSONSchema="http://www.asp.net/media/4878834/project.json" />
|
||||
</VisualStudio>
|
||||
</ProjectExtensions>
|
||||
</Project>
|
||||
|
|
@ -2,12 +2,14 @@
|
|||
"dependencies": {
|
||||
"Microsoft.AspNet.Http": "1.0.0-*",
|
||||
"Microsoft.AspNet.WebUtilities": "1.0.0-*",
|
||||
"Microsoft.AspNet.PipelineCore": "1.0.0-*",
|
||||
"xunit.runner.kre": "1.0.0-*"
|
||||
},
|
||||
"commands": {
|
||||
"test": "xunit.runner.kre"
|
||||
},
|
||||
"frameworks": {
|
||||
"aspnet50": { }
|
||||
"aspnet50": { },
|
||||
"aspnetcore50": { }
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,599 @@
|
|||
// Copyright (c) Microsoft Open Technologies, Inc. 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.Linq;
|
||||
using Xunit;
|
||||
|
||||
namespace Microsoft.Net.Http.Headers
|
||||
{
|
||||
public class CacheControlHeaderValueTest
|
||||
{
|
||||
[Fact]
|
||||
public void Properties_SetAndGetAllProperties_SetValueReturnedInGetter()
|
||||
{
|
||||
var cacheControl = new CacheControlHeaderValue();
|
||||
|
||||
// Bool properties
|
||||
cacheControl.NoCache = true;
|
||||
Assert.True(cacheControl.NoCache, "NoCache");
|
||||
cacheControl.NoStore = true;
|
||||
Assert.True(cacheControl.NoStore, "NoStore");
|
||||
cacheControl.MaxStale = true;
|
||||
Assert.True(cacheControl.MaxStale, "MaxStale");
|
||||
cacheControl.NoTransform = true;
|
||||
Assert.True(cacheControl.NoTransform, "NoTransform");
|
||||
cacheControl.OnlyIfCached = true;
|
||||
Assert.True(cacheControl.OnlyIfCached, "OnlyIfCached");
|
||||
cacheControl.Public = true;
|
||||
Assert.True(cacheControl.Public, "Public");
|
||||
cacheControl.Private = true;
|
||||
Assert.True(cacheControl.Private, "Private");
|
||||
cacheControl.MustRevalidate = true;
|
||||
Assert.True(cacheControl.MustRevalidate, "MustRevalidate");
|
||||
cacheControl.ProxyRevalidate = true;
|
||||
Assert.True(cacheControl.ProxyRevalidate, "ProxyRevalidate");
|
||||
|
||||
// TimeSpan properties
|
||||
TimeSpan timeSpan = new TimeSpan(1, 2, 3);
|
||||
cacheControl.MaxAge = timeSpan;
|
||||
Assert.Equal(timeSpan, cacheControl.MaxAge);
|
||||
cacheControl.SharedMaxAge = timeSpan;
|
||||
Assert.Equal(timeSpan, cacheControl.SharedMaxAge);
|
||||
cacheControl.MaxStaleLimit = timeSpan;
|
||||
Assert.Equal(timeSpan, cacheControl.MaxStaleLimit);
|
||||
cacheControl.MinFresh = timeSpan;
|
||||
Assert.Equal(timeSpan, cacheControl.MinFresh);
|
||||
|
||||
// String collection properties
|
||||
Assert.NotNull(cacheControl.NoCacheHeaders);
|
||||
Assert.Throws<ArgumentException>(() => cacheControl.NoCacheHeaders.Add(null));
|
||||
Assert.Throws<FormatException>(() => cacheControl.NoCacheHeaders.Add("invalid token"));
|
||||
cacheControl.NoCacheHeaders.Add("token");
|
||||
Assert.Equal(1, cacheControl.NoCacheHeaders.Count);
|
||||
Assert.Equal("token", cacheControl.NoCacheHeaders.First());
|
||||
|
||||
Assert.NotNull(cacheControl.PrivateHeaders);
|
||||
Assert.Throws<ArgumentException>(() => cacheControl.PrivateHeaders.Add(null));
|
||||
Assert.Throws<FormatException>(() => cacheControl.PrivateHeaders.Add("invalid token"));
|
||||
cacheControl.PrivateHeaders.Add("token");
|
||||
Assert.Equal(1, cacheControl.PrivateHeaders.Count);
|
||||
Assert.Equal("token", cacheControl.PrivateHeaders.First());
|
||||
|
||||
// NameValueHeaderValue collection property
|
||||
Assert.NotNull(cacheControl.Extensions);
|
||||
Assert.Throws<ArgumentNullException>(() => cacheControl.Extensions.Add(null));
|
||||
cacheControl.Extensions.Add(new NameValueHeaderValue("name", "value"));
|
||||
Assert.Equal(1, cacheControl.Extensions.Count);
|
||||
Assert.Equal(new NameValueHeaderValue("name", "value"), cacheControl.Extensions.First());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ToString_UseRequestDirectiveValues_AllSerializedCorrectly()
|
||||
{
|
||||
var cacheControl = new CacheControlHeaderValue();
|
||||
Assert.Equal("", cacheControl.ToString());
|
||||
|
||||
// Note that we allow all combinations of all properties even though the RFC specifies rules what value
|
||||
// can be used together.
|
||||
// Also for property pairs (bool property + collection property) like 'NoCache' and 'NoCacheHeaders' the
|
||||
// caller needs to set the bool property in order for the collection to be populated as string.
|
||||
|
||||
// Cache Request Directive sample
|
||||
cacheControl.NoStore = true;
|
||||
Assert.Equal("no-store", cacheControl.ToString());
|
||||
cacheControl.NoCache = true;
|
||||
Assert.Equal("no-store, no-cache", cacheControl.ToString());
|
||||
cacheControl.MaxAge = new TimeSpan(0, 1, 10);
|
||||
Assert.Equal("no-store, no-cache, max-age=70", cacheControl.ToString());
|
||||
cacheControl.MaxStale = true;
|
||||
Assert.Equal("no-store, no-cache, max-age=70, max-stale", cacheControl.ToString());
|
||||
cacheControl.MaxStaleLimit = new TimeSpan(0, 2, 5);
|
||||
Assert.Equal("no-store, no-cache, max-age=70, max-stale=125", cacheControl.ToString());
|
||||
cacheControl.MinFresh = new TimeSpan(0, 3, 0);
|
||||
Assert.Equal("no-store, no-cache, max-age=70, max-stale=125, min-fresh=180", cacheControl.ToString());
|
||||
|
||||
cacheControl = new CacheControlHeaderValue();
|
||||
cacheControl.NoTransform = true;
|
||||
Assert.Equal("no-transform", cacheControl.ToString());
|
||||
cacheControl.OnlyIfCached = true;
|
||||
Assert.Equal("no-transform, only-if-cached", cacheControl.ToString());
|
||||
cacheControl.Extensions.Add(new NameValueHeaderValue("custom"));
|
||||
cacheControl.Extensions.Add(new NameValueHeaderValue("customName", "customValue"));
|
||||
Assert.Equal("no-transform, only-if-cached, custom, customName=customValue", cacheControl.ToString());
|
||||
|
||||
cacheControl = new CacheControlHeaderValue();
|
||||
cacheControl.Extensions.Add(new NameValueHeaderValue("custom"));
|
||||
Assert.Equal("custom", cacheControl.ToString());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ToString_UseResponseDirectiveValues_AllSerializedCorrectly()
|
||||
{
|
||||
var cacheControl = new CacheControlHeaderValue();
|
||||
Assert.Equal("", cacheControl.ToString());
|
||||
|
||||
cacheControl.NoCache = true;
|
||||
Assert.Equal("no-cache", cacheControl.ToString());
|
||||
cacheControl.NoCacheHeaders.Add("token1");
|
||||
Assert.Equal("no-cache=\"token1\"", cacheControl.ToString());
|
||||
cacheControl.Public = true;
|
||||
Assert.Equal("public, no-cache=\"token1\"", cacheControl.ToString());
|
||||
|
||||
cacheControl = new CacheControlHeaderValue();
|
||||
cacheControl.Private = true;
|
||||
Assert.Equal("private", cacheControl.ToString());
|
||||
cacheControl.PrivateHeaders.Add("token2");
|
||||
cacheControl.PrivateHeaders.Add("token3");
|
||||
Assert.Equal("private=\"token2, token3\"", cacheControl.ToString());
|
||||
cacheControl.MustRevalidate = true;
|
||||
Assert.Equal("must-revalidate, private=\"token2, token3\"", cacheControl.ToString());
|
||||
cacheControl.ProxyRevalidate = true;
|
||||
Assert.Equal("must-revalidate, proxy-revalidate, private=\"token2, token3\"", cacheControl.ToString());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetHashCode_CompareValuesWithBoolFieldsSet_MatchExpectation()
|
||||
{
|
||||
// Verify that different bool fields return different hash values.
|
||||
var values = new CacheControlHeaderValue[9];
|
||||
|
||||
for (int i = 0; i < values.Length; i++)
|
||||
{
|
||||
values[i] = new CacheControlHeaderValue();
|
||||
}
|
||||
|
||||
values[0].ProxyRevalidate = true;
|
||||
values[1].NoCache = true;
|
||||
values[2].NoStore = true;
|
||||
values[3].MaxStale = true;
|
||||
values[4].NoTransform = true;
|
||||
values[5].OnlyIfCached = true;
|
||||
values[6].Public = true;
|
||||
values[7].Private = true;
|
||||
values[8].MustRevalidate = true;
|
||||
|
||||
// Only one bool field set. All hash codes should differ
|
||||
for (int i = 0; i < values.Length; i++)
|
||||
{
|
||||
for (int j = 0; j < values.Length; j++)
|
||||
{
|
||||
if (i != j)
|
||||
{
|
||||
CompareHashCodes(values[i], values[j], false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Validate that two instances with the same bool fields set are equal.
|
||||
values[0].NoCache = true;
|
||||
CompareHashCodes(values[0], values[1], false);
|
||||
values[1].ProxyRevalidate = true;
|
||||
CompareHashCodes(values[0], values[1], true);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetHashCode_CompareValuesWithTimeSpanFieldsSet_MatchExpectation()
|
||||
{
|
||||
// Verify that different timespan fields return different hash values.
|
||||
var values = new CacheControlHeaderValue[4];
|
||||
|
||||
for (int i = 0; i < values.Length; i++)
|
||||
{
|
||||
values[i] = new CacheControlHeaderValue();
|
||||
}
|
||||
|
||||
values[0].MaxAge = new TimeSpan(0, 1, 1);
|
||||
values[1].MaxStaleLimit = new TimeSpan(0, 1, 1);
|
||||
values[2].MinFresh = new TimeSpan(0, 1, 1);
|
||||
values[3].SharedMaxAge = new TimeSpan(0, 1, 1);
|
||||
|
||||
// Only one timespan field set. All hash codes should differ
|
||||
for (int i = 0; i < values.Length; i++)
|
||||
{
|
||||
for (int j = 0; j < values.Length; j++)
|
||||
{
|
||||
if (i != j)
|
||||
{
|
||||
CompareHashCodes(values[i], values[j], false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
values[0].MaxStaleLimit = new TimeSpan(0, 1, 2);
|
||||
CompareHashCodes(values[0], values[1], false);
|
||||
|
||||
values[1].MaxAge = new TimeSpan(0, 1, 1);
|
||||
values[1].MaxStaleLimit = new TimeSpan(0, 1, 2);
|
||||
CompareHashCodes(values[0], values[1], true);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetHashCode_CompareCollectionFieldsSet_MatchExpectation()
|
||||
{
|
||||
var cacheControl1 = new CacheControlHeaderValue();
|
||||
var cacheControl2 = new CacheControlHeaderValue();
|
||||
var cacheControl3 = new CacheControlHeaderValue();
|
||||
var cacheControl4 = new CacheControlHeaderValue();
|
||||
var cacheControl5 = new CacheControlHeaderValue();
|
||||
|
||||
cacheControl1.NoCache = true;
|
||||
cacheControl1.NoCacheHeaders.Add("token2");
|
||||
|
||||
cacheControl2.NoCache = true;
|
||||
cacheControl2.NoCacheHeaders.Add("token1");
|
||||
cacheControl2.NoCacheHeaders.Add("token2");
|
||||
|
||||
CompareHashCodes(cacheControl1, cacheControl2, false);
|
||||
|
||||
cacheControl1.NoCacheHeaders.Add("token1");
|
||||
CompareHashCodes(cacheControl1, cacheControl2, true);
|
||||
|
||||
// Since NoCache and Private generate different hash codes, even if NoCacheHeaders and PrivateHeaders
|
||||
// have the same values, the hash code will be different.
|
||||
cacheControl3.Private = true;
|
||||
cacheControl3.PrivateHeaders.Add("token2");
|
||||
CompareHashCodes(cacheControl1, cacheControl3, false);
|
||||
|
||||
|
||||
cacheControl4.Extensions.Add(new NameValueHeaderValue("custom"));
|
||||
CompareHashCodes(cacheControl1, cacheControl4, false);
|
||||
|
||||
cacheControl5.Extensions.Add(new NameValueHeaderValue("customN", "customV"));
|
||||
cacheControl5.Extensions.Add(new NameValueHeaderValue("custom"));
|
||||
CompareHashCodes(cacheControl4, cacheControl5, false);
|
||||
|
||||
cacheControl4.Extensions.Add(new NameValueHeaderValue("customN", "customV"));
|
||||
CompareHashCodes(cacheControl4, cacheControl5, true);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Equals_CompareValuesWithBoolFieldsSet_MatchExpectation()
|
||||
{
|
||||
// Verify that different bool fields return different hash values.
|
||||
var values = new CacheControlHeaderValue[9];
|
||||
|
||||
for (int i = 0; i < values.Length; i++)
|
||||
{
|
||||
values[i] = new CacheControlHeaderValue();
|
||||
}
|
||||
|
||||
values[0].ProxyRevalidate = true;
|
||||
values[1].NoCache = true;
|
||||
values[2].NoStore = true;
|
||||
values[3].MaxStale = true;
|
||||
values[4].NoTransform = true;
|
||||
values[5].OnlyIfCached = true;
|
||||
values[6].Public = true;
|
||||
values[7].Private = true;
|
||||
values[8].MustRevalidate = true;
|
||||
|
||||
// Only one bool field set. All hash codes should differ
|
||||
for (int i = 0; i < values.Length; i++)
|
||||
{
|
||||
for (int j = 0; j < values.Length; j++)
|
||||
{
|
||||
if (i != j)
|
||||
{
|
||||
CompareValues(values[i], values[j], false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Validate that two instances with the same bool fields set are equal.
|
||||
values[0].NoCache = true;
|
||||
CompareValues(values[0], values[1], false);
|
||||
values[1].ProxyRevalidate = true;
|
||||
CompareValues(values[0], values[1], true);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Equals_CompareValuesWithTimeSpanFieldsSet_MatchExpectation()
|
||||
{
|
||||
// Verify that different timespan fields return different hash values.
|
||||
var values = new CacheControlHeaderValue[4];
|
||||
|
||||
for (int i = 0; i < values.Length; i++)
|
||||
{
|
||||
values[i] = new CacheControlHeaderValue();
|
||||
}
|
||||
|
||||
values[0].MaxAge = new TimeSpan(0, 1, 1);
|
||||
values[1].MaxStaleLimit = new TimeSpan(0, 1, 1);
|
||||
values[2].MinFresh = new TimeSpan(0, 1, 1);
|
||||
values[3].SharedMaxAge = new TimeSpan(0, 1, 1);
|
||||
|
||||
// Only one timespan field set. All hash codes should differ
|
||||
for (int i = 0; i < values.Length; i++)
|
||||
{
|
||||
for (int j = 0; j < values.Length; j++)
|
||||
{
|
||||
if (i != j)
|
||||
{
|
||||
CompareValues(values[i], values[j], false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
values[0].MaxStaleLimit = new TimeSpan(0, 1, 2);
|
||||
CompareValues(values[0], values[1], false);
|
||||
|
||||
values[1].MaxAge = new TimeSpan(0, 1, 1);
|
||||
values[1].MaxStaleLimit = new TimeSpan(0, 1, 2);
|
||||
CompareValues(values[0], values[1], true);
|
||||
|
||||
var value1 = new CacheControlHeaderValue();
|
||||
value1.MaxStale = true;
|
||||
var value2 = new CacheControlHeaderValue();
|
||||
value2.MaxStale = true;
|
||||
CompareValues(value1, value2, true);
|
||||
|
||||
value2.MaxStaleLimit = new TimeSpan(1, 2, 3);
|
||||
CompareValues(value1, value2, false);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Equals_CompareCollectionFieldsSet_MatchExpectation()
|
||||
{
|
||||
var cacheControl1 = new CacheControlHeaderValue();
|
||||
var cacheControl2 = new CacheControlHeaderValue();
|
||||
var cacheControl3 = new CacheControlHeaderValue();
|
||||
var cacheControl4 = new CacheControlHeaderValue();
|
||||
var cacheControl5 = new CacheControlHeaderValue();
|
||||
var cacheControl6 = new CacheControlHeaderValue();
|
||||
|
||||
cacheControl1.NoCache = true;
|
||||
cacheControl1.NoCacheHeaders.Add("token2");
|
||||
|
||||
Assert.False(cacheControl1.Equals(null), "Compare with 'null'");
|
||||
|
||||
cacheControl2.NoCache = true;
|
||||
cacheControl2.NoCacheHeaders.Add("token1");
|
||||
cacheControl2.NoCacheHeaders.Add("token2");
|
||||
|
||||
CompareValues(cacheControl1, cacheControl2, false);
|
||||
|
||||
cacheControl1.NoCacheHeaders.Add("token1");
|
||||
CompareValues(cacheControl1, cacheControl2, true);
|
||||
|
||||
// Since NoCache and Private generate different hash codes, even if NoCacheHeaders and PrivateHeaders
|
||||
// have the same values, the hash code will be different.
|
||||
cacheControl3.Private = true;
|
||||
cacheControl3.PrivateHeaders.Add("token2");
|
||||
CompareValues(cacheControl1, cacheControl3, false);
|
||||
|
||||
cacheControl4.Private = true;
|
||||
cacheControl4.PrivateHeaders.Add("token3");
|
||||
CompareValues(cacheControl3, cacheControl4, false);
|
||||
|
||||
cacheControl5.Extensions.Add(new NameValueHeaderValue("custom"));
|
||||
CompareValues(cacheControl1, cacheControl5, false);
|
||||
|
||||
cacheControl6.Extensions.Add(new NameValueHeaderValue("customN", "customV"));
|
||||
cacheControl6.Extensions.Add(new NameValueHeaderValue("custom"));
|
||||
CompareValues(cacheControl5, cacheControl6, false);
|
||||
|
||||
cacheControl5.Extensions.Add(new NameValueHeaderValue("customN", "customV"));
|
||||
CompareValues(cacheControl5, cacheControl6, true);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TryParse_DifferentValidScenarios_AllReturnTrue()
|
||||
{
|
||||
var expected = new CacheControlHeaderValue();
|
||||
expected.NoCache = true;
|
||||
CheckValidTryParse(" , no-cache ,,", expected);
|
||||
|
||||
expected = new CacheControlHeaderValue();
|
||||
expected.NoCache = true;
|
||||
expected.NoCacheHeaders.Add("token1");
|
||||
expected.NoCacheHeaders.Add("token2");
|
||||
CheckValidTryParse("no-cache=\"token1, token2\"", expected);
|
||||
|
||||
expected = new CacheControlHeaderValue();
|
||||
expected.NoStore = true;
|
||||
expected.MaxAge = new TimeSpan(0, 0, 125);
|
||||
expected.MaxStale = true;
|
||||
CheckValidTryParse(" no-store , max-age = 125, max-stale,", expected);
|
||||
|
||||
expected = new CacheControlHeaderValue();
|
||||
expected.MinFresh = new TimeSpan(0, 0, 123);
|
||||
expected.NoTransform = true;
|
||||
expected.OnlyIfCached = true;
|
||||
expected.Extensions.Add(new NameValueHeaderValue("custom"));
|
||||
CheckValidTryParse("min-fresh=123, no-transform, only-if-cached, custom", expected);
|
||||
|
||||
expected = new CacheControlHeaderValue();
|
||||
expected.Public = true;
|
||||
expected.Private = true;
|
||||
expected.PrivateHeaders.Add("token1");
|
||||
expected.MustRevalidate = true;
|
||||
expected.ProxyRevalidate = true;
|
||||
expected.Extensions.Add(new NameValueHeaderValue("c", "d"));
|
||||
expected.Extensions.Add(new NameValueHeaderValue("a", "b"));
|
||||
CheckValidTryParse(",public, , private=\"token1\", must-revalidate, c=d, proxy-revalidate, a=b", expected);
|
||||
|
||||
expected = new CacheControlHeaderValue();
|
||||
expected.Private = true;
|
||||
expected.SharedMaxAge = new TimeSpan(0, 0, 1234567890);
|
||||
expected.MaxAge = new TimeSpan(0, 0, 987654321);
|
||||
CheckValidTryParse("s-maxage=1234567890, private, max-age = 987654321,", expected);
|
||||
|
||||
expected = new CacheControlHeaderValue();
|
||||
expected.Extensions.Add(new NameValueHeaderValue("custom", ""));
|
||||
CheckValidTryParse("custom=", expected);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(null)]
|
||||
[InlineData("")]
|
||||
[InlineData(" ")]
|
||||
// Token-only values
|
||||
[InlineData("no-store=15")]
|
||||
[InlineData("no-store=")]
|
||||
[InlineData("no-transform=a")]
|
||||
[InlineData("no-transform=")]
|
||||
[InlineData("only-if-cached=\"x\"")]
|
||||
[InlineData("only-if-cached=")]
|
||||
[InlineData("public=\"x\"")]
|
||||
[InlineData("public=")]
|
||||
[InlineData("must-revalidate=\"1\"")]
|
||||
[InlineData("must-revalidate=")]
|
||||
[InlineData("proxy-revalidate=x")]
|
||||
[InlineData("proxy-revalidate=")]
|
||||
// Token with optional field-name list
|
||||
[InlineData("no-cache=")]
|
||||
[InlineData("no-cache=token")]
|
||||
[InlineData("no-cache=\"token")]
|
||||
[InlineData("no-cache=\"\"")] // at least one token expected as value
|
||||
[InlineData("private=")]
|
||||
[InlineData("private=token")]
|
||||
[InlineData("private=\"token")]
|
||||
[InlineData("private=\",\"")] // at least one token expected as value
|
||||
[InlineData("private=\"=\"")]
|
||||
// Token with delta-seconds value
|
||||
[InlineData("max-age")]
|
||||
[InlineData("max-age=")]
|
||||
[InlineData("max-age=a")]
|
||||
[InlineData("max-age=\"1\"")]
|
||||
[InlineData("max-age=1.5")]
|
||||
[InlineData("max-stale=")]
|
||||
[InlineData("max-stale=a")]
|
||||
[InlineData("max-stale=\"1\"")]
|
||||
[InlineData("max-stale=1.5")]
|
||||
[InlineData("min-fresh")]
|
||||
[InlineData("min-fresh=")]
|
||||
[InlineData("min-fresh=a")]
|
||||
[InlineData("min-fresh=\"1\"")]
|
||||
[InlineData("min-fresh=1.5")]
|
||||
[InlineData("s-maxage")]
|
||||
[InlineData("s-maxage=")]
|
||||
[InlineData("s-maxage=a")]
|
||||
[InlineData("s-maxage=\"1\"")]
|
||||
[InlineData("s-maxage=1.5")]
|
||||
// Invalid Extension values
|
||||
[InlineData("custom value")]
|
||||
public void TryParse_DifferentInvalidScenarios_ReturnsFalse(string input)
|
||||
{
|
||||
CheckInvalidTryParse(input);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Parse_SetOfValidValueStrings_ParsedCorrectly()
|
||||
{
|
||||
// Just verify parser is implemented correctly. Don't try to test syntax parsed by CacheControlHeaderValue.
|
||||
var expected = new CacheControlHeaderValue();
|
||||
expected.NoStore = true;
|
||||
expected.MinFresh = new TimeSpan(0, 2, 3);
|
||||
CheckValidParse(" , no-store, min-fresh=123", expected);
|
||||
|
||||
expected = new CacheControlHeaderValue();
|
||||
expected.MaxStale = true;
|
||||
expected.NoCache = true;
|
||||
expected.NoCacheHeaders.Add("t");
|
||||
CheckValidParse("max-stale, no-cache=\"t\", ,,", expected);
|
||||
|
||||
expected = new CacheControlHeaderValue();
|
||||
expected.Extensions.Add(new NameValueHeaderValue("custom"));
|
||||
CheckValidParse("custom =", expected);
|
||||
|
||||
expected = new CacheControlHeaderValue();
|
||||
expected.Extensions.Add(new NameValueHeaderValue("custom", ""));
|
||||
CheckValidParse("custom =", expected);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Parse_SetOfInvalidValueStrings_Throws()
|
||||
{
|
||||
CheckInvalidParse(null);
|
||||
CheckInvalidParse("");
|
||||
CheckInvalidParse(" ");
|
||||
CheckInvalidParse("no-cache,=");
|
||||
CheckInvalidParse("max-age=123x");
|
||||
CheckInvalidParse("=no-cache");
|
||||
CheckInvalidParse("no-cache no-store");
|
||||
CheckInvalidParse("会");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TryParse_SetOfValidValueStrings_ParsedCorrectly()
|
||||
{
|
||||
// Just verify parser is implemented correctly. Don't try to test syntax parsed by CacheControlHeaderValue.
|
||||
var expected = new CacheControlHeaderValue();
|
||||
expected.NoStore = true;
|
||||
expected.MinFresh = new TimeSpan(0, 2, 3);
|
||||
CheckValidTryParse(" , no-store, min-fresh=123", expected);
|
||||
|
||||
expected = new CacheControlHeaderValue();
|
||||
expected.MaxStale = true;
|
||||
expected.NoCache = true;
|
||||
expected.NoCacheHeaders.Add("t");
|
||||
CheckValidTryParse("max-stale, no-cache=\"t\", ,,", expected);
|
||||
|
||||
expected = new CacheControlHeaderValue();
|
||||
expected.Extensions.Add(new NameValueHeaderValue("custom"));
|
||||
CheckValidTryParse("custom = ", expected);
|
||||
|
||||
expected = new CacheControlHeaderValue();
|
||||
expected.Extensions.Add(new NameValueHeaderValue("custom", ""));
|
||||
CheckValidTryParse("custom =", expected);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TryParse_SetOfInvalidValueStrings_ReturnsFalse()
|
||||
{
|
||||
CheckInvalidTryParse("no-cache,=");
|
||||
CheckInvalidTryParse("max-age=123x");
|
||||
CheckInvalidTryParse("=no-cache");
|
||||
CheckInvalidTryParse("no-cache no-store");
|
||||
CheckInvalidTryParse("会");
|
||||
}
|
||||
|
||||
#region Helper methods
|
||||
|
||||
private void CompareHashCodes(CacheControlHeaderValue x, CacheControlHeaderValue y, bool areEqual)
|
||||
{
|
||||
if (areEqual)
|
||||
{
|
||||
Assert.Equal(x.GetHashCode(), y.GetHashCode());
|
||||
}
|
||||
else
|
||||
{
|
||||
Assert.NotEqual(x.GetHashCode(), y.GetHashCode());
|
||||
}
|
||||
}
|
||||
|
||||
private void CompareValues(CacheControlHeaderValue x, CacheControlHeaderValue y, bool areEqual)
|
||||
{
|
||||
Assert.Equal(areEqual, x.Equals(y));
|
||||
Assert.Equal(areEqual, y.Equals(x));
|
||||
}
|
||||
|
||||
private void CheckValidParse(string input, CacheControlHeaderValue expectedResult)
|
||||
{
|
||||
var result = CacheControlHeaderValue.Parse(input);
|
||||
Assert.Equal(expectedResult, result);
|
||||
}
|
||||
|
||||
private void CheckInvalidParse(string input)
|
||||
{
|
||||
Assert.Throws<FormatException>(() => CacheControlHeaderValue.Parse(input));
|
||||
}
|
||||
|
||||
private void CheckValidTryParse(string input, CacheControlHeaderValue expectedResult)
|
||||
{
|
||||
CacheControlHeaderValue result = null;
|
||||
Assert.True(CacheControlHeaderValue.TryParse(input, out result));
|
||||
Assert.Equal(expectedResult, result);
|
||||
}
|
||||
|
||||
private void CheckInvalidTryParse(string input)
|
||||
{
|
||||
CacheControlHeaderValue result = null;
|
||||
Assert.False(CacheControlHeaderValue.TryParse(input, out result));
|
||||
Assert.Null(result);
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,609 @@
|
|||
// Copyright (c) Microsoft Open Technologies, Inc. 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.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Xunit;
|
||||
|
||||
namespace Microsoft.Net.Http.Headers
|
||||
{
|
||||
public class ContentDispositionHeaderValueTest
|
||||
{
|
||||
[Fact]
|
||||
public void Ctor_ContentDispositionNull_Throw()
|
||||
{
|
||||
Assert.Throws<ArgumentException>(() => new ContentDispositionHeaderValue(null));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Ctor_ContentDispositionEmpty_Throw()
|
||||
{
|
||||
// null and empty should be treated the same. So we also throw for empty strings.
|
||||
Assert.Throws<ArgumentException>(() => new ContentDispositionHeaderValue(string.Empty));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Ctor_ContentDispositionInvalidFormat_ThrowFormatException()
|
||||
{
|
||||
// When adding values using strongly typed objects, no leading/trailing LWS (whitespaces) are allowed.
|
||||
AssertFormatException(" inline ");
|
||||
AssertFormatException(" inline");
|
||||
AssertFormatException("inline ");
|
||||
AssertFormatException("\"inline\"");
|
||||
AssertFormatException("te xt");
|
||||
AssertFormatException("te=xt");
|
||||
AssertFormatException("teäxt");
|
||||
AssertFormatException("text;");
|
||||
AssertFormatException("te/xt;");
|
||||
AssertFormatException("inline; name=someName; ");
|
||||
AssertFormatException("text;name=someName"); // ctor takes only disposition-type name, no parameters
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Ctor_ContentDispositionValidFormat_SuccessfullyCreated()
|
||||
{
|
||||
var contentDisposition = new ContentDispositionHeaderValue("inline");
|
||||
Assert.Equal("inline", contentDisposition.DispositionType);
|
||||
Assert.Equal(0, contentDisposition.Parameters.Count);
|
||||
Assert.Null(contentDisposition.Name);
|
||||
Assert.Null(contentDisposition.FileName);
|
||||
Assert.Null(contentDisposition.CreationDate);
|
||||
Assert.Null(contentDisposition.ModificationDate);
|
||||
Assert.Null(contentDisposition.ReadDate);
|
||||
Assert.Null(contentDisposition.Size);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Parameters_AddNull_Throw()
|
||||
{
|
||||
var contentDisposition = new ContentDispositionHeaderValue("inline");
|
||||
Assert.Throws<ArgumentNullException>(() => contentDisposition.Parameters.Add(null));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ContentDisposition_SetAndGetContentDisposition_MatchExpectations()
|
||||
{
|
||||
var contentDisposition = new ContentDispositionHeaderValue("inline");
|
||||
Assert.Equal("inline", contentDisposition.DispositionType);
|
||||
|
||||
contentDisposition.DispositionType = "attachment";
|
||||
Assert.Equal("attachment", contentDisposition.DispositionType);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Name_SetNameAndValidateObject_ParametersEntryForNameAdded()
|
||||
{
|
||||
var contentDisposition = new ContentDispositionHeaderValue("inline");
|
||||
contentDisposition.Name = "myname";
|
||||
Assert.Equal("myname", contentDisposition.Name);
|
||||
Assert.Equal(1, contentDisposition.Parameters.Count);
|
||||
Assert.Equal("name", contentDisposition.Parameters.First().Name);
|
||||
|
||||
contentDisposition.Name = null;
|
||||
Assert.Null(contentDisposition.Name);
|
||||
Assert.Equal(0, contentDisposition.Parameters.Count);
|
||||
contentDisposition.Name = null; // It's OK to set it again to null; no exception.
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Name_AddNameParameterThenUseProperty_ParametersEntryIsOverwritten()
|
||||
{
|
||||
var contentDisposition = new ContentDispositionHeaderValue("inline");
|
||||
|
||||
// Note that uppercase letters are used. Comparison should happen case-insensitive.
|
||||
NameValueHeaderValue name = new NameValueHeaderValue("NAME", "old_name");
|
||||
contentDisposition.Parameters.Add(name);
|
||||
Assert.Equal(1, contentDisposition.Parameters.Count);
|
||||
Assert.Equal("NAME", contentDisposition.Parameters.First().Name);
|
||||
|
||||
contentDisposition.Name = "new_name";
|
||||
Assert.Equal("new_name", contentDisposition.Name);
|
||||
Assert.Equal(1, contentDisposition.Parameters.Count);
|
||||
Assert.Equal("NAME", contentDisposition.Parameters.First().Name);
|
||||
|
||||
contentDisposition.Parameters.Remove(name);
|
||||
Assert.Null(contentDisposition.Name);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void FileName_AddNameParameterThenUseProperty_ParametersEntryIsOverwritten()
|
||||
{
|
||||
var contentDisposition = new ContentDispositionHeaderValue("inline");
|
||||
|
||||
// Note that uppercase letters are used. Comparison should happen case-insensitive.
|
||||
var fileName = new NameValueHeaderValue("FILENAME", "old_name");
|
||||
contentDisposition.Parameters.Add(fileName);
|
||||
Assert.Equal(1, contentDisposition.Parameters.Count);
|
||||
Assert.Equal("FILENAME", contentDisposition.Parameters.First().Name);
|
||||
|
||||
contentDisposition.FileName = "new_name";
|
||||
Assert.Equal("new_name", contentDisposition.FileName);
|
||||
Assert.Equal(1, contentDisposition.Parameters.Count);
|
||||
Assert.Equal("FILENAME", contentDisposition.Parameters.First().Name);
|
||||
|
||||
contentDisposition.Parameters.Remove(fileName);
|
||||
Assert.Null(contentDisposition.FileName);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void FileName_NeedsEncoding_EncodedAndDecodedCorrectly()
|
||||
{
|
||||
var contentDisposition = new ContentDispositionHeaderValue("inline");
|
||||
|
||||
contentDisposition.FileName = "FileÃName.bat";
|
||||
Assert.Equal("FileÃName.bat", contentDisposition.FileName);
|
||||
Assert.Equal(1, contentDisposition.Parameters.Count);
|
||||
Assert.Equal("filename", contentDisposition.Parameters.First().Name);
|
||||
Assert.Equal("\"=?utf-8?B?RmlsZcODTmFtZS5iYXQ=?=\"", contentDisposition.Parameters.First().Value);
|
||||
|
||||
contentDisposition.Parameters.Remove(contentDisposition.Parameters.First());
|
||||
Assert.Null(contentDisposition.FileName);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void FileName_UnknownOrBadEncoding_PropertyFails()
|
||||
{
|
||||
var contentDisposition = new ContentDispositionHeaderValue("inline");
|
||||
|
||||
// Note that uppercase letters are used. Comparison should happen case-insensitive.
|
||||
var fileName = new NameValueHeaderValue("FILENAME", "\"=?utf-99?Q?R=mlsZcODTmFtZS5iYXQ=?=\"");
|
||||
contentDisposition.Parameters.Add(fileName);
|
||||
Assert.Equal(1, contentDisposition.Parameters.Count);
|
||||
Assert.Equal("FILENAME", contentDisposition.Parameters.First().Name);
|
||||
Assert.Equal("\"=?utf-99?Q?R=mlsZcODTmFtZS5iYXQ=?=\"", contentDisposition.Parameters.First().Value);
|
||||
Assert.Equal("\"=?utf-99?Q?R=mlsZcODTmFtZS5iYXQ=?=\"", contentDisposition.FileName);
|
||||
|
||||
contentDisposition.FileName = "new_name";
|
||||
Assert.Equal("new_name", contentDisposition.FileName);
|
||||
Assert.Equal(1, contentDisposition.Parameters.Count);
|
||||
Assert.Equal("FILENAME", contentDisposition.Parameters.First().Name);
|
||||
|
||||
contentDisposition.Parameters.Remove(fileName);
|
||||
Assert.Null(contentDisposition.FileName);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void FileNameStar_AddNameParameterThenUseProperty_ParametersEntryIsOverwritten()
|
||||
{
|
||||
var contentDisposition = new ContentDispositionHeaderValue("inline");
|
||||
|
||||
// Note that uppercase letters are used. Comparison should happen case-insensitive.
|
||||
var fileNameStar = new NameValueHeaderValue("FILENAME*", "old_name");
|
||||
contentDisposition.Parameters.Add(fileNameStar);
|
||||
Assert.Equal(1, contentDisposition.Parameters.Count);
|
||||
Assert.Equal("FILENAME*", contentDisposition.Parameters.First().Name);
|
||||
Assert.Null(contentDisposition.FileNameStar); // Decode failure
|
||||
|
||||
contentDisposition.FileNameStar = "new_name";
|
||||
Assert.Equal("new_name", contentDisposition.FileNameStar);
|
||||
Assert.Equal(1, contentDisposition.Parameters.Count);
|
||||
Assert.Equal("FILENAME*", contentDisposition.Parameters.First().Name);
|
||||
Assert.Equal("UTF-8\'\'new_name", contentDisposition.Parameters.First().Value);
|
||||
|
||||
contentDisposition.Parameters.Remove(fileNameStar);
|
||||
Assert.Null(contentDisposition.FileNameStar);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void FileNameStar_NeedsEncoding_EncodedAndDecodedCorrectly()
|
||||
{
|
||||
var contentDisposition = new ContentDispositionHeaderValue("inline");
|
||||
|
||||
contentDisposition.FileNameStar = "FileÃName.bat";
|
||||
Assert.Equal("FileÃName.bat", contentDisposition.FileNameStar);
|
||||
Assert.Equal(1, contentDisposition.Parameters.Count);
|
||||
Assert.Equal("filename*", contentDisposition.Parameters.First().Name);
|
||||
Assert.Equal("UTF-8\'\'File%C3%83Name.bat", contentDisposition.Parameters.First().Value);
|
||||
|
||||
contentDisposition.Parameters.Remove(contentDisposition.Parameters.First());
|
||||
Assert.Null(contentDisposition.FileNameStar);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void FileNameStar_UnknownOrBadEncoding_PropertyFails()
|
||||
{
|
||||
var contentDisposition = new ContentDispositionHeaderValue("inline");
|
||||
|
||||
// Note that uppercase letters are used. Comparison should happen case-insensitive.
|
||||
var fileNameStar = new NameValueHeaderValue("FILENAME*", "utf-99'lang'File%CZName.bat");
|
||||
contentDisposition.Parameters.Add(fileNameStar);
|
||||
Assert.Equal(1, contentDisposition.Parameters.Count);
|
||||
Assert.Equal("FILENAME*", contentDisposition.Parameters.First().Name);
|
||||
Assert.Equal("utf-99'lang'File%CZName.bat", contentDisposition.Parameters.First().Value);
|
||||
Assert.Null(contentDisposition.FileNameStar); // Decode failure
|
||||
|
||||
contentDisposition.FileNameStar = "new_name";
|
||||
Assert.Equal("new_name", contentDisposition.FileNameStar);
|
||||
Assert.Equal(1, contentDisposition.Parameters.Count);
|
||||
Assert.Equal("FILENAME*", contentDisposition.Parameters.First().Name);
|
||||
|
||||
contentDisposition.Parameters.Remove(fileNameStar);
|
||||
Assert.Null(contentDisposition.FileNameStar);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Dates_AddDateParameterThenUseProperty_ParametersEntryIsOverwritten()
|
||||
{
|
||||
string validDateString = "\"Tue, 15 Nov 1994 08:12:31 GMT\"";
|
||||
DateTimeOffset validDate = DateTimeOffset.Parse("Tue, 15 Nov 1994 08:12:31 GMT");
|
||||
|
||||
var contentDisposition = new ContentDispositionHeaderValue("inline");
|
||||
|
||||
// Note that uppercase letters are used. Comparison should happen case-insensitive.
|
||||
var dateParameter = new NameValueHeaderValue("Creation-DATE", validDateString);
|
||||
contentDisposition.Parameters.Add(dateParameter);
|
||||
Assert.Equal(1, contentDisposition.Parameters.Count);
|
||||
Assert.Equal("Creation-DATE", contentDisposition.Parameters.First().Name);
|
||||
|
||||
Assert.Equal(validDate, contentDisposition.CreationDate);
|
||||
|
||||
var newDate = validDate.AddSeconds(1);
|
||||
contentDisposition.CreationDate = newDate;
|
||||
Assert.Equal(newDate, contentDisposition.CreationDate);
|
||||
Assert.Equal(1, contentDisposition.Parameters.Count);
|
||||
Assert.Equal("Creation-DATE", contentDisposition.Parameters.First().Name);
|
||||
Assert.Equal("\"Tue, 15 Nov 1994 08:12:32 GMT\"", contentDisposition.Parameters.First().Value);
|
||||
|
||||
contentDisposition.Parameters.Remove(dateParameter);
|
||||
Assert.Null(contentDisposition.CreationDate);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Dates_InvalidDates_PropertyFails()
|
||||
{
|
||||
string invalidDateString = "\"Tue, 15 Nov 94 08:12 GMT\"";
|
||||
|
||||
var contentDisposition = new ContentDispositionHeaderValue("inline");
|
||||
|
||||
// Note that uppercase letters are used. Comparison should happen case-insensitive.
|
||||
var dateParameter = new NameValueHeaderValue("read-DATE", invalidDateString);
|
||||
contentDisposition.Parameters.Add(dateParameter);
|
||||
Assert.Equal(1, contentDisposition.Parameters.Count);
|
||||
Assert.Equal("read-DATE", contentDisposition.Parameters.First().Name);
|
||||
|
||||
Assert.Null(contentDisposition.ReadDate);
|
||||
|
||||
contentDisposition.ReadDate = null;
|
||||
Assert.Null(contentDisposition.ReadDate);
|
||||
Assert.Equal(0, contentDisposition.Parameters.Count);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Size_AddSizeParameterThenUseProperty_ParametersEntryIsOverwritten()
|
||||
{
|
||||
var contentDisposition = new ContentDispositionHeaderValue("inline");
|
||||
|
||||
// Note that uppercase letters are used. Comparison should happen case-insensitive.
|
||||
var sizeParameter = new NameValueHeaderValue("SIZE", "279172874239");
|
||||
contentDisposition.Parameters.Add(sizeParameter);
|
||||
Assert.Equal(1, contentDisposition.Parameters.Count);
|
||||
Assert.Equal("SIZE", contentDisposition.Parameters.First().Name);
|
||||
Assert.Equal(279172874239, contentDisposition.Size);
|
||||
|
||||
contentDisposition.Size = 279172874240;
|
||||
Assert.Equal(279172874240, contentDisposition.Size);
|
||||
Assert.Equal(1, contentDisposition.Parameters.Count);
|
||||
Assert.Equal("SIZE", contentDisposition.Parameters.First().Name);
|
||||
|
||||
contentDisposition.Parameters.Remove(sizeParameter);
|
||||
Assert.Null(contentDisposition.Size);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Size_InvalidSizes_PropertyFails()
|
||||
{
|
||||
var contentDisposition = new ContentDispositionHeaderValue("inline");
|
||||
|
||||
// Note that uppercase letters are used. Comparison should happen case-insensitive.
|
||||
var sizeParameter = new NameValueHeaderValue("SIZE", "-279172874239");
|
||||
contentDisposition.Parameters.Add(sizeParameter);
|
||||
Assert.Equal(1, contentDisposition.Parameters.Count);
|
||||
Assert.Equal("SIZE", contentDisposition.Parameters.First().Name);
|
||||
Assert.Null(contentDisposition.Size);
|
||||
|
||||
// Negatives not allowed
|
||||
Assert.Throws<ArgumentOutOfRangeException>(() => contentDisposition.Size = -279172874240);
|
||||
Assert.Null(contentDisposition.Size);
|
||||
Assert.Equal(1, contentDisposition.Parameters.Count);
|
||||
Assert.Equal("SIZE", contentDisposition.Parameters.First().Name);
|
||||
|
||||
contentDisposition.Parameters.Remove(sizeParameter);
|
||||
Assert.Null(contentDisposition.Size);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ToString_UseDifferentContentDispositions_AllSerializedCorrectly()
|
||||
{
|
||||
var contentDisposition = new ContentDispositionHeaderValue("inline");
|
||||
Assert.Equal("inline", contentDisposition.ToString());
|
||||
|
||||
contentDisposition.Name = "myname";
|
||||
Assert.Equal("inline; name=myname", contentDisposition.ToString());
|
||||
|
||||
contentDisposition.FileName = "my File Name";
|
||||
Assert.Equal("inline; name=myname; filename=\"my File Name\"", contentDisposition.ToString());
|
||||
|
||||
contentDisposition.CreationDate = new DateTimeOffset(new DateTime(2011, 2, 15));
|
||||
Assert.Equal("inline; name=myname; filename=\"my File Name\"; creation-date="
|
||||
+ "\"Tue, 15 Feb 2011 08:00:00 GMT\"", contentDisposition.ToString());
|
||||
|
||||
contentDisposition.Parameters.Add(new NameValueHeaderValue("custom", "\"custom value\""));
|
||||
Assert.Equal("inline; name=myname; filename=\"my File Name\"; creation-date="
|
||||
+ "\"Tue, 15 Feb 2011 08:00:00 GMT\"; custom=\"custom value\"", contentDisposition.ToString());
|
||||
|
||||
contentDisposition.Name = null;
|
||||
Assert.Equal("inline; filename=\"my File Name\"; creation-date="
|
||||
+ "\"Tue, 15 Feb 2011 08:00:00 GMT\"; custom=\"custom value\"", contentDisposition.ToString());
|
||||
|
||||
contentDisposition.FileNameStar = "File%Name";
|
||||
Assert.Equal("inline; filename=\"my File Name\"; creation-date="
|
||||
+ "\"Tue, 15 Feb 2011 08:00:00 GMT\"; custom=\"custom value\"; filename*=UTF-8\'\'File%25Name",
|
||||
contentDisposition.ToString());
|
||||
|
||||
contentDisposition.FileName = null;
|
||||
Assert.Equal("inline; creation-date=\"Tue, 15 Feb 2011 08:00:00 GMT\"; custom=\"custom value\";"
|
||||
+ " filename*=UTF-8\'\'File%25Name", contentDisposition.ToString());
|
||||
|
||||
contentDisposition.CreationDate = null;
|
||||
Assert.Equal("inline; custom=\"custom value\"; filename*=UTF-8\'\'File%25Name",
|
||||
contentDisposition.ToString());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetHashCode_UseContentDispositionWithAndWithoutParameters_SameOrDifferentHashCodes()
|
||||
{
|
||||
var contentDisposition1 = new ContentDispositionHeaderValue("inline");
|
||||
var contentDisposition2 = new ContentDispositionHeaderValue("inline");
|
||||
contentDisposition2.Name = "myname";
|
||||
var contentDisposition3 = new ContentDispositionHeaderValue("inline");
|
||||
contentDisposition3.Parameters.Add(new NameValueHeaderValue("name", "value"));
|
||||
var contentDisposition4 = new ContentDispositionHeaderValue("INLINE");
|
||||
var contentDisposition5 = new ContentDispositionHeaderValue("INLINE");
|
||||
contentDisposition5.Parameters.Add(new NameValueHeaderValue("NAME", "MYNAME"));
|
||||
|
||||
Assert.NotEqual(contentDisposition1.GetHashCode(), contentDisposition2.GetHashCode());
|
||||
Assert.NotEqual(contentDisposition1.GetHashCode(), contentDisposition3.GetHashCode());
|
||||
Assert.NotEqual(contentDisposition2.GetHashCode(), contentDisposition3.GetHashCode());
|
||||
Assert.Equal(contentDisposition1.GetHashCode(), contentDisposition4.GetHashCode());
|
||||
Assert.Equal(contentDisposition2.GetHashCode(), contentDisposition5.GetHashCode());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Equals_UseContentDispositionWithAndWithoutParameters_EqualOrNotEqualNoExceptions()
|
||||
{
|
||||
var contentDisposition1 = new ContentDispositionHeaderValue("inline");
|
||||
var contentDisposition2 = new ContentDispositionHeaderValue("inline");
|
||||
contentDisposition2.Name = "myName";
|
||||
var contentDisposition3 = new ContentDispositionHeaderValue("inline");
|
||||
contentDisposition3.Parameters.Add(new NameValueHeaderValue("name", "value"));
|
||||
var contentDisposition4 = new ContentDispositionHeaderValue("INLINE");
|
||||
var contentDisposition5 = new ContentDispositionHeaderValue("INLINE");
|
||||
contentDisposition5.Parameters.Add(new NameValueHeaderValue("NAME", "MYNAME"));
|
||||
var contentDisposition6 = new ContentDispositionHeaderValue("INLINE");
|
||||
contentDisposition6.Parameters.Add(new NameValueHeaderValue("NAME", "MYNAME"));
|
||||
contentDisposition6.Parameters.Add(new NameValueHeaderValue("custom", "value"));
|
||||
var contentDisposition7 = new ContentDispositionHeaderValue("attachment");
|
||||
|
||||
Assert.False(contentDisposition1.Equals(contentDisposition2), "No params vs. name.");
|
||||
Assert.False(contentDisposition2.Equals(contentDisposition1), "name vs. no params.");
|
||||
Assert.False(contentDisposition1.Equals(null), "No params vs. <null>.");
|
||||
Assert.False(contentDisposition1.Equals(contentDisposition3), "No params vs. custom param.");
|
||||
Assert.False(contentDisposition2.Equals(contentDisposition3), "name vs. custom param.");
|
||||
Assert.True(contentDisposition1.Equals(contentDisposition4), "Different casing.");
|
||||
Assert.True(contentDisposition2.Equals(contentDisposition5), "Different casing in name.");
|
||||
Assert.False(contentDisposition5.Equals(contentDisposition6), "name vs. custom param.");
|
||||
Assert.False(contentDisposition1.Equals(contentDisposition7), "inline vs. text/other.");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Parse_SetOfValidValueStrings_ParsedCorrectly()
|
||||
{
|
||||
var expected = new ContentDispositionHeaderValue("inline");
|
||||
CheckValidParse("\r\n inline ", expected);
|
||||
CheckValidParse("inline", expected);
|
||||
|
||||
// We don't have to test all possible input strings, since most of the pieces are handled by other parsers.
|
||||
// The purpose of this test is to verify that these other parsers are combined correctly to build a
|
||||
// Content-Disposition parser.
|
||||
expected.Name = "myName";
|
||||
CheckValidParse("\r\n inline ; name = myName ", expected);
|
||||
CheckValidParse(" inline;name=myName", expected);
|
||||
|
||||
expected.Name = null;
|
||||
expected.DispositionType = "attachment";
|
||||
expected.FileName = "foo-ae.html";
|
||||
expected.Parameters.Add(new NameValueHeaderValue("filename*", "UTF-8''foo-%c3%a4.html"));
|
||||
CheckValidParse(@"attachment; filename*=UTF-8''foo-%c3%a4.html; filename=foo-ae.html", expected);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Parse_SetOfInvalidValueStrings_Throws()
|
||||
{
|
||||
CheckInvalidParse("");
|
||||
CheckInvalidParse(" ");
|
||||
CheckInvalidParse(null);
|
||||
CheckInvalidParse("inline会");
|
||||
CheckInvalidParse("inline ,");
|
||||
CheckInvalidParse("inline,");
|
||||
CheckInvalidParse("inline; name=myName ,");
|
||||
CheckInvalidParse("inline; name=myName,");
|
||||
CheckInvalidParse("inline; name=my会Name");
|
||||
CheckInvalidParse("inline/");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TryParse_SetOfValidValueStrings_ParsedCorrectly()
|
||||
{
|
||||
var expected = new ContentDispositionHeaderValue("inline");
|
||||
CheckValidTryParse("\r\n inline ", expected);
|
||||
CheckValidTryParse("inline", expected);
|
||||
|
||||
// We don't have to test all possible input strings, since most of the pieces are handled by other parsers.
|
||||
// The purpose of this test is to verify that these other parsers are combined correctly to build a
|
||||
// Content-Disposition parser.
|
||||
expected.Name = "myName";
|
||||
CheckValidTryParse("\r\n inline ; name = myName ", expected);
|
||||
CheckValidTryParse(" inline;name=myName", expected);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TryParse_SetOfInvalidValueStrings_ReturnsFalse()
|
||||
{
|
||||
CheckInvalidTryParse("");
|
||||
CheckInvalidTryParse(" ");
|
||||
CheckInvalidTryParse(null);
|
||||
CheckInvalidTryParse("inline会");
|
||||
CheckInvalidTryParse("inline ,");
|
||||
CheckInvalidTryParse("inline,");
|
||||
CheckInvalidTryParse("inline; name=myName ,");
|
||||
CheckInvalidTryParse("inline; name=myName,");
|
||||
CheckInvalidTryParse("text/");
|
||||
}
|
||||
|
||||
public static TheoryData<string, ContentDispositionHeaderValue> ValidContentDispositionTestCases = new TheoryData<string, ContentDispositionHeaderValue>()
|
||||
{
|
||||
{ "inline", new ContentDispositionHeaderValue("inline") }, // @"This should be equivalent to not including the header at all."
|
||||
{ "inline;", new ContentDispositionHeaderValue("inline") },
|
||||
{ "inline;name=", new ContentDispositionHeaderValue("inline") { Parameters = { new NameValueHeaderValue("name", "") } } }, // TODO: passing in a null value causes a strange assert on CoreCLR before the test even starts. Not reproducable in the body of a test.
|
||||
{ "inline;name=value", new ContentDispositionHeaderValue("inline") { Name = "value" } },
|
||||
{ "inline;name=value;", new ContentDispositionHeaderValue("inline") { Name = "value" } },
|
||||
{ "inline;name=value;", new ContentDispositionHeaderValue("inline") { Name = "value" } },
|
||||
{ @"inline; filename=""foo.html""", new ContentDispositionHeaderValue("inline") { FileName = @"""foo.html""" } },
|
||||
{ @"inline; filename=""Not an attachment!""", new ContentDispositionHeaderValue("inline") { FileName = @"""Not an attachment!""" } }, // 'inline', specifying a filename of Not an attachment! - this checks for proper parsing for disposition types.
|
||||
{ @"inline; filename=""foo.pdf""", new ContentDispositionHeaderValue("inline") { FileName = @"""foo.pdf""" } },
|
||||
{ "attachment", new ContentDispositionHeaderValue("attachment") },
|
||||
{ "ATTACHMENT", new ContentDispositionHeaderValue("ATTACHMENT") },
|
||||
{ @"attachment; filename=""foo.html""", new ContentDispositionHeaderValue("attachment") { FileName = @"""foo.html""" } },
|
||||
{ @"attachment; filename=""\""quoting\"" tested.html""", new ContentDispositionHeaderValue("attachment") { FileName = "\"\"quoting\" tested.html\"" } }, // 'attachment', specifying a filename of \"quoting\" tested.html (using double quotes around "quoting" to test... quoting)
|
||||
{ @"attachment; filename=""Here's a semicolon;.html""", new ContentDispositionHeaderValue("attachment") { FileName = @"""Here's a semicolon;.html""" } }, // , 'attachment', specifying a filename of Here's a semicolon;.html - this checks for proper parsing for parameters.
|
||||
{ @"attachment; foo=""bar""; filename=""foo.html""", new ContentDispositionHeaderValue(@"attachment") { FileName = @"""foo.html""", Parameters = { new NameValueHeaderValue("foo", @"""bar""") } } }, // 'attachment', specifying a filename of foo.html and an extension parameter "foo" which should be ignored (see <a href="http://greenbytes.de/tech/webdav/rfc2183.html#rfc.section.2.8">Section 2.8 of RFC 2183</a>.).
|
||||
{ @"attachment; foo=""\""\\"";filename=""foo.html""", new ContentDispositionHeaderValue(@"attachment") { FileName = @"""foo.html""", Parameters = { new NameValueHeaderValue("foo", @"""\""\\""") } } }, // 'attachment', specifying a filename of foo.html and an extension parameter "foo" which should be ignored (see <a href="http://greenbytes.de/tech/webdav/rfc2183.html#rfc.section.2.8">Section 2.8 of RFC 2183</a>.). The extension parameter actually uses backslash-escapes. This tests whether the UA properly skips the parameter.
|
||||
{ @"attachment; FILENAME=""foo.html""", new ContentDispositionHeaderValue("attachment") { FileName = @"""foo.html""" } },
|
||||
{ @"attachment; filename=foo.html", new ContentDispositionHeaderValue("attachment") { FileName = "foo.html" } }, // 'attachment', specifying a filename of foo.html using a token instead of a quoted-string.
|
||||
{ @"attachment; filename='foo.bar'", new ContentDispositionHeaderValue("attachment") { FileName = "'foo.bar'" } }, // 'attachment', specifying a filename of 'foo.bar' using single quotes.
|
||||
{ @"attachment; filename=""foo-ä.html""", new ContentDispositionHeaderValue("attachment" ) { Parameters = { new NameValueHeaderValue("filename", @"""foo-ä.html""") } } }, // 'attachment', specifying a filename of foo-ä.html, using plain ISO-8859-1
|
||||
{ @"attachment; filename=""foo-ä.html""", new ContentDispositionHeaderValue("attachment") { FileName = @"""foo-ä.html""" } }, // 'attachment', specifying a filename of foo-ä.html, which happens to be foo-ä.html using UTF-8 encoding.
|
||||
{ @"attachment; filename=""foo-%41.html""", new ContentDispositionHeaderValue("attachment") { Parameters = { new NameValueHeaderValue("filename", @"""foo-%41.html""") } } },
|
||||
{ @"attachment; filename=""50%.html""", new ContentDispositionHeaderValue("attachment") { Parameters = { new NameValueHeaderValue("filename", @"""50%.html""") } } },
|
||||
{ @"attachment; filename=""foo-%\41.html""", new ContentDispositionHeaderValue("attachment") { Parameters = { new NameValueHeaderValue("filename", @"""foo-%\41.html""") } } }, // 'attachment', specifying a filename of foo-%41.html, using an escape character (this tests whether adding an escape character inside a %xx sequence can be used to disable the non-conformant %xx-unescaping).
|
||||
{ @"attachment; name=""foo-%41.html""", new ContentDispositionHeaderValue("attachment") { Name = @"""foo-%41.html""" } }, // 'attachment', specifying a <i>name</i> parameter of foo-%41.html. (this test was added to observe the behavior of the (unspecified) treatment of ""name"" as synonym for ""filename""; see <a href=""http://www.imc.org/ietf-smtp/mail-archive/msg05023.html"">Ned Freed's summary</a> where this comes from in MIME messages)
|
||||
{ @"attachment; filename=""ä-%41.html""", new ContentDispositionHeaderValue("attachment") { Parameters = { new NameValueHeaderValue("filename", @"""ä-%41.html""") } } }, // 'attachment', specifying a filename parameter of ä-%41.html. (this test was added to observe the behavior when non-ASCII characters and percent-hexdig sequences are combined)
|
||||
{ @"attachment; filename=""foo-%c3%a4-%e2%82%ac.html""", new ContentDispositionHeaderValue("attachment") { FileName = @"""foo-%c3%a4-%e2%82%ac.html""" } }, // 'attachment', specifying a filename of foo-%c3%a4-%e2%82%ac.html, using raw percent encoded UTF-8 to represent foo-ä-€.html
|
||||
{ @"attachment; filename =""foo.html""", new ContentDispositionHeaderValue("attachment") { FileName = @"""foo.html""" } },
|
||||
{ @"attachment; xfilename=foo.html", new ContentDispositionHeaderValue("attachment" ) { Parameters = { new NameValueHeaderValue("xfilename", "foo.html") } } },
|
||||
{ @"attachment; filename=""/foo.html""", new ContentDispositionHeaderValue("attachment") { FileName = @"""/foo.html""" } },
|
||||
{ @"attachment; creation-date=""Wed, 12 Feb 1997 16:29:51 -0500""", new ContentDispositionHeaderValue("attachment") { Parameters = { new NameValueHeaderValue("creation-date", @"""Wed, 12 Feb 1997 16:29:51 -0500""") } } },
|
||||
{ @"attachment; modification-date=""Wed, 12 Feb 1997 16:29:51 -0500""", new ContentDispositionHeaderValue("attachment") { Parameters = { new NameValueHeaderValue("modification-date", @"""Wed, 12 Feb 1997 16:29:51 -0500""") } } },
|
||||
{ @"foobar", new ContentDispositionHeaderValue("foobar") }, // @"This should be equivalent to using ""attachment""."
|
||||
{ @"attachment; example=""filename=example.txt""", new ContentDispositionHeaderValue("attachment") { Parameters = { new NameValueHeaderValue("example", @"""filename=example.txt""") } } },
|
||||
{ @"attachment; filename*=iso-8859-1''foo-%E4.html", new ContentDispositionHeaderValue("attachment") { Parameters = { new NameValueHeaderValue("filename*", "iso-8859-1''foo-%E4.html") } } }, // 'attachment', specifying a filename of foo-ä.html, using RFC2231 encoded ISO-8859-1
|
||||
{ @"attachment; filename*=UTF-8''foo-%c3%a4-%e2%82%ac.html", new ContentDispositionHeaderValue("attachment") { Parameters = { new NameValueHeaderValue("filename*", "UTF-8''foo-%c3%a4-%e2%82%ac.html") } } }, // 'attachment', specifying a filename of foo-ä-€.html, using RFC2231 encoded UTF-8
|
||||
{ @"attachment; filename*=''foo-%c3%a4-%e2%82%ac.html", new ContentDispositionHeaderValue("attachment") { Parameters = { new NameValueHeaderValue("filename*", "''foo-%c3%a4-%e2%82%ac.html") } } }, // Behavior is undefined in RFC 2231, the charset part is missing, although UTF-8 was used.
|
||||
{ @"attachment; filename*=UTF-8''foo-a%22.html", new ContentDispositionHeaderValue("attachment") { FileNameStar = @"foo-a"".html" } },
|
||||
{ @"attachment; filename*= UTF-8''foo-%c3%a4.html", new ContentDispositionHeaderValue("attachment") { FileNameStar = "foo-ä.html" } },
|
||||
{ @"attachment; filename* =UTF-8''foo-%c3%a4.html", new ContentDispositionHeaderValue("attachment") { FileNameStar = "foo-ä.html" } },
|
||||
{ @"attachment; filename*=UTF-8''A-%2541.html", new ContentDispositionHeaderValue("attachment") { FileNameStar = "A-%41.html" } },
|
||||
{ @"attachment; filename*=UTF-8''%5cfoo.html", new ContentDispositionHeaderValue("attachment") { FileNameStar = @"\foo.html" } },
|
||||
{ @"attachment; filename=""foo-ae.html""; filename*=UTF-8''foo-%c3%a4.html", new ContentDispositionHeaderValue("attachment") { FileName = @"""foo-ae.html""", FileNameStar = "foo-ä.html" } },
|
||||
{ @"attachment; filename*=UTF-8''foo-%c3%a4.html; filename=""foo-ae.html""", new ContentDispositionHeaderValue("attachment") { FileNameStar = "foo-ä.html", FileName = @"""foo-ae.html""" } },
|
||||
{ @"attachment; foobar=x; filename=""foo.html""", new ContentDispositionHeaderValue("attachment") { FileName = @"""foo.html""", Parameters = { new NameValueHeaderValue("foobar", "x") } } },
|
||||
{ @"attachment; filename=""=?ISO-8859-1?Q?foo-=E4.html?=""", new ContentDispositionHeaderValue("attachment") { FileName = @"""=?ISO-8859-1?Q?foo-=E4.html?=""" } }, // attachment; filename="=?ISO-8859-1?Q?foo-=E4.html?="
|
||||
{ @"attachment; filename=""=?utf-8?B?Zm9vLeQuaHRtbA==?=""", new ContentDispositionHeaderValue("attachment") { FileName = @"""=?utf-8?B?Zm9vLeQuaHRtbA==?=""" } }, // attachment; filename="=?utf-8?B?Zm9vLeQuaHRtbA==?="
|
||||
{ @"attachment; filename=foo.html ;", new ContentDispositionHeaderValue("attachment") { FileName="foo.html" } }, // 'attachment', specifying a filename of foo.html using a token instead of a quoted-string, and adding a trailing semicolon.,
|
||||
};
|
||||
|
||||
[Theory]
|
||||
[MemberData(nameof(ValidContentDispositionTestCases))]
|
||||
public void ContentDispositionHeaderValue_ParseValid_Success(string input, ContentDispositionHeaderValue expected)
|
||||
{
|
||||
// System.Diagnostics.Debugger.Launch();
|
||||
var result = ContentDispositionHeaderValue.Parse(input);
|
||||
Assert.Equal(expected, result);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
// Invalid values
|
||||
[InlineData(@"""inline""")] // @"'inline' only, using double quotes", false) },
|
||||
[InlineData(@"""attachment""")] // @"'attachment' only, using double quotes", false) },
|
||||
[InlineData(@"attachment; filename=foo bar.html")] // @"'attachment', specifying a filename of foo bar.html without using quoting.", false) },
|
||||
// Duplicate file name parameter
|
||||
// @"attachment; filename=""foo.html""; // filename=""bar.html""", @"'attachment', specifying two filename parameters. This is invalid syntax.", false) },
|
||||
[InlineData(@"attachment; filename=foo[1](2).html")] // @"'attachment', specifying a filename of foo[1](2).html, but missing the quotes. Also, ""["", ""]"", ""("" and "")"" are not allowed in the HTTP <a href=""http://greenbytes.de/tech/webdav/draft-ietf-httpbis-p1-messaging-latest.html#rfc.section.1.2.2"">token</a> production.", false) },
|
||||
[InlineData(@"attachment; filename=foo-ä.html")] // @"'attachment', specifying a filename of foo-ä.html, but missing the quotes.", false) },
|
||||
// HTML escaping, not supported
|
||||
// @"attachment; filename=foo-ä.html", // "'attachment', specifying a filename of foo-ä.html (which happens to be foo-ä.html using UTF-8 encoding) but missing the quotes.", false) },
|
||||
[InlineData(@"filename=foo.html")] // @"Disposition type missing, filename specified.", false) },
|
||||
[InlineData(@"x=y; filename=foo.html")] // @"Disposition type missing, filename specified after extension parameter.", false) },
|
||||
[InlineData(@"""foo; filename=bar;baz""; filename=qux")] // @"Disposition type missing, filename ""qux"". Can it be more broken? (Probably)", false) },
|
||||
[InlineData(@"filename=foo.html, filename=bar.html")] // @"Disposition type missing, two filenames specified separated by a comma (this is syntactically equivalent to have two instances of the header with one filename parameter each).", false) },
|
||||
[InlineData(@"; filename=foo.html")] // @"Disposition type missing (but delimiter present), filename specified.", false) },
|
||||
// This is permitted as a parameter without a value
|
||||
// @"inline; attachment; filename=foo.html", // @"Both disposition types specified.", false) },
|
||||
// This is permitted as a parameter without a value
|
||||
// @"inline; attachment; filename=foo.html", // @"Both disposition types specified.", false) },
|
||||
[InlineData(@"attachment; filename=""foo.html"".txt")] // @"'attachment', specifying a filename parameter that is broken (quoted-string followed by more characters). This is invalid syntax. ", false) },
|
||||
[InlineData(@"attachment; filename=""bar")] // @"'attachment', specifying a filename parameter that is broken (missing ending double quote). This is invalid syntax.", false) },
|
||||
[InlineData(@"attachment; filename=foo""bar;baz""qux")] // @"'attachment', specifying a filename parameter that is broken (disallowed characters in token syntax). This is invalid syntax.", false) },
|
||||
[InlineData(@"attachment; filename=foo.html, attachment; filename=bar.html")] // @"'attachment', two comma-separated instances of the header field. As Content-Disposition doesn't use a list-style syntax, this is invalid syntax and, according to <a href=""http://greenbytes.de/tech/webdav/rfc2616.html#rfc.section.4.2.p.5"">RFC 2616, Section 4.2</a>, roughly equivalent to having two separate header field instances.", false) },
|
||||
[InlineData(@"filename=foo.html; attachment")] // @"filename parameter and disposition type reversed.", false) },
|
||||
// Escaping is not verified
|
||||
// @"attachment; filename*=iso-8859-1''foo-%c3%a4-%e2%82%ac.html", // @"'attachment', specifying a filename of foo-ä-€.html, using RFC2231 encoded UTF-8, but declaring ISO-8859-1", false) },
|
||||
// Escaping is not verified
|
||||
// @"attachment; filename *=UTF-8''foo-%c3%a4.html", // @"'attachment', specifying a filename of foo-ä.html, using RFC2231 encoded UTF-8, with whitespace before ""*=""", false) },
|
||||
// Escaping is not verified
|
||||
// @"attachment; filename*=""UTF-8''foo-%c3%a4.html""", // @"'attachment', specifying a filename of foo-ä.html, using RFC2231 encoded UTF-8, with double quotes around the parameter value.", false) },
|
||||
[InlineData(@"attachment; filename==?ISO-8859-1?Q?foo-=E4.html?=")] // @"Uses RFC 2047 style encoded word. ""="" is invalid inside the token production, so this is invalid.", false) },
|
||||
[InlineData(@"attachment; filename==?utf-8?B?Zm9vLeQuaHRtbA==?=")] // @"Uses RFC 2047 style encoded word. ""="" is invalid inside the token production, so this is invalid.", false) },
|
||||
public void ContentDispositionHeaderValue_ParseInvalid_Throws(string input)
|
||||
{
|
||||
Assert.Throws<FormatException>(() => ContentDispositionHeaderValue.Parse(input));
|
||||
}
|
||||
|
||||
public class ContentDispositionValue
|
||||
{
|
||||
public ContentDispositionValue(string value, string description, bool valid)
|
||||
{
|
||||
Value = value;
|
||||
Description = description;
|
||||
Valid = valid;
|
||||
}
|
||||
|
||||
public string Value { get; private set; }
|
||||
|
||||
public string Description { get; private set; }
|
||||
|
||||
public bool Valid { get; private set; }
|
||||
}
|
||||
|
||||
private void CheckValidParse(string input, ContentDispositionHeaderValue expectedResult)
|
||||
{
|
||||
var result = ContentDispositionHeaderValue.Parse(input);
|
||||
Assert.Equal(expectedResult, result);
|
||||
}
|
||||
|
||||
private void CheckInvalidParse(string input)
|
||||
{
|
||||
Assert.Throws<FormatException>(() => ContentDispositionHeaderValue.Parse(input));
|
||||
}
|
||||
|
||||
private void CheckValidTryParse(string input, ContentDispositionHeaderValue expectedResult)
|
||||
{
|
||||
ContentDispositionHeaderValue result = null;
|
||||
Assert.True(ContentDispositionHeaderValue.TryParse(input, out result), input);
|
||||
Assert.Equal(expectedResult, result);
|
||||
}
|
||||
|
||||
private void CheckInvalidTryParse(string input)
|
||||
{
|
||||
ContentDispositionHeaderValue result = null;
|
||||
Assert.False(ContentDispositionHeaderValue.TryParse(input, out result), input);
|
||||
Assert.Null(result);
|
||||
}
|
||||
|
||||
private static void AssertFormatException(string contentDisposition)
|
||||
{
|
||||
Assert.Throws<FormatException>(() => new ContentDispositionHeaderValue(contentDisposition));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,272 @@
|
|||
// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved.
|
||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
using System;
|
||||
using Xunit;
|
||||
|
||||
namespace Microsoft.Net.Http.Headers
|
||||
{
|
||||
public class ContentRangeHeaderValueTest
|
||||
{
|
||||
[Fact]
|
||||
public void Ctor_LengthOnlyOverloadUseInvalidValues_Throw()
|
||||
{
|
||||
Assert.Throws<ArgumentOutOfRangeException>(() => new ContentRangeHeaderValue(-1));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Ctor_LengthOnlyOverloadValidValues_ValuesCorrectlySet()
|
||||
{
|
||||
var range = new ContentRangeHeaderValue(5);
|
||||
|
||||
Assert.False(range.HasRange, "HasRange");
|
||||
Assert.True(range.HasLength, "HasLength");
|
||||
Assert.Equal("bytes", range.Unit);
|
||||
Assert.Null(range.From);
|
||||
Assert.Null(range.To);
|
||||
Assert.Equal(5, range.Length);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Ctor_FromAndToOverloadUseInvalidValues_Throw()
|
||||
{
|
||||
Assert.Throws<ArgumentOutOfRangeException>(() => new ContentRangeHeaderValue(-1, 1));
|
||||
Assert.Throws<ArgumentOutOfRangeException>(() => new ContentRangeHeaderValue(0, -1));
|
||||
Assert.Throws<ArgumentOutOfRangeException>(() => new ContentRangeHeaderValue(2, 1));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Ctor_FromAndToOverloadValidValues_ValuesCorrectlySet()
|
||||
{
|
||||
var range = new ContentRangeHeaderValue(0, 1);
|
||||
|
||||
Assert.True(range.HasRange, "HasRange");
|
||||
Assert.False(range.HasLength, "HasLength");
|
||||
Assert.Equal("bytes", range.Unit);
|
||||
Assert.Equal(0, range.From);
|
||||
Assert.Equal(1, range.To);
|
||||
Assert.Null(range.Length);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Ctor_FromToAndLengthOverloadUseInvalidValues_Throw()
|
||||
{
|
||||
Assert.Throws<ArgumentOutOfRangeException>(() => new ContentRangeHeaderValue(-1, 1, 2));
|
||||
Assert.Throws<ArgumentOutOfRangeException>(() => new ContentRangeHeaderValue(0, -1, 2));
|
||||
Assert.Throws<ArgumentOutOfRangeException>(() => new ContentRangeHeaderValue(0, 1, -1));
|
||||
Assert.Throws<ArgumentOutOfRangeException>(() => new ContentRangeHeaderValue(2, 1, 3));
|
||||
Assert.Throws<ArgumentOutOfRangeException>(() => new ContentRangeHeaderValue(1, 2, 1));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Ctor_FromToAndLengthOverloadValidValues_ValuesCorrectlySet()
|
||||
{
|
||||
var range = new ContentRangeHeaderValue(0, 1, 2);
|
||||
|
||||
Assert.True(range.HasRange, "HasRange");
|
||||
Assert.True(range.HasLength, "HasLength");
|
||||
Assert.Equal("bytes", range.Unit);
|
||||
Assert.Equal(0, range.From);
|
||||
Assert.Equal(1, range.To);
|
||||
Assert.Equal(2, range.Length);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Unit_GetAndSetValidAndInvalidValues_MatchExpectation()
|
||||
{
|
||||
var range = new ContentRangeHeaderValue(0);
|
||||
range.Unit = "myunit";
|
||||
Assert.Equal("myunit", range.Unit);
|
||||
|
||||
Assert.Throws<ArgumentException>(() => range.Unit = null);
|
||||
Assert.Throws<ArgumentException>(() => range.Unit = "");
|
||||
Assert.Throws<FormatException>(() => range.Unit = " x");
|
||||
Assert.Throws<FormatException>(() => range.Unit = "x ");
|
||||
Assert.Throws<FormatException>(() => range.Unit = "x y");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ToString_UseDifferentRanges_AllSerializedCorrectly()
|
||||
{
|
||||
var range = new ContentRangeHeaderValue(1, 2, 3);
|
||||
range.Unit = "myunit";
|
||||
Assert.Equal("myunit 1-2/3", range.ToString());
|
||||
|
||||
range = new ContentRangeHeaderValue(123456789012345678, 123456789012345679);
|
||||
Assert.Equal("bytes 123456789012345678-123456789012345679/*", range.ToString());
|
||||
|
||||
range = new ContentRangeHeaderValue(150);
|
||||
Assert.Equal("bytes */150", range.ToString());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetHashCode_UseSameAndDifferentRanges_SameOrDifferentHashCodes()
|
||||
{
|
||||
var range1 = new ContentRangeHeaderValue(1, 2, 5);
|
||||
var range2 = new ContentRangeHeaderValue(1, 2);
|
||||
var range3 = new ContentRangeHeaderValue(5);
|
||||
var range4 = new ContentRangeHeaderValue(1, 2, 5);
|
||||
range4.Unit = "BYTES";
|
||||
var range5 = new ContentRangeHeaderValue(1, 2, 5);
|
||||
range5.Unit = "myunit";
|
||||
|
||||
Assert.NotEqual(range1.GetHashCode(), range2.GetHashCode());
|
||||
Assert.NotEqual(range1.GetHashCode(), range3.GetHashCode());
|
||||
Assert.NotEqual(range2.GetHashCode(), range3.GetHashCode());
|
||||
Assert.Equal(range1.GetHashCode(), range4.GetHashCode());
|
||||
Assert.NotEqual(range1.GetHashCode(), range5.GetHashCode());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Equals_UseSameAndDifferentRanges_EqualOrNotEqualNoExceptions()
|
||||
{
|
||||
var range1 = new ContentRangeHeaderValue(1, 2, 5);
|
||||
var range2 = new ContentRangeHeaderValue(1, 2);
|
||||
var range3 = new ContentRangeHeaderValue(5);
|
||||
var range4 = new ContentRangeHeaderValue(1, 2, 5);
|
||||
range4.Unit = "BYTES";
|
||||
var range5 = new ContentRangeHeaderValue(1, 2, 5);
|
||||
range5.Unit = "myunit";
|
||||
var range6 = new ContentRangeHeaderValue(1, 3, 5);
|
||||
var range7 = new ContentRangeHeaderValue(2, 2, 5);
|
||||
var range8 = new ContentRangeHeaderValue(1, 2, 6);
|
||||
|
||||
Assert.False(range1.Equals(null), "bytes 1-2/5 vs. <null>");
|
||||
Assert.False(range1.Equals(range2), "bytes 1-2/5 vs. bytes 1-2/*");
|
||||
Assert.False(range1.Equals(range3), "bytes 1-2/5 vs. bytes */5");
|
||||
Assert.False(range2.Equals(range3), "bytes 1-2/* vs. bytes */5");
|
||||
Assert.True(range1.Equals(range4), "bytes 1-2/5 vs. BYTES 1-2/5");
|
||||
Assert.True(range4.Equals(range1), "BYTES 1-2/5 vs. bytes 1-2/5");
|
||||
Assert.False(range1.Equals(range5), "bytes 1-2/5 vs. myunit 1-2/5");
|
||||
Assert.False(range1.Equals(range6), "bytes 1-2/5 vs. bytes 1-3/5");
|
||||
Assert.False(range1.Equals(range7), "bytes 1-2/5 vs. bytes 2-2/5");
|
||||
Assert.False(range1.Equals(range8), "bytes 1-2/5 vs. bytes 1-2/6");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Parse_SetOfValidValueStrings_ParsedCorrectly()
|
||||
{
|
||||
CheckValidParse(" bytes 1-2/3 ", new ContentRangeHeaderValue(1, 2, 3));
|
||||
CheckValidParse("bytes * / 3", new ContentRangeHeaderValue(3));
|
||||
|
||||
CheckValidParse(" custom 1234567890123456789-1234567890123456799/*",
|
||||
new ContentRangeHeaderValue(1234567890123456789, 1234567890123456799) { Unit = "custom" });
|
||||
|
||||
CheckValidParse(" custom * / 123 ",
|
||||
new ContentRangeHeaderValue(123) { Unit = "custom" });
|
||||
|
||||
// Note that we don't have a public constructor for value 'bytes */*' since the RFC doesn't mention a
|
||||
// scenario for it. However, if a server returns this value, we're flexible and accept it.
|
||||
var result = ContentRangeHeaderValue.Parse("bytes */*");
|
||||
Assert.Equal("bytes", result.Unit);
|
||||
Assert.Null(result.From);
|
||||
Assert.Null(result.To);
|
||||
Assert.Null(result.Length);
|
||||
Assert.False(result.HasRange, "HasRange");
|
||||
Assert.False(result.HasLength, "HasLength");
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("bytes 1-2/3,")] // no character after 'length' allowed
|
||||
[InlineData("x bytes 1-2/3")]
|
||||
[InlineData("bytes 1-2/3.4")]
|
||||
[InlineData(null)]
|
||||
[InlineData("")]
|
||||
[InlineData("bytes 3-2/5")]
|
||||
[InlineData("bytes 6-6/5")]
|
||||
[InlineData("bytes 1-6/5")]
|
||||
[InlineData("bytes 1-2/")]
|
||||
[InlineData("bytes 1-2")]
|
||||
[InlineData("bytes 1-/")]
|
||||
[InlineData("bytes 1-")]
|
||||
[InlineData("bytes 1")]
|
||||
[InlineData("bytes ")]
|
||||
[InlineData("bytes a-2/3")]
|
||||
[InlineData("bytes 1-b/3")]
|
||||
[InlineData("bytes 1-2/c")]
|
||||
[InlineData("bytes1-2/3")]
|
||||
// More than 19 digits >>Int64.MaxValue
|
||||
[InlineData("bytes 1-12345678901234567890/3")]
|
||||
[InlineData("bytes 12345678901234567890-3/3")]
|
||||
[InlineData("bytes 1-2/12345678901234567890")]
|
||||
// Exceed Int64.MaxValue, but use 19 digits
|
||||
[InlineData("bytes 1-9999999999999999999/3")]
|
||||
[InlineData("bytes 9999999999999999999-3/3")]
|
||||
[InlineData("bytes 1-2/9999999999999999999")]
|
||||
public void Parse_SetOfInvalidValueStrings_Throws(string input)
|
||||
{
|
||||
Assert.Throws<FormatException>(() => ContentRangeHeaderValue.Parse(input));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TryParse_SetOfValidValueStrings_ParsedCorrectly()
|
||||
{
|
||||
CheckValidTryParse(" bytes 1-2/3 ", new ContentRangeHeaderValue(1, 2, 3));
|
||||
CheckValidTryParse("bytes * / 3", new ContentRangeHeaderValue(3));
|
||||
|
||||
CheckValidTryParse(" custom 1234567890123456789-1234567890123456799/*",
|
||||
new ContentRangeHeaderValue(1234567890123456789, 1234567890123456799) { Unit = "custom" });
|
||||
|
||||
CheckValidTryParse(" custom * / 123 ",
|
||||
new ContentRangeHeaderValue(123) { Unit = "custom" });
|
||||
|
||||
// Note that we don't have a public constructor for value 'bytes */*' since the RFC doesn't mention a
|
||||
// scenario for it. However, if a server returns this value, we're flexible and accept it.
|
||||
ContentRangeHeaderValue result = null;
|
||||
Assert.True(ContentRangeHeaderValue.TryParse("bytes */*", out result));
|
||||
Assert.Equal("bytes", result.Unit);
|
||||
Assert.Null(result.From);
|
||||
Assert.Null(result.To);
|
||||
Assert.Null(result.Length);
|
||||
Assert.False(result.HasRange, "HasRange");
|
||||
Assert.False(result.HasLength, "HasLength");
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("bytes 1-2/3,")] // no character after 'length' allowed
|
||||
[InlineData("x bytes 1-2/3")]
|
||||
[InlineData("bytes 1-2/3.4")]
|
||||
[InlineData(null)]
|
||||
[InlineData("")]
|
||||
[InlineData("bytes 3-2/5")]
|
||||
[InlineData("bytes 6-6/5")]
|
||||
[InlineData("bytes 1-6/5")]
|
||||
[InlineData("bytes 1-2/")]
|
||||
[InlineData("bytes 1-2")]
|
||||
[InlineData("bytes 1-/")]
|
||||
[InlineData("bytes 1-")]
|
||||
[InlineData("bytes 1")]
|
||||
[InlineData("bytes ")]
|
||||
[InlineData("bytes a-2/3")]
|
||||
[InlineData("bytes 1-b/3")]
|
||||
[InlineData("bytes 1-2/c")]
|
||||
[InlineData("bytes1-2/3")]
|
||||
// More than 19 digits >>Int64.MaxValue
|
||||
[InlineData("bytes 1-12345678901234567890/3")]
|
||||
[InlineData("bytes 12345678901234567890-3/3")]
|
||||
[InlineData("bytes 1-2/12345678901234567890")]
|
||||
// Exceed Int64.MaxValue, but use 19 digits
|
||||
[InlineData("bytes 1-9999999999999999999/3")]
|
||||
[InlineData("bytes 9999999999999999999-3/3")]
|
||||
[InlineData("bytes 1-2/9999999999999999999")]
|
||||
public void TryParse_SetOfInvalidValueStrings_ReturnsFalse(string input)
|
||||
{
|
||||
ContentRangeHeaderValue result = null;
|
||||
Assert.False(ContentRangeHeaderValue.TryParse(input, out result));
|
||||
Assert.Null(result);
|
||||
}
|
||||
|
||||
private void CheckValidParse(string input, ContentRangeHeaderValue expectedResult)
|
||||
{
|
||||
var result = ContentRangeHeaderValue.Parse(input);
|
||||
Assert.Equal(expectedResult, result);
|
||||
}
|
||||
|
||||
private void CheckValidTryParse(string input, ContentRangeHeaderValue expectedResult)
|
||||
{
|
||||
ContentRangeHeaderValue result = null;
|
||||
Assert.True(ContentRangeHeaderValue.TryParse(input, out result));
|
||||
Assert.Equal(expectedResult, result);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,229 @@
|
|||
// Copyright (c) Microsoft Open Technologies, Inc. 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.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Xunit;
|
||||
|
||||
namespace Microsoft.Net.Http.Headers
|
||||
{
|
||||
public class CookieHeaderValueTest
|
||||
{
|
||||
public static TheoryData<CookieHeaderValue, string> CookieHeaderDataSet
|
||||
{
|
||||
get
|
||||
{
|
||||
var dataset = new TheoryData<CookieHeaderValue, string>();
|
||||
var header1 = new CookieHeaderValue("name1", "n1=v1&n2=v2&n3=v3");
|
||||
dataset.Add(header1, "name1=n1=v1&n2=v2&n3=v3");
|
||||
|
||||
var header2 = new CookieHeaderValue("name2", "");
|
||||
dataset.Add(header2, "name2=");
|
||||
|
||||
var header3 = new CookieHeaderValue("name3", "value3");
|
||||
dataset.Add(header3, "name3=value3");
|
||||
|
||||
var header4 = new CookieHeaderValue("name4", "\"value4\"");
|
||||
dataset.Add(header4, "name4=\"value4\"");
|
||||
|
||||
return dataset;
|
||||
}
|
||||
}
|
||||
|
||||
public static TheoryData<string> InvalidCookieHeaderDataSet
|
||||
{
|
||||
get
|
||||
{
|
||||
return new TheoryData<string>
|
||||
{
|
||||
"=value",
|
||||
"name=value;",
|
||||
"name=value,",
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
public static TheoryData<string> InvalidCookieNames
|
||||
{
|
||||
get
|
||||
{
|
||||
return new TheoryData<string>
|
||||
{
|
||||
"<acb>",
|
||||
"{acb}",
|
||||
"[acb]",
|
||||
"\"acb\"",
|
||||
"a,b",
|
||||
"a;b",
|
||||
"a\\b",
|
||||
"a b",
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
public static TheoryData<string> InvalidCookieValues
|
||||
{
|
||||
get
|
||||
{
|
||||
return new TheoryData<string>
|
||||
{
|
||||
{ "\"" },
|
||||
{ "a,b" },
|
||||
{ "a;b" },
|
||||
{ "a\\b" },
|
||||
{ "\"abc" },
|
||||
{ "a\"bc" },
|
||||
{ "abc\"" },
|
||||
{ "a b" },
|
||||
};
|
||||
}
|
||||
}
|
||||
public static TheoryData<IList<CookieHeaderValue>, string[]> ListOfCookieHeaderDataSet
|
||||
{
|
||||
get
|
||||
{
|
||||
var dataset = new TheoryData<IList<CookieHeaderValue>, string[]>();
|
||||
var header1 = new CookieHeaderValue("name1", "n1=v1&n2=v2&n3=v3");
|
||||
var string1 = "name1=n1=v1&n2=v2&n3=v3";
|
||||
|
||||
var header2 = new CookieHeaderValue("name2", "value2");
|
||||
var string2 = "name2=value2";
|
||||
|
||||
var header3 = new CookieHeaderValue("name3", "value3");
|
||||
var string3 = "name3=value3";
|
||||
|
||||
var header4 = new CookieHeaderValue("name4", "\"value4\"");
|
||||
var string4 = "name4=\"value4\"";
|
||||
|
||||
dataset.Add(new[] { header1 }.ToList(), new[] { string1 });
|
||||
dataset.Add(new[] { header1, header1 }.ToList(), new[] { string1, string1 });
|
||||
dataset.Add(new[] { header1, header1 }.ToList(), new[] { string1, null, "", " ", ";", " , ", string1 });
|
||||
dataset.Add(new[] { header2 }.ToList(), new[] { string2 });
|
||||
dataset.Add(new[] { header1, header2 }.ToList(), new[] { string1, string2 });
|
||||
dataset.Add(new[] { header1, header2 }.ToList(), new[] { string1 + ", " + string2 });
|
||||
dataset.Add(new[] { header2, header1 }.ToList(), new[] { string2 + "; " + string1 });
|
||||
dataset.Add(new[] { header1, header2, header3, header4 }.ToList(), new[] { string1, string2, string3, string4 });
|
||||
dataset.Add(new[] { header1, header2, header3, header4 }.ToList(), new[] { string.Join(",", string1, string2, string3, string4) });
|
||||
dataset.Add(new[] { header1, header2, header3, header4 }.ToList(), new[] { string.Join(";", string1, string2, string3, string4) });
|
||||
|
||||
return dataset;
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: [Fact]
|
||||
public void CookieHeaderValue_CtorThrowsOnNullName()
|
||||
{
|
||||
Assert.Throws<ArgumentNullException>(() => new CookieHeaderValue(null, "value"));
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[MemberData(nameof(InvalidCookieNames))]
|
||||
public void CookieHeaderValue_CtorThrowsOnInvalidName(string name)
|
||||
{
|
||||
Assert.Throws<ArgumentException>(() => new CookieHeaderValue(name, "value"));
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[MemberData(nameof(InvalidCookieValues))]
|
||||
public void CookieHeaderValue_CtorThrowsOnInvalidValue(string value)
|
||||
{
|
||||
Assert.Throws<ArgumentException>(() => new CookieHeaderValue("name", value));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CookieHeaderValue_Ctor1_InitializesCorrectly()
|
||||
{
|
||||
var header = new CookieHeaderValue("cookie");
|
||||
Assert.Equal("cookie", header.Name);
|
||||
Assert.Equal(string.Empty, header.Value);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("name", "")]
|
||||
[InlineData("name", "value")]
|
||||
[InlineData("name", "\"acb\"")]
|
||||
public void CookieHeaderValue_Ctor2InitializesCorrectly(string name, string value)
|
||||
{
|
||||
var header = new CookieHeaderValue(name, value);
|
||||
Assert.Equal(name, header.Name);
|
||||
Assert.Equal(value, header.Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CookieHeaderValue_Value()
|
||||
{
|
||||
var cookie = new CookieHeaderValue("name");
|
||||
Assert.Equal(String.Empty, cookie.Value);
|
||||
|
||||
cookie.Value = "value1";
|
||||
Assert.Equal("value1", cookie.Value);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[MemberData(nameof(CookieHeaderDataSet))]
|
||||
public void CookieHeaderValue_ToString(CookieHeaderValue input, string expectedValue)
|
||||
{
|
||||
Assert.Equal(expectedValue, input.ToString());
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[MemberData(nameof(CookieHeaderDataSet))]
|
||||
public void CookieHeaderValue_Parse_AcceptsValidValues(CookieHeaderValue cookie, string expectedValue)
|
||||
{
|
||||
var header = CookieHeaderValue.Parse(expectedValue);
|
||||
|
||||
Assert.Equal(cookie, header);
|
||||
Assert.Equal(expectedValue, header.ToString());
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[MemberData(nameof(CookieHeaderDataSet))]
|
||||
public void CookieHeaderValue_TryParse_AcceptsValidValues(CookieHeaderValue cookie, string expectedValue)
|
||||
{
|
||||
CookieHeaderValue header;
|
||||
bool result = CookieHeaderValue.TryParse(expectedValue, out header);
|
||||
Assert.True(result);
|
||||
|
||||
Assert.Equal(cookie, header);
|
||||
Assert.Equal(expectedValue, header.ToString());
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[MemberData(nameof(InvalidCookieHeaderDataSet))]
|
||||
public void CookieHeaderValue_Parse_RejectsInvalidValues(string value)
|
||||
{
|
||||
Assert.Throws<FormatException>(() => CookieHeaderValue.Parse(value));
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[MemberData(nameof(InvalidCookieHeaderDataSet))]
|
||||
public void CookieHeaderValue_TryParse_RejectsInvalidValues(string value)
|
||||
{
|
||||
CookieHeaderValue header;
|
||||
bool result = CookieHeaderValue.TryParse(value, out header);
|
||||
|
||||
Assert.False(result);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[MemberData(nameof(ListOfCookieHeaderDataSet))]
|
||||
public void CookieHeaderValue_ParseList_AcceptsValidValues(IList<CookieHeaderValue> cookies, string[] input)
|
||||
{
|
||||
var results = CookieHeaderValue.ParseList(input);
|
||||
|
||||
Assert.Equal(cookies, results);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[MemberData(nameof(ListOfCookieHeaderDataSet))]
|
||||
public void CookieHeaderValue_TryParseList_AcceptsValidValues(IList<CookieHeaderValue> cookies, string[] input)
|
||||
{
|
||||
IList<CookieHeaderValue> results;
|
||||
bool result = CookieHeaderValue.TryParseList(input, out results);
|
||||
Assert.True(result);
|
||||
|
||||
Assert.Equal(cookies, results);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,58 @@
|
|||
// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved.
|
||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
using System;
|
||||
using Xunit;
|
||||
|
||||
namespace Microsoft.Net.Http.Headers
|
||||
{
|
||||
public class DateParserTest
|
||||
{
|
||||
[Fact]
|
||||
public void TryParse_SetOfValidValueStrings_ParsedCorrectly()
|
||||
{
|
||||
// We don't need to validate all possible date values, since they're already tested in HttpRuleParserTest.
|
||||
// Just make sure the parser calls HttpRuleParser methods correctly.
|
||||
CheckValidParsedValue("Tue, 15 Nov 1994 08:12:31 GMT", new DateTimeOffset(1994, 11, 15, 8, 12, 31, TimeSpan.Zero));
|
||||
CheckValidParsedValue(" Sunday, 06-Nov-94 08:49:37 GMT ", new DateTimeOffset(1994, 11, 6, 8, 49, 37, TimeSpan.Zero));
|
||||
CheckValidParsedValue(" Tue,\r\n 15 Nov\r\n 1994 08:12:31 GMT ", new DateTimeOffset(1994, 11, 15, 8, 12, 31, TimeSpan.Zero));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TryParse_SetOfInvalidValueStrings_ReturnsFalse()
|
||||
{
|
||||
CheckInvalidParsedValue(null);
|
||||
CheckInvalidParsedValue(string.Empty);
|
||||
CheckInvalidParsedValue(" ");
|
||||
CheckInvalidParsedValue("!!Sunday, 06-Nov-94 08:49:37 GMT");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ToString_UseDifferentValues_MatchExpectation()
|
||||
{
|
||||
Assert.Equal("Sat, 31 Jul 2010 15:38:57 GMT",
|
||||
HeaderUtilities.FormatDate(new DateTimeOffset(2010, 7, 31, 15, 38, 57, TimeSpan.Zero)));
|
||||
|
||||
Assert.Equal("Fri, 01 Jan 2010 01:01:01 GMT",
|
||||
HeaderUtilities.FormatDate(new DateTimeOffset(2010, 1, 1, 1, 1, 1, TimeSpan.Zero)));
|
||||
}
|
||||
|
||||
#region Helper methods
|
||||
|
||||
private void CheckValidParsedValue(string input, DateTimeOffset expectedResult)
|
||||
{
|
||||
DateTimeOffset result;
|
||||
Assert.True(HeaderUtilities.TryParseDate(input, out result));
|
||||
Assert.Equal(expectedResult, result);
|
||||
}
|
||||
|
||||
private void CheckInvalidParsedValue(string input)
|
||||
{
|
||||
DateTimeOffset result;
|
||||
Assert.False(HeaderUtilities.TryParseDate(input, out result));
|
||||
Assert.Equal(new DateTimeOffset(), result);
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,315 @@
|
|||
// Copyright (c) Microsoft Open Technologies, Inc. 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.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Xunit;
|
||||
|
||||
namespace Microsoft.Net.Http.Headers
|
||||
{
|
||||
public class EntityTagHeaderValueTest
|
||||
{
|
||||
[Fact]
|
||||
public void Ctor_ETagNull_Throw()
|
||||
{
|
||||
Assert.Throws<ArgumentException>(() => new EntityTagHeaderValue(null));
|
||||
// null and empty should be treated the same. So we also throw for empty strings.
|
||||
Assert.Throws<ArgumentException>(() => new EntityTagHeaderValue(string.Empty));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Ctor_ETagInvalidFormat_ThrowFormatException()
|
||||
{
|
||||
// When adding values using strongly typed objects, no leading/trailing LWS (whitespaces) are allowed.
|
||||
AssertFormatException("tag");
|
||||
AssertFormatException(" tag ");
|
||||
AssertFormatException("\"tag\" invalid");
|
||||
AssertFormatException("\"tag");
|
||||
AssertFormatException("tag\"");
|
||||
AssertFormatException("\"tag\"\"");
|
||||
AssertFormatException("\"\"tag\"\"");
|
||||
AssertFormatException("\"\"tag\"");
|
||||
AssertFormatException("W/\"tag\""); // tag value must not contain 'W/'
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Ctor_ETagValidFormat_SuccessfullyCreated()
|
||||
{
|
||||
var etag = new EntityTagHeaderValue("\"tag\"");
|
||||
Assert.Equal("\"tag\"", etag.Tag);
|
||||
Assert.False(etag.IsWeak, "IsWeak");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Ctor_ETagValidFormatAndIsWeak_SuccessfullyCreated()
|
||||
{
|
||||
var etag = new EntityTagHeaderValue("\"e tag\"", true);
|
||||
Assert.Equal("\"e tag\"", etag.Tag);
|
||||
Assert.True(etag.IsWeak, "IsWeak");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ToString_UseDifferentETags_AllSerializedCorrectly()
|
||||
{
|
||||
var etag = new EntityTagHeaderValue("\"e tag\"");
|
||||
Assert.Equal("\"e tag\"", etag.ToString());
|
||||
|
||||
etag = new EntityTagHeaderValue("\"e tag\"", true);
|
||||
Assert.Equal("W/\"e tag\"", etag.ToString());
|
||||
|
||||
etag = new EntityTagHeaderValue("\"\"", false);
|
||||
Assert.Equal("\"\"", etag.ToString());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetHashCode_UseSameAndDifferentETags_SameOrDifferentHashCodes()
|
||||
{
|
||||
var etag1 = new EntityTagHeaderValue("\"tag\"");
|
||||
var etag2 = new EntityTagHeaderValue("\"TAG\"");
|
||||
var etag3 = new EntityTagHeaderValue("\"tag\"", true);
|
||||
var etag4 = new EntityTagHeaderValue("\"tag1\"");
|
||||
var etag5 = new EntityTagHeaderValue("\"tag\"");
|
||||
var etag6 = EntityTagHeaderValue.Any;
|
||||
|
||||
Assert.NotEqual(etag1.GetHashCode(), etag2.GetHashCode());
|
||||
Assert.NotEqual(etag1.GetHashCode(), etag3.GetHashCode());
|
||||
Assert.NotEqual(etag1.GetHashCode(), etag4.GetHashCode());
|
||||
Assert.NotEqual(etag1.GetHashCode(), etag6.GetHashCode());
|
||||
Assert.Equal(etag1.GetHashCode(), etag5.GetHashCode());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Equals_UseSameAndDifferentETags_EqualOrNotEqualNoExceptions()
|
||||
{
|
||||
var etag1 = new EntityTagHeaderValue("\"tag\"");
|
||||
var etag2 = new EntityTagHeaderValue("\"TAG\"");
|
||||
var etag3 = new EntityTagHeaderValue("\"tag\"", true);
|
||||
var etag4 = new EntityTagHeaderValue("\"tag1\"");
|
||||
var etag5 = new EntityTagHeaderValue("\"tag\"");
|
||||
var etag6 = EntityTagHeaderValue.Any;
|
||||
|
||||
Assert.False(etag1.Equals(etag2), "Different casing.");
|
||||
Assert.False(etag2.Equals(etag1), "Different casing.");
|
||||
Assert.False(etag1.Equals(null), "tag vs. <null>.");
|
||||
Assert.False(etag1.Equals(etag3), "strong vs. weak.");
|
||||
Assert.False(etag3.Equals(etag1), "weak vs. strong.");
|
||||
Assert.False(etag1.Equals(etag4), "tag vs. tag1.");
|
||||
Assert.False(etag1.Equals(etag6), "tag vs. *.");
|
||||
Assert.True(etag1.Equals(etag5), "tag vs. tag..");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Parse_SetOfValidValueStrings_ParsedCorrectly()
|
||||
{
|
||||
CheckValidParse("\"tag\"", new EntityTagHeaderValue("\"tag\""));
|
||||
CheckValidParse(" \"tag\" ", new EntityTagHeaderValue("\"tag\""));
|
||||
CheckValidParse("\r\n \"tag\"\r\n ", new EntityTagHeaderValue("\"tag\""));
|
||||
CheckValidParse("\"tag\"", new EntityTagHeaderValue("\"tag\""));
|
||||
CheckValidParse("\"tag会\"", new EntityTagHeaderValue("\"tag会\""));
|
||||
CheckValidParse("W/\"tag\"", new EntityTagHeaderValue("\"tag\"", true));
|
||||
CheckValidParse("*", new EntityTagHeaderValue("*"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Parse_SetOfInvalidValueStrings_Throws()
|
||||
{
|
||||
CheckInvalidParse(null);
|
||||
CheckInvalidParse(string.Empty);
|
||||
CheckInvalidParse(" ");
|
||||
CheckInvalidParse(" !");
|
||||
CheckInvalidParse("tag\" !");
|
||||
CheckInvalidParse("!\"tag\"");
|
||||
CheckInvalidParse("\"tag\",");
|
||||
CheckInvalidParse("W");
|
||||
CheckInvalidParse("W/");
|
||||
CheckInvalidParse("W/\"");
|
||||
CheckInvalidParse("\"tag\" \"tag2\"");
|
||||
CheckInvalidParse("/\"tag\"");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TryParse_SetOfValidValueStrings_ParsedCorrectly()
|
||||
{
|
||||
CheckValidTryParse("\"tag\"", new EntityTagHeaderValue("\"tag\""));
|
||||
CheckValidTryParse(" \"tag\" ", new EntityTagHeaderValue("\"tag\""));
|
||||
CheckValidTryParse("\r\n \"tag\"\r\n ", new EntityTagHeaderValue("\"tag\""));
|
||||
CheckValidTryParse("\"tag\"", new EntityTagHeaderValue("\"tag\""));
|
||||
CheckValidTryParse("\"tag会\"", new EntityTagHeaderValue("\"tag会\""));
|
||||
CheckValidTryParse("W/\"tag\"", new EntityTagHeaderValue("\"tag\"", true));
|
||||
CheckValidTryParse("*", new EntityTagHeaderValue("*"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TryParse_SetOfInvalidValueStrings_ReturnsFalse()
|
||||
{
|
||||
CheckInvalidTryParse(null);
|
||||
CheckInvalidTryParse(string.Empty);
|
||||
CheckInvalidTryParse(" ");
|
||||
CheckInvalidTryParse(" !");
|
||||
CheckInvalidTryParse("tag\" !");
|
||||
CheckInvalidTryParse("!\"tag\"");
|
||||
CheckInvalidTryParse("\"tag\",");
|
||||
CheckInvalidTryParse("\"tag\" \"tag2\"");
|
||||
CheckInvalidTryParse("/\"tag\"");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ParseList_NullOrEmptyArray_ReturnsEmptyList()
|
||||
{
|
||||
var result = EntityTagHeaderValue.ParseList(null);
|
||||
Assert.NotNull(result);
|
||||
Assert.Equal(0, result.Count);
|
||||
|
||||
result = EntityTagHeaderValue.ParseList(new string[0]);
|
||||
Assert.NotNull(result);
|
||||
Assert.Equal(0, result.Count);
|
||||
|
||||
result = EntityTagHeaderValue.ParseList(new string[] { "" });
|
||||
Assert.NotNull(result);
|
||||
Assert.Equal(0, result.Count);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TryParseList_NullOrEmptyArray_ReturnsFalse()
|
||||
{
|
||||
IList<EntityTagHeaderValue> results = null;
|
||||
Assert.False(EntityTagHeaderValue.TryParseList(null, out results));
|
||||
Assert.False(EntityTagHeaderValue.TryParseList(new string[0], out results));
|
||||
Assert.False(EntityTagHeaderValue.TryParseList(new string[] { "" }, out results));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ParseList_SetOfValidValueStrings_ParsedCorrectly()
|
||||
{
|
||||
var inputs = new[]
|
||||
{
|
||||
"",
|
||||
"\"tag\"",
|
||||
"",
|
||||
" \"tag\" ",
|
||||
"\r\n \"tag\"\r\n ",
|
||||
"\"tag会\"",
|
||||
"\"tag\",\"tag\"",
|
||||
"\"tag\", \"tag\"",
|
||||
"W/\"tag\"",
|
||||
};
|
||||
IList<EntityTagHeaderValue> results = EntityTagHeaderValue.ParseList(inputs);
|
||||
|
||||
var expectedResults = new[]
|
||||
{
|
||||
new EntityTagHeaderValue("\"tag\""),
|
||||
new EntityTagHeaderValue("\"tag\""),
|
||||
new EntityTagHeaderValue("\"tag\""),
|
||||
new EntityTagHeaderValue("\"tag会\""),
|
||||
new EntityTagHeaderValue("\"tag\""),
|
||||
new EntityTagHeaderValue("\"tag\""),
|
||||
new EntityTagHeaderValue("\"tag\""),
|
||||
new EntityTagHeaderValue("\"tag\""),
|
||||
new EntityTagHeaderValue("\"tag\"", true),
|
||||
}.ToList();
|
||||
|
||||
Assert.Equal(expectedResults, results);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TryParseList_SetOfValidValueStrings_ParsedCorrectly()
|
||||
{
|
||||
var inputs = new[]
|
||||
{
|
||||
"",
|
||||
"\"tag\"",
|
||||
"",
|
||||
" \"tag\" ",
|
||||
"\r\n \"tag\"\r\n ",
|
||||
"\"tag会\"",
|
||||
"\"tag\",\"tag\"",
|
||||
"\"tag\", \"tag\"",
|
||||
"W/\"tag\"",
|
||||
};
|
||||
IList<EntityTagHeaderValue> results;
|
||||
Assert.True(EntityTagHeaderValue.TryParseList(inputs, out results));
|
||||
var expectedResults = new[]
|
||||
{
|
||||
new EntityTagHeaderValue("\"tag\""),
|
||||
new EntityTagHeaderValue("\"tag\""),
|
||||
new EntityTagHeaderValue("\"tag\""),
|
||||
new EntityTagHeaderValue("\"tag会\""),
|
||||
new EntityTagHeaderValue("\"tag\""),
|
||||
new EntityTagHeaderValue("\"tag\""),
|
||||
new EntityTagHeaderValue("\"tag\""),
|
||||
new EntityTagHeaderValue("\"tag\""),
|
||||
new EntityTagHeaderValue("\"tag\"", true),
|
||||
}.ToList();
|
||||
|
||||
Assert.Equal(expectedResults, results);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ParseList_WithSomeInvlaidValues_Throws()
|
||||
{
|
||||
var inputs = new[]
|
||||
{
|
||||
"",
|
||||
"\"tag\", tag, \"tag\"",
|
||||
"tag, \"tag\"",
|
||||
"",
|
||||
" \"tag ",
|
||||
"\r\n tag\"\r\n ",
|
||||
"\"tag会\"",
|
||||
"\"tag\", \"tag\"",
|
||||
"W/\"tag\"",
|
||||
};
|
||||
Assert.Throws<FormatException>(() => EntityTagHeaderValue.ParseList(inputs));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TryParseList_WithSomeInvlaidValues_ReturnsFalse()
|
||||
{
|
||||
var inputs = new[]
|
||||
{
|
||||
"",
|
||||
"\"tag\", tag, \"tag\"",
|
||||
"tag, \"tag\"",
|
||||
"",
|
||||
" \"tag ",
|
||||
"\r\n tag\"\r\n ",
|
||||
"\"tag会\"",
|
||||
"\"tag\", \"tag\"",
|
||||
"W/\"tag\"",
|
||||
};
|
||||
IList<EntityTagHeaderValue> results;
|
||||
Assert.False(EntityTagHeaderValue.TryParseList(inputs, out results));
|
||||
}
|
||||
|
||||
private void CheckValidParse(string input, EntityTagHeaderValue expectedResult)
|
||||
{
|
||||
var result = EntityTagHeaderValue.Parse(input);
|
||||
Assert.Equal(expectedResult, result);
|
||||
}
|
||||
|
||||
private void CheckInvalidParse(string input)
|
||||
{
|
||||
Assert.Throws<FormatException>(() => EntityTagHeaderValue.Parse(input));
|
||||
}
|
||||
|
||||
private void CheckValidTryParse(string input, EntityTagHeaderValue expectedResult)
|
||||
{
|
||||
EntityTagHeaderValue result = null;
|
||||
Assert.True(EntityTagHeaderValue.TryParse(input, out result));
|
||||
Assert.Equal(expectedResult, result);
|
||||
}
|
||||
|
||||
private void CheckInvalidTryParse(string input)
|
||||
{
|
||||
EntityTagHeaderValue result = null;
|
||||
Assert.False(EntityTagHeaderValue.TryParse(input, out result));
|
||||
Assert.Null(result);
|
||||
}
|
||||
|
||||
private static void AssertFormatException(string tag)
|
||||
{
|
||||
Assert.Throws<FormatException>(() => new EntityTagHeaderValue(tag));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,69 @@
|
|||
// Copyright (c) Microsoft Open Technologies, Inc. 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.Linq;
|
||||
using Xunit;
|
||||
|
||||
namespace Microsoft.Net.Http.Headers
|
||||
{
|
||||
public class MediaTypeHeaderValueComparerTests
|
||||
{
|
||||
public static IEnumerable<object[]> SortValues
|
||||
{
|
||||
get
|
||||
{
|
||||
yield return new object[] {
|
||||
new string[]
|
||||
{
|
||||
"application/*",
|
||||
"text/plain",
|
||||
"text/plain;q=1.0",
|
||||
"text/plain",
|
||||
"text/plain;q=0",
|
||||
"*/*;q=0.8",
|
||||
"*/*;q=1",
|
||||
"text/*;q=1",
|
||||
"text/plain;q=0.8",
|
||||
"text/*;q=0.8",
|
||||
"text/*;q=0.6",
|
||||
"text/*;q=1.0",
|
||||
"*/*;q=0.4",
|
||||
"text/plain;q=0.6",
|
||||
"text/xml",
|
||||
},
|
||||
new string[]
|
||||
{
|
||||
"text/plain",
|
||||
"text/plain;q=1.0",
|
||||
"text/plain",
|
||||
"text/xml",
|
||||
"application/*",
|
||||
"text/*;q=1",
|
||||
"text/*;q=1.0",
|
||||
"*/*;q=1",
|
||||
"text/plain;q=0.8",
|
||||
"text/*;q=0.8",
|
||||
"*/*;q=0.8",
|
||||
"text/plain;q=0.6",
|
||||
"text/*;q=0.6",
|
||||
"*/*;q=0.4",
|
||||
"text/plain;q=0",
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[MemberData(nameof(SortValues))]
|
||||
public void SortMediaTypeHeaderValuesByQFactor_SortsCorrectly(IEnumerable<string> unsorted, IEnumerable<string> expectedSorted)
|
||||
{
|
||||
var unsortedValues = MediaTypeHeaderValue.ParseList(unsorted.ToList());
|
||||
var expectedSortedValues = MediaTypeHeaderValue.ParseList(expectedSorted.ToList());
|
||||
|
||||
var actualSorted = unsortedValues.OrderByDescending(m => m, MediaTypeHeaderValueComparer.QualityComparer).ToList();
|
||||
|
||||
Assert.Equal(expectedSortedValues, actualSorted);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,520 @@
|
|||
// Copyright (c) Microsoft Open Technologies, Inc. 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.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Xunit;
|
||||
|
||||
namespace Microsoft.Net.Http.Headers
|
||||
{
|
||||
public class MediaTypeHeaderValueTest
|
||||
{
|
||||
[Fact]
|
||||
public void Ctor_MediaTypeNull_Throw()
|
||||
{
|
||||
Assert.Throws<ArgumentException>(() => new MediaTypeHeaderValue(null));
|
||||
// null and empty should be treated the same. So we also throw for empty strings.
|
||||
Assert.Throws<ArgumentException>(() => new MediaTypeHeaderValue(string.Empty));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Ctor_MediaTypeInvalidFormat_ThrowFormatException()
|
||||
{
|
||||
// When adding values using strongly typed objects, no leading/trailing LWS (whitespaces) are allowed.
|
||||
AssertFormatException(" text/plain ");
|
||||
AssertFormatException("text / plain");
|
||||
AssertFormatException("text/ plain");
|
||||
AssertFormatException("text /plain");
|
||||
AssertFormatException("text/plain ");
|
||||
AssertFormatException(" text/plain");
|
||||
AssertFormatException("te xt/plain");
|
||||
AssertFormatException("te=xt/plain");
|
||||
AssertFormatException("teäxt/plain");
|
||||
AssertFormatException("text/pläin");
|
||||
AssertFormatException("text");
|
||||
AssertFormatException("\"text/plain\"");
|
||||
AssertFormatException("text/plain; charset=utf-8; ");
|
||||
AssertFormatException("text/plain;");
|
||||
AssertFormatException("text/plain;charset=utf-8"); // ctor takes only media-type name, no parameters
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Ctor_MediaTypeValidFormat_SuccessfullyCreated()
|
||||
{
|
||||
var mediaType = new MediaTypeHeaderValue("text/plain");
|
||||
Assert.Equal("text/plain", mediaType.MediaType);
|
||||
Assert.Equal(0, mediaType.Parameters.Count);
|
||||
Assert.Null(mediaType.Charset);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Ctor_AddNameAndQuality_QualityParameterAdded()
|
||||
{
|
||||
var mediaType = new MediaTypeHeaderValue("application/xml", 0.08);
|
||||
Assert.Equal(0.08, mediaType.Quality);
|
||||
Assert.Equal("application/xml", mediaType.MediaType);
|
||||
Assert.Equal(1, mediaType.Parameters.Count);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Parameters_AddNull_Throw()
|
||||
{
|
||||
var mediaType = new MediaTypeHeaderValue("text/plain");
|
||||
Assert.Throws<ArgumentNullException>(() => mediaType.Parameters.Add(null));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void MediaType_SetAndGetMediaType_MatchExpectations()
|
||||
{
|
||||
var mediaType = new MediaTypeHeaderValue("text/plain");
|
||||
Assert.Equal("text/plain", mediaType.MediaType);
|
||||
|
||||
mediaType.MediaType = "application/xml";
|
||||
Assert.Equal("application/xml", mediaType.MediaType);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Charset_SetCharsetAndValidateObject_ParametersEntryForCharsetAdded()
|
||||
{
|
||||
var mediaType = new MediaTypeHeaderValue("text/plain");
|
||||
mediaType.Charset = "mycharset";
|
||||
Assert.Equal("mycharset", mediaType.Charset);
|
||||
Assert.Equal(1, mediaType.Parameters.Count);
|
||||
Assert.Equal("charset", mediaType.Parameters.First().Name);
|
||||
|
||||
mediaType.Charset = null;
|
||||
Assert.Null(mediaType.Charset);
|
||||
Assert.Equal(0, mediaType.Parameters.Count);
|
||||
mediaType.Charset = null; // It's OK to set it again to null; no exception.
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Charset_AddCharsetParameterThenUseProperty_ParametersEntryIsOverwritten()
|
||||
{
|
||||
var mediaType = new MediaTypeHeaderValue("text/plain");
|
||||
|
||||
// Note that uppercase letters are used. Comparison should happen case-insensitive.
|
||||
var charset = new NameValueHeaderValue("CHARSET", "old_charset");
|
||||
mediaType.Parameters.Add(charset);
|
||||
Assert.Equal(1, mediaType.Parameters.Count);
|
||||
Assert.Equal("CHARSET", mediaType.Parameters.First().Name);
|
||||
|
||||
mediaType.Charset = "new_charset";
|
||||
Assert.Equal("new_charset", mediaType.Charset);
|
||||
Assert.Equal(1, mediaType.Parameters.Count);
|
||||
Assert.Equal("CHARSET", mediaType.Parameters.First().Name);
|
||||
|
||||
mediaType.Parameters.Remove(charset);
|
||||
Assert.Null(mediaType.Charset);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Quality_SetCharsetAndValidateObject_ParametersEntryForCharsetAdded()
|
||||
{
|
||||
var mediaType = new MediaTypeHeaderValue("text/plain");
|
||||
mediaType.Quality = 0.563156454;
|
||||
Assert.Equal(0.563, mediaType.Quality);
|
||||
Assert.Equal(1, mediaType.Parameters.Count);
|
||||
Assert.Equal("q", mediaType.Parameters.First().Name);
|
||||
Assert.Equal("0.563", mediaType.Parameters.First().Value);
|
||||
|
||||
mediaType.Quality = null;
|
||||
Assert.Null(mediaType.Quality);
|
||||
Assert.Equal(0, mediaType.Parameters.Count);
|
||||
mediaType.Quality = null; // It's OK to set it again to null; no exception.
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Quality_AddQualityParameterThenUseProperty_ParametersEntryIsOverwritten()
|
||||
{
|
||||
var mediaType = new MediaTypeHeaderValue("text/plain");
|
||||
|
||||
var quality = new NameValueHeaderValue("q", "0.132");
|
||||
mediaType.Parameters.Add(quality);
|
||||
Assert.Equal(1, mediaType.Parameters.Count);
|
||||
Assert.Equal("q", mediaType.Parameters.First().Name);
|
||||
Assert.Equal(0.132, mediaType.Quality);
|
||||
|
||||
mediaType.Quality = 0.9;
|
||||
Assert.Equal(0.9, mediaType.Quality);
|
||||
Assert.Equal(1, mediaType.Parameters.Count);
|
||||
Assert.Equal("q", mediaType.Parameters.First().Name);
|
||||
|
||||
mediaType.Parameters.Remove(quality);
|
||||
Assert.Null(mediaType.Quality);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Quality_AddQualityParameterUpperCase_CaseInsensitiveComparison()
|
||||
{
|
||||
var mediaType = new MediaTypeHeaderValue("text/plain");
|
||||
|
||||
var quality = new NameValueHeaderValue("Q", "0.132");
|
||||
mediaType.Parameters.Add(quality);
|
||||
Assert.Equal(1, mediaType.Parameters.Count);
|
||||
Assert.Equal("Q", mediaType.Parameters.First().Name);
|
||||
Assert.Equal(0.132, mediaType.Quality);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Quality_LessThanZero_Throw()
|
||||
{
|
||||
Assert.Throws<ArgumentOutOfRangeException>(() => new MediaTypeHeaderValue("application/xml", -0.01));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Quality_GreaterThanOne_Throw()
|
||||
{
|
||||
var mediaType = new MediaTypeHeaderValue("application/xml");
|
||||
Assert.Throws<ArgumentOutOfRangeException>(() => mediaType.Quality = 1.01);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ToString_UseDifferentMediaTypes_AllSerializedCorrectly()
|
||||
{
|
||||
var mediaType = new MediaTypeHeaderValue("text/plain");
|
||||
Assert.Equal("text/plain", mediaType.ToString());
|
||||
|
||||
mediaType.Charset = "utf-8";
|
||||
Assert.Equal("text/plain; charset=utf-8", mediaType.ToString());
|
||||
|
||||
mediaType.Parameters.Add(new NameValueHeaderValue("custom", "\"custom value\""));
|
||||
Assert.Equal("text/plain; charset=utf-8; custom=\"custom value\"", mediaType.ToString());
|
||||
|
||||
mediaType.Charset = null;
|
||||
Assert.Equal("text/plain; custom=\"custom value\"", mediaType.ToString());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetHashCode_UseMediaTypeWithAndWithoutParameters_SameOrDifferentHashCodes()
|
||||
{
|
||||
var mediaType1 = new MediaTypeHeaderValue("text/plain");
|
||||
var mediaType2 = new MediaTypeHeaderValue("text/plain");
|
||||
mediaType2.Charset = "utf-8";
|
||||
var mediaType3 = new MediaTypeHeaderValue("text/plain");
|
||||
mediaType3.Parameters.Add(new NameValueHeaderValue("name", "value"));
|
||||
var mediaType4 = new MediaTypeHeaderValue("TEXT/plain");
|
||||
var mediaType5 = new MediaTypeHeaderValue("TEXT/plain");
|
||||
mediaType5.Parameters.Add(new NameValueHeaderValue("CHARSET", "UTF-8"));
|
||||
|
||||
Assert.NotEqual(mediaType1.GetHashCode(), mediaType2.GetHashCode());
|
||||
Assert.NotEqual(mediaType1.GetHashCode(), mediaType3.GetHashCode());
|
||||
Assert.NotEqual(mediaType2.GetHashCode(), mediaType3.GetHashCode());
|
||||
Assert.Equal(mediaType1.GetHashCode(), mediaType4.GetHashCode());
|
||||
Assert.Equal(mediaType2.GetHashCode(), mediaType5.GetHashCode());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Equals_UseMediaTypeWithAndWithoutParameters_EqualOrNotEqualNoExceptions()
|
||||
{
|
||||
var mediaType1 = new MediaTypeHeaderValue("text/plain");
|
||||
var mediaType2 = new MediaTypeHeaderValue("text/plain");
|
||||
mediaType2.Charset = "utf-8";
|
||||
var mediaType3 = new MediaTypeHeaderValue("text/plain");
|
||||
mediaType3.Parameters.Add(new NameValueHeaderValue("name", "value"));
|
||||
var mediaType4 = new MediaTypeHeaderValue("TEXT/plain");
|
||||
var mediaType5 = new MediaTypeHeaderValue("TEXT/plain");
|
||||
mediaType5.Parameters.Add(new NameValueHeaderValue("CHARSET", "UTF-8"));
|
||||
var mediaType6 = new MediaTypeHeaderValue("TEXT/plain");
|
||||
mediaType6.Parameters.Add(new NameValueHeaderValue("CHARSET", "UTF-8"));
|
||||
mediaType6.Parameters.Add(new NameValueHeaderValue("custom", "value"));
|
||||
var mediaType7 = new MediaTypeHeaderValue("text/other");
|
||||
|
||||
Assert.False(mediaType1.Equals(mediaType2), "No params vs. charset.");
|
||||
Assert.False(mediaType2.Equals(mediaType1), "charset vs. no params.");
|
||||
Assert.False(mediaType1.Equals(null), "No params vs. <null>.");
|
||||
Assert.False(mediaType1.Equals(mediaType3), "No params vs. custom param.");
|
||||
Assert.False(mediaType2.Equals(mediaType3), "charset vs. custom param.");
|
||||
Assert.True(mediaType1.Equals(mediaType4), "Different casing.");
|
||||
Assert.True(mediaType2.Equals(mediaType5), "Different casing in charset.");
|
||||
Assert.False(mediaType5.Equals(mediaType6), "charset vs. custom param.");
|
||||
Assert.False(mediaType1.Equals(mediaType7), "text/plain vs. text/other.");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Parse_SetOfValidValueStrings_ParsedCorrectly()
|
||||
{
|
||||
CheckValidParse("\r\n text/plain ", new MediaTypeHeaderValue("text/plain"));
|
||||
CheckValidParse("text/plain", new MediaTypeHeaderValue("text/plain"));
|
||||
|
||||
CheckValidParse("\r\n text / plain ; charset = utf-8 ", new MediaTypeHeaderValue("text/plain") { Charset = "utf-8" });
|
||||
CheckValidParse(" text/plain;charset=utf-8", new MediaTypeHeaderValue("text/plain") { Charset = "utf-8" });
|
||||
|
||||
CheckValidParse("text/plain; charset=iso-8859-1", new MediaTypeHeaderValue("text/plain") { Charset = "iso-8859-1" });
|
||||
|
||||
var expected = new MediaTypeHeaderValue("text/plain") { Charset = "utf-8" };
|
||||
expected.Parameters.Add(new NameValueHeaderValue("custom", "value"));
|
||||
CheckValidParse(" text/plain; custom=value;charset=utf-8", expected);
|
||||
|
||||
expected = new MediaTypeHeaderValue("text/plain");
|
||||
expected.Parameters.Add(new NameValueHeaderValue("custom"));
|
||||
CheckValidParse(" text/plain; custom", expected);
|
||||
|
||||
expected = new MediaTypeHeaderValue("text/plain") { Charset = "utf-8" };
|
||||
expected.Parameters.Add(new NameValueHeaderValue("custom", "\"x\""));
|
||||
CheckValidParse("text / plain ; custom =\r\n \"x\" ; charset = utf-8 ", expected);
|
||||
|
||||
expected = new MediaTypeHeaderValue("text/plain") { Charset = "utf-8" };
|
||||
expected.Parameters.Add(new NameValueHeaderValue("custom", "\"x\""));
|
||||
CheckValidParse("text/plain;custom=\"x\";charset=utf-8", expected);
|
||||
|
||||
expected = new MediaTypeHeaderValue("text/plain");
|
||||
CheckValidParse("text/plain;", expected);
|
||||
|
||||
expected = new MediaTypeHeaderValue("text/plain");
|
||||
expected.Parameters.Add(new NameValueHeaderValue("name", ""));
|
||||
CheckValidParse("text/plain;name=", expected);
|
||||
|
||||
expected = new MediaTypeHeaderValue("text/plain");
|
||||
expected.Parameters.Add(new NameValueHeaderValue("name", "value"));
|
||||
CheckValidParse("text/plain;name=value;", expected);
|
||||
|
||||
expected = new MediaTypeHeaderValue("text/plain");
|
||||
expected.Charset = "iso-8859-1";
|
||||
expected.Quality = 1.0;
|
||||
CheckValidParse("text/plain; charset=iso-8859-1; q=1.0", expected);
|
||||
|
||||
expected = new MediaTypeHeaderValue("*/xml");
|
||||
expected.Charset = "utf-8";
|
||||
expected.Quality = 0.5;
|
||||
CheckValidParse("\r\n */xml; charset=utf-8; q=0.5", expected);
|
||||
|
||||
expected = new MediaTypeHeaderValue("*/*");
|
||||
CheckValidParse("*/*", expected);
|
||||
|
||||
expected = new MediaTypeHeaderValue("text/*");
|
||||
expected.Charset = "utf-8";
|
||||
expected.Parameters.Add(new NameValueHeaderValue("foo", "bar"));
|
||||
CheckValidParse("text/*; charset=utf-8; foo=bar", expected);
|
||||
|
||||
expected = new MediaTypeHeaderValue("text/plain");
|
||||
expected.Charset = "utf-8";
|
||||
expected.Quality = 0;
|
||||
expected.Parameters.Add(new NameValueHeaderValue("foo", "bar"));
|
||||
CheckValidParse("text/plain; charset=utf-8; foo=bar; q=0.0", expected);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Parse_SetOfInvalidValueStrings_Throws()
|
||||
{
|
||||
CheckInvalidParse("");
|
||||
CheckInvalidParse(" ");
|
||||
CheckInvalidParse(null);
|
||||
CheckInvalidParse("text/plain会");
|
||||
CheckInvalidParse("text/plain ,");
|
||||
CheckInvalidParse("text/plain,");
|
||||
CheckInvalidParse("text/plain; charset=utf-8 ,");
|
||||
CheckInvalidParse("text/plain; charset=utf-8,");
|
||||
CheckInvalidParse("textplain");
|
||||
CheckInvalidParse("text/");
|
||||
CheckInvalidParse(",, , ,,text/plain; charset=iso-8859-1; q=1.0,\r\n */xml; charset=utf-8; q=0.5,,,");
|
||||
CheckInvalidParse("text/plain; charset=iso-8859-1; q=1.0, */xml; charset=utf-8; q=0.5");
|
||||
CheckInvalidParse(" , */xml; charset=utf-8; q=0.5 ");
|
||||
CheckInvalidParse("text/plain; charset=iso-8859-1; q=1.0 , ");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TryParse_SetOfValidValueStrings_ParsedCorrectly()
|
||||
{
|
||||
var expected = new MediaTypeHeaderValue("text/plain");
|
||||
CheckValidTryParse("\r\n text/plain ", expected);
|
||||
CheckValidTryParse("text/plain", expected);
|
||||
|
||||
// We don't have to test all possible input strings, since most of the pieces are handled by other parsers.
|
||||
// The purpose of this test is to verify that these other parsers are combined correctly to build a
|
||||
// media-type parser.
|
||||
expected.Charset = "utf-8";
|
||||
CheckValidTryParse("\r\n text / plain ; charset = utf-8 ", expected);
|
||||
CheckValidTryParse(" text/plain;charset=utf-8", expected);
|
||||
|
||||
var value1 = new MediaTypeHeaderValue("text/plain");
|
||||
value1.Charset = "iso-8859-1";
|
||||
value1.Quality = 1.0;
|
||||
|
||||
CheckValidTryParse("text/plain; charset=iso-8859-1; q=1.0", value1);
|
||||
|
||||
var value2 = new MediaTypeHeaderValue("*/xml");
|
||||
value2.Charset = "utf-8";
|
||||
value2.Quality = 0.5;
|
||||
|
||||
CheckValidTryParse("\r\n */xml; charset=utf-8; q=0.5", value2);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TryParse_SetOfInvalidValueStrings_ReturnsFalse()
|
||||
{
|
||||
CheckInvalidTryParse("");
|
||||
CheckInvalidTryParse(" ");
|
||||
CheckInvalidTryParse(null);
|
||||
CheckInvalidTryParse("text/plain会");
|
||||
CheckInvalidTryParse("text/plain ,");
|
||||
CheckInvalidTryParse("text/plain,");
|
||||
CheckInvalidTryParse("text/plain; charset=utf-8 ,");
|
||||
CheckInvalidTryParse("text/plain; charset=utf-8,");
|
||||
CheckInvalidTryParse("textplain");
|
||||
CheckInvalidTryParse("text/");
|
||||
CheckInvalidTryParse(",, , ,,text/plain; charset=iso-8859-1; q=1.0,\r\n */xml; charset=utf-8; q=0.5,,,");
|
||||
CheckInvalidTryParse("text/plain; charset=iso-8859-1; q=1.0, */xml; charset=utf-8; q=0.5");
|
||||
CheckInvalidTryParse(" , */xml; charset=utf-8; q=0.5 ");
|
||||
CheckInvalidTryParse("text/plain; charset=iso-8859-1; q=1.0 , ");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ParseList_NullOrEmptyArray_ReturnsEmptyList()
|
||||
{
|
||||
var results = MediaTypeHeaderValue.ParseList(null);
|
||||
Assert.NotNull(results);
|
||||
Assert.Equal(0, results.Count);
|
||||
|
||||
results = MediaTypeHeaderValue.ParseList(new string[0]);
|
||||
Assert.NotNull(results);
|
||||
Assert.Equal(0, results.Count);
|
||||
|
||||
results = MediaTypeHeaderValue.ParseList(new string[] { "" });
|
||||
Assert.NotNull(results);
|
||||
Assert.Equal(0, results.Count);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TryParseList_NullOrEmptyArray_ReturnsFalse()
|
||||
{
|
||||
IList<MediaTypeHeaderValue> results;
|
||||
Assert.False(MediaTypeHeaderValue.TryParseList(null, out results));
|
||||
Assert.False(MediaTypeHeaderValue.TryParseList(new string[0], out results));
|
||||
Assert.False(MediaTypeHeaderValue.TryParseList(new string[] { "" }, out results));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ParseList_SetOfValidValueStrings_ReturnsValues()
|
||||
{
|
||||
var inputs = new[] { "text/html,application/xhtml+xml,", "application/xml;q=0.9,image/webp,*/*;q=0.8" };
|
||||
var results = MediaTypeHeaderValue.ParseList(inputs);
|
||||
|
||||
var expectedResults = new[]
|
||||
{
|
||||
new MediaTypeHeaderValue("text/html"),
|
||||
new MediaTypeHeaderValue("application/xhtml+xml"),
|
||||
new MediaTypeHeaderValue("application/xml", 0.9),
|
||||
new MediaTypeHeaderValue("image/webp"),
|
||||
new MediaTypeHeaderValue("*/*", 0.8),
|
||||
}.ToList();
|
||||
|
||||
Assert.Equal(expectedResults, results);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TryParseList_SetOfValidValueStrings_ReturnsTrue()
|
||||
{
|
||||
var inputs = new[] { "text/html,application/xhtml+xml,", "application/xml;q=0.9,image/webp,*/*;q=0.8" };
|
||||
IList<MediaTypeHeaderValue> results;
|
||||
Assert.True(MediaTypeHeaderValue.TryParseList(inputs, out results));
|
||||
|
||||
var expectedResults = new[]
|
||||
{
|
||||
new MediaTypeHeaderValue("text/html"),
|
||||
new MediaTypeHeaderValue("application/xhtml+xml"),
|
||||
new MediaTypeHeaderValue("application/xml", 0.9),
|
||||
new MediaTypeHeaderValue("image/webp"),
|
||||
new MediaTypeHeaderValue("*/*", 0.8),
|
||||
}.ToList();
|
||||
|
||||
Assert.Equal(expectedResults, results);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ParseList_WithSomeInvlaidValues_Throws()
|
||||
{
|
||||
var inputs = new[]
|
||||
{
|
||||
"text/html,application/xhtml+xml, ignore-this, ignore/this",
|
||||
"application/xml;q=0.9,image/webp,*/*;q=0.8"
|
||||
};
|
||||
Assert.Throws<FormatException>(() => MediaTypeHeaderValue.ParseList(inputs));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TryParseList_WithSomeInvlaidValues_ReturnsFalse()
|
||||
{
|
||||
var inputs = new[]
|
||||
{
|
||||
"text/html,application/xhtml+xml, ignore-this, ignore/this",
|
||||
"application/xml;q=0.9,image/webp,*/*;q=0.8",
|
||||
"application/xml;q=0 4"
|
||||
};
|
||||
IList<MediaTypeHeaderValue> results;
|
||||
Assert.False(MediaTypeHeaderValue.TryParseList(inputs, out results));
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("*/*;", "*/*")]
|
||||
[InlineData("text/*;", "text/*")]
|
||||
[InlineData("text/plain;", "text/plain")]
|
||||
[InlineData("*/*;", "*/*;charset=utf-8;")]
|
||||
[InlineData("text/*;", "*/*;charset=utf-8;")]
|
||||
[InlineData("text/plain;", "*/*;charset=utf-8;")]
|
||||
[InlineData("text/plain;", "text/*;charset=utf-8;")]
|
||||
[InlineData("text/plain;", "text/plain;charset=utf-8;")]
|
||||
[InlineData("text/plain;version=v1", "Text/plain;Version=v1")]
|
||||
[InlineData("text/plain;version=v1", "tExT/plain;version=V1")]
|
||||
[InlineData("text/plain;version=v1", "TEXT/PLAIN;VERSION=V1")]
|
||||
[InlineData("text/plain;charset=utf-8;foo=bar;q=0.0", "text/plain;charset=utf-8;foo=bar;q=0.0")]
|
||||
[InlineData("text/plain;charset=utf-8;foo=bar;q=0.0", "text/plain;foo=bar;q=0.0;charset=utf-8")] // different order of parameters
|
||||
[InlineData("text/plain;charset=utf-8;foo=bar;q=0.0", "text/*;charset=utf-8;foo=bar;q=0.0")]
|
||||
[InlineData("text/plain;charset=utf-8;foo=bar;q=0.0", "*/*;charset=utf-8;foo=bar;q=0.0")]
|
||||
public void IsSubsetOf_PositiveCases(string mediaType1, string mediaType2)
|
||||
{
|
||||
var parsedMediaType1 = MediaTypeHeaderValue.Parse(mediaType1);
|
||||
var parsedMediaType2 = MediaTypeHeaderValue.Parse(mediaType2);
|
||||
|
||||
var isSubset = parsedMediaType1.IsSubsetOf(parsedMediaType2);
|
||||
Assert.True(isSubset);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("text/plain;version=v1", "text/plain;version=")]
|
||||
[InlineData("*/*;", "text/plain;charset=utf-8;foo=bar;q=0.0")]
|
||||
[InlineData("text/*;", "text/plain;charset=utf-8;foo=bar;q=0.0")]
|
||||
[InlineData("text/plain;missingparam=4;", "text/plain;charset=utf-8;foo=bar;q=0.0")]
|
||||
[InlineData("text/plain;missingparam=4;", "text/*;charset=utf-8;foo=bar;q=0.0")]
|
||||
[InlineData("text/plain;missingparam=4;", "*/*;charset=utf-8;foo=bar;q=0.0")]
|
||||
public void IsSubsetOf_NegativeCases(string mediaType1, string mediaType2)
|
||||
{
|
||||
var parsedMediaType1 = MediaTypeHeaderValue.Parse(mediaType1);
|
||||
var parsedMediaType2 = MediaTypeHeaderValue.Parse(mediaType2);
|
||||
|
||||
var isSubset = parsedMediaType1.IsSubsetOf(parsedMediaType2);
|
||||
Assert.False(isSubset);
|
||||
}
|
||||
|
||||
private void CheckValidParse(string input, MediaTypeHeaderValue expectedResult)
|
||||
{
|
||||
var result = MediaTypeHeaderValue.Parse(input);
|
||||
Assert.Equal(expectedResult, result);
|
||||
}
|
||||
|
||||
private void CheckInvalidParse(string input)
|
||||
{
|
||||
Assert.Throws<FormatException>(() => MediaTypeHeaderValue.Parse(input));
|
||||
}
|
||||
|
||||
private void CheckValidTryParse(string input, MediaTypeHeaderValue expectedResult)
|
||||
{
|
||||
MediaTypeHeaderValue result = null;
|
||||
Assert.True(MediaTypeHeaderValue.TryParse(input, out result));
|
||||
Assert.Equal(expectedResult, result);
|
||||
}
|
||||
|
||||
private void CheckInvalidTryParse(string input)
|
||||
{
|
||||
MediaTypeHeaderValue result = null;
|
||||
Assert.False(MediaTypeHeaderValue.TryParse(input, out result));
|
||||
Assert.Null(result);
|
||||
}
|
||||
|
||||
private static void AssertFormatException(string mediaType)
|
||||
{
|
||||
Assert.Throws<FormatException>(() => new MediaTypeHeaderValue(mediaType));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,23 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<Project ToolsVersion="14.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
|
||||
<PropertyGroup>
|
||||
<VisualStudioVersion Condition="'$(VisualStudioVersion)' == ''">14.0</VisualStudioVersion>
|
||||
<VSToolsPath Condition="'$(VSToolsPath)' == ''">$(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion)</VSToolsPath>
|
||||
</PropertyGroup>
|
||||
<Import Project="$(VSToolsPath)\AspNet\Microsoft.Web.AspNet.Props" Condition="'$(VSToolsPath)' != ''" />
|
||||
<PropertyGroup Label="Globals">
|
||||
<ProjectGuid>e6bb7ad1-bd10-4a23-b780-f4a86adf00d1</ProjectGuid>
|
||||
<RootNamespace>Microsoft.Net.Http.Headers.Tests</RootNamespace>
|
||||
<BaseIntermediateOutputPath Condition="'$(BaseIntermediateOutputPath)'=='' ">..\..\artifacts\obj\$(MSBuildProjectName)</BaseIntermediateOutputPath>
|
||||
<OutputPath Condition="'$(OutputPath)'=='' ">..\..\artifacts\bin\$(MSBuildProjectName)\</OutputPath>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup>
|
||||
<SchemaVersion>2.0</SchemaVersion>
|
||||
</PropertyGroup>
|
||||
<Import Project="$(VSToolsPath)\AspNet\Microsoft.Web.AspNet.targets" Condition="'$(VSToolsPath)' != ''" />
|
||||
<ProjectExtensions>
|
||||
<VisualStudio>
|
||||
<UserProperties project_1json__JSONSchema="http://www.asp.net/media/4878834/project.json" />
|
||||
</VisualStudio>
|
||||
</ProjectExtensions>
|
||||
</Project>
|
||||
|
|
@ -0,0 +1,394 @@
|
|||
// Copyright (c) Microsoft Open Technologies, Inc. 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.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Xunit;
|
||||
|
||||
namespace Microsoft.Net.Http.Headers
|
||||
{
|
||||
public class NameValueHeaderValueTest
|
||||
{
|
||||
[Fact]
|
||||
public void Ctor_NameNull_Throw()
|
||||
{
|
||||
Assert.Throws<ArgumentException>(() => new NameValueHeaderValue(null));
|
||||
// null and empty should be treated the same. So we also throw for empty strings.
|
||||
Assert.Throws<ArgumentException>(() => new NameValueHeaderValue(string.Empty));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Ctor_NameInvalidFormat_ThrowFormatException()
|
||||
{
|
||||
// When adding values using strongly typed objects, no leading/trailing LWS (whitespaces) are allowed.
|
||||
AssertFormatException(" text ", null);
|
||||
AssertFormatException("text ", null);
|
||||
AssertFormatException(" text", null);
|
||||
AssertFormatException("te xt", null);
|
||||
AssertFormatException("te=xt", null); // The ctor takes a name which must not contain '='.
|
||||
AssertFormatException("teäxt", null);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Ctor_NameValidFormat_SuccessfullyCreated()
|
||||
{
|
||||
var nameValue = new NameValueHeaderValue("text", null);
|
||||
Assert.Equal("text", nameValue.Name);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Ctor_ValueInvalidFormat_ThrowFormatException()
|
||||
{
|
||||
// When adding values using strongly typed objects, no leading/trailing LWS (whitespaces) are allowed.
|
||||
AssertFormatException("text", " token ");
|
||||
AssertFormatException("text", "token ");
|
||||
AssertFormatException("text", " token");
|
||||
AssertFormatException("text", "token string");
|
||||
AssertFormatException("text", "\"quoted string with \" quotes\"");
|
||||
AssertFormatException("text", "\"quoted string with \"two\" quotes\"");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Ctor_ValueValidFormat_SuccessfullyCreated()
|
||||
{
|
||||
CheckValue(null);
|
||||
CheckValue(string.Empty);
|
||||
CheckValue("token_string");
|
||||
CheckValue("\"quoted string\"");
|
||||
CheckValue("\"quoted string with quoted \\\" quote-pair\"");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Value_CallSetterWithInvalidValues_Throw()
|
||||
{
|
||||
// Just verify that the setter calls the same validation the ctor invokes.
|
||||
Assert.Throws<FormatException>(() => { var x = new NameValueHeaderValue("name"); x.Value = " x "; });
|
||||
Assert.Throws<FormatException>(() => { var x = new NameValueHeaderValue("name"); x.Value = "x y"; });
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ToString_UseNoValueAndTokenAndQuotedStringValues_SerializedCorrectly()
|
||||
{
|
||||
var nameValue = new NameValueHeaderValue("text", "token");
|
||||
Assert.Equal("text=token", nameValue.ToString());
|
||||
|
||||
nameValue.Value = "\"quoted string\"";
|
||||
Assert.Equal("text=\"quoted string\"", nameValue.ToString());
|
||||
|
||||
nameValue.Value = null;
|
||||
Assert.Equal("text", nameValue.ToString());
|
||||
|
||||
nameValue.Value = string.Empty;
|
||||
Assert.Equal("text", nameValue.ToString());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetHashCode_ValuesUseDifferentValues_HashDiffersAccordingToRfc()
|
||||
{
|
||||
var nameValue1 = new NameValueHeaderValue("text");
|
||||
var nameValue2 = new NameValueHeaderValue("text");
|
||||
|
||||
nameValue1.Value = null;
|
||||
nameValue2.Value = null;
|
||||
Assert.Equal(nameValue1.GetHashCode(), nameValue2.GetHashCode());
|
||||
|
||||
nameValue1.Value = "token";
|
||||
nameValue2.Value = null;
|
||||
Assert.NotEqual(nameValue1.GetHashCode(), nameValue2.GetHashCode());
|
||||
|
||||
nameValue1.Value = "token";
|
||||
nameValue2.Value = string.Empty;
|
||||
Assert.NotEqual(nameValue1.GetHashCode(), nameValue2.GetHashCode());
|
||||
|
||||
nameValue1.Value = null;
|
||||
nameValue2.Value = string.Empty;
|
||||
Assert.Equal(nameValue1.GetHashCode(), nameValue2.GetHashCode());
|
||||
|
||||
nameValue1.Value = "token";
|
||||
nameValue2.Value = "TOKEN";
|
||||
Assert.Equal(nameValue1.GetHashCode(), nameValue2.GetHashCode());
|
||||
|
||||
nameValue1.Value = "token";
|
||||
nameValue2.Value = "token";
|
||||
Assert.Equal(nameValue1.GetHashCode(), nameValue2.GetHashCode());
|
||||
|
||||
nameValue1.Value = "\"quoted string\"";
|
||||
nameValue2.Value = "\"QUOTED STRING\"";
|
||||
Assert.NotEqual(nameValue1.GetHashCode(), nameValue2.GetHashCode());
|
||||
|
||||
nameValue1.Value = "\"quoted string\"";
|
||||
nameValue2.Value = "\"quoted string\"";
|
||||
Assert.Equal(nameValue1.GetHashCode(), nameValue2.GetHashCode());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetHashCode_NameUseDifferentCasing_HashDiffersAccordingToRfc()
|
||||
{
|
||||
var nameValue1 = new NameValueHeaderValue("text");
|
||||
var nameValue2 = new NameValueHeaderValue("TEXT");
|
||||
Assert.Equal(nameValue1.GetHashCode(), nameValue2.GetHashCode());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Equals_ValuesUseDifferentValues_ValuesAreEqualOrDifferentAccordingToRfc()
|
||||
{
|
||||
var nameValue1 = new NameValueHeaderValue("text");
|
||||
var nameValue2 = new NameValueHeaderValue("text");
|
||||
|
||||
nameValue1.Value = null;
|
||||
nameValue2.Value = null;
|
||||
Assert.True(nameValue1.Equals(nameValue2), "<null> vs. <null>.");
|
||||
|
||||
nameValue1.Value = "token";
|
||||
nameValue2.Value = null;
|
||||
Assert.False(nameValue1.Equals(nameValue2), "token vs. <null>.");
|
||||
|
||||
nameValue1.Value = null;
|
||||
nameValue2.Value = "token";
|
||||
Assert.False(nameValue1.Equals(nameValue2), "<null> vs. token.");
|
||||
|
||||
nameValue1.Value = string.Empty;
|
||||
nameValue2.Value = "token";
|
||||
Assert.False(nameValue1.Equals(nameValue2), "string.Empty vs. token.");
|
||||
|
||||
nameValue1.Value = null;
|
||||
nameValue2.Value = string.Empty;
|
||||
Assert.True(nameValue1.Equals(nameValue2), "<null> vs. string.Empty.");
|
||||
|
||||
nameValue1.Value = "token";
|
||||
nameValue2.Value = "TOKEN";
|
||||
Assert.True(nameValue1.Equals(nameValue2), "token vs. TOKEN.");
|
||||
|
||||
nameValue1.Value = "token";
|
||||
nameValue2.Value = "token";
|
||||
Assert.True(nameValue1.Equals(nameValue2), "token vs. token.");
|
||||
|
||||
nameValue1.Value = "\"quoted string\"";
|
||||
nameValue2.Value = "\"QUOTED STRING\"";
|
||||
Assert.False(nameValue1.Equals(nameValue2), "\"quoted string\" vs. \"QUOTED STRING\".");
|
||||
|
||||
nameValue1.Value = "\"quoted string\"";
|
||||
nameValue2.Value = "\"quoted string\"";
|
||||
Assert.True(nameValue1.Equals(nameValue2), "\"quoted string\" vs. \"quoted string\".");
|
||||
|
||||
Assert.False(nameValue1.Equals(null), "\"quoted string\" vs. <null>.");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Equals_NameUseDifferentCasing_ConsideredEqual()
|
||||
{
|
||||
var nameValue1 = new NameValueHeaderValue("text");
|
||||
var nameValue2 = new NameValueHeaderValue("TEXT");
|
||||
Assert.True(nameValue1.Equals(nameValue2), "text vs. TEXT.");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Parse_SetOfValidValueStrings_ParsedCorrectly()
|
||||
{
|
||||
CheckValidParse(" name = value ", new NameValueHeaderValue("name", "value"));
|
||||
CheckValidParse(" name", new NameValueHeaderValue("name"));
|
||||
CheckValidParse(" name ", new NameValueHeaderValue("name"));
|
||||
CheckValidParse(" name=\"value\"", new NameValueHeaderValue("name", "\"value\""));
|
||||
CheckValidParse("name=value", new NameValueHeaderValue("name", "value"));
|
||||
CheckValidParse("name=\"quoted str\"", new NameValueHeaderValue("name", "\"quoted str\""));
|
||||
CheckValidParse("name\t =va1ue", new NameValueHeaderValue("name", "va1ue"));
|
||||
CheckValidParse("name= va*ue ", new NameValueHeaderValue("name", "va*ue"));
|
||||
CheckValidParse("name=", new NameValueHeaderValue("name", ""));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Parse_SetOfInvalidValueStrings_Throws()
|
||||
{
|
||||
CheckInvalidParse("name[value");
|
||||
CheckInvalidParse("name=value=");
|
||||
CheckInvalidParse("name=会");
|
||||
CheckInvalidParse("name==value");
|
||||
CheckInvalidParse("name= va:ue");
|
||||
CheckInvalidParse("=value");
|
||||
CheckInvalidParse("name value");
|
||||
CheckInvalidParse("name=,value");
|
||||
CheckInvalidParse("会");
|
||||
CheckInvalidParse(null);
|
||||
CheckInvalidParse(string.Empty);
|
||||
CheckInvalidParse(" ");
|
||||
CheckInvalidParse(" ,,");
|
||||
CheckInvalidParse(" , , name = value , ");
|
||||
CheckInvalidParse(" name,");
|
||||
CheckInvalidParse(" ,name=\"value\"");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TryParse_SetOfValidValueStrings_ParsedCorrectly()
|
||||
{
|
||||
CheckValidTryParse(" name = value ", new NameValueHeaderValue("name", "value"));
|
||||
CheckValidTryParse(" name", new NameValueHeaderValue("name"));
|
||||
CheckValidTryParse(" name=\"value\"", new NameValueHeaderValue("name", "\"value\""));
|
||||
CheckValidTryParse("name=value", new NameValueHeaderValue("name", "value"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TryParse_SetOfInvalidValueStrings_ReturnsFalse()
|
||||
{
|
||||
CheckInvalidTryParse("name[value");
|
||||
CheckInvalidTryParse("name=value=");
|
||||
CheckInvalidTryParse("name=会");
|
||||
CheckInvalidTryParse("name==value");
|
||||
CheckInvalidTryParse("=value");
|
||||
CheckInvalidTryParse("name value");
|
||||
CheckInvalidTryParse("name=,value");
|
||||
CheckInvalidTryParse("会");
|
||||
CheckInvalidTryParse(null);
|
||||
CheckInvalidTryParse(string.Empty);
|
||||
CheckInvalidTryParse(" ");
|
||||
CheckInvalidTryParse(" ,,");
|
||||
CheckInvalidTryParse(" , , name = value , ");
|
||||
CheckInvalidTryParse(" name,");
|
||||
CheckInvalidTryParse(" ,name=\"value\"");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ParseList_SetOfValidValueStrings_ParsedCorrectly()
|
||||
{
|
||||
var inputs = new[]
|
||||
{
|
||||
"",
|
||||
"name=value1",
|
||||
"",
|
||||
" name = value2 ",
|
||||
"\r\n name =value3\r\n ",
|
||||
"name=\"value 4\"",
|
||||
"name=\"value会5\"",
|
||||
"name=value6,name=value7",
|
||||
"name=\"value 8\", name= \"value 9\"",
|
||||
};
|
||||
var results = NameValueHeaderValue.ParseList(inputs);
|
||||
|
||||
var expectedResults = new[]
|
||||
{
|
||||
new NameValueHeaderValue("name", "value1"),
|
||||
new NameValueHeaderValue("name", "value2"),
|
||||
new NameValueHeaderValue("name", "value3"),
|
||||
new NameValueHeaderValue("name", "\"value 4\""),
|
||||
new NameValueHeaderValue("name", "\"value会5\""),
|
||||
new NameValueHeaderValue("name", "value6"),
|
||||
new NameValueHeaderValue("name", "value7"),
|
||||
new NameValueHeaderValue("name", "\"value 8\""),
|
||||
new NameValueHeaderValue("name", "\"value 9\""),
|
||||
}.ToList();
|
||||
|
||||
Assert.Equal(expectedResults, results);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TryParseList_SetOfValidValueStrings_ParsedCorrectly()
|
||||
{
|
||||
var inputs = new[]
|
||||
{
|
||||
"",
|
||||
"name=value1",
|
||||
"",
|
||||
" name = value2 ",
|
||||
"\r\n name =value3\r\n ",
|
||||
"name=\"value 4\"",
|
||||
"name=\"value会5\"",
|
||||
"name=value6,name=value7",
|
||||
"name=\"value 8\", name= \"value 9\"",
|
||||
};
|
||||
IList<NameValueHeaderValue> results;
|
||||
Assert.True(NameValueHeaderValue.TryParseList(inputs, out results));
|
||||
|
||||
var expectedResults = new[]
|
||||
{
|
||||
new NameValueHeaderValue("name", "value1"),
|
||||
new NameValueHeaderValue("name", "value2"),
|
||||
new NameValueHeaderValue("name", "value3"),
|
||||
new NameValueHeaderValue("name", "\"value 4\""),
|
||||
new NameValueHeaderValue("name", "\"value会5\""),
|
||||
new NameValueHeaderValue("name", "value6"),
|
||||
new NameValueHeaderValue("name", "value7"),
|
||||
new NameValueHeaderValue("name", "\"value 8\""),
|
||||
new NameValueHeaderValue("name", "\"value 9\""),
|
||||
}.ToList();
|
||||
|
||||
Assert.Equal(expectedResults, results);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ParseList_WithSomeInvlaidValues_Throws()
|
||||
{
|
||||
var inputs = new[]
|
||||
{
|
||||
"",
|
||||
"name1=value1",
|
||||
"name2",
|
||||
" name3 = 3, value a",
|
||||
"name4 =value4, name5 = value5 b",
|
||||
"name6=\"value 6",
|
||||
"name7=\"value会7\"",
|
||||
"name8=value8,name9=value9",
|
||||
"name10=\"value 10\", name11= \"value 11\"",
|
||||
};
|
||||
Assert.Throws<FormatException>(() => NameValueHeaderValue.ParseList(inputs));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TryParseList_WithSomeInvlaidValues_ReturnsFalse()
|
||||
{
|
||||
var inputs = new[]
|
||||
{
|
||||
"",
|
||||
"name1=value1",
|
||||
"name2",
|
||||
" name3 = 3, value a",
|
||||
"name4 =value4, name5 = value5 b",
|
||||
"name6=\"value 6",
|
||||
"name7=\"value会7\"",
|
||||
"name8=value8,name9=value9",
|
||||
"name10=\"value 10\", name11= \"value 11\"",
|
||||
};
|
||||
IList<NameValueHeaderValue> results;
|
||||
Assert.False(NameValueHeaderValue.TryParseList(inputs, out results));
|
||||
}
|
||||
|
||||
#region Helper methods
|
||||
|
||||
private void CheckValidParse(string input, NameValueHeaderValue expectedResult)
|
||||
{
|
||||
var result = NameValueHeaderValue.Parse(input);
|
||||
Assert.Equal(expectedResult, result);
|
||||
}
|
||||
|
||||
private void CheckInvalidParse(string input)
|
||||
{
|
||||
Assert.Throws<FormatException>(() => NameValueHeaderValue.Parse(input));
|
||||
}
|
||||
|
||||
private void CheckValidTryParse(string input, NameValueHeaderValue expectedResult)
|
||||
{
|
||||
NameValueHeaderValue result = null;
|
||||
Assert.True(NameValueHeaderValue.TryParse(input, out result));
|
||||
Assert.Equal(expectedResult, result);
|
||||
}
|
||||
|
||||
private void CheckInvalidTryParse(string input)
|
||||
{
|
||||
NameValueHeaderValue result = null;
|
||||
Assert.False(NameValueHeaderValue.TryParse(input, out result));
|
||||
Assert.Null(result);
|
||||
}
|
||||
|
||||
private static void CheckValue(string value)
|
||||
{
|
||||
var nameValue = new NameValueHeaderValue("text", value);
|
||||
Assert.Equal(value, nameValue.Value);
|
||||
}
|
||||
|
||||
private static void AssertFormatException(string name, string value)
|
||||
{
|
||||
Assert.Throws<FormatException>(() => new NameValueHeaderValue(name, value));
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,174 @@
|
|||
// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved.
|
||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
using System;
|
||||
using Xunit;
|
||||
|
||||
namespace Microsoft.Net.Http.Headers
|
||||
{
|
||||
public class RangeConditionHeaderValueTest
|
||||
{
|
||||
[Fact]
|
||||
public void Ctor_EntityTagOverload_MatchExpectation()
|
||||
{
|
||||
var rangeCondition = new RangeConditionHeaderValue(new EntityTagHeaderValue("\"x\""));
|
||||
Assert.Equal(new EntityTagHeaderValue("\"x\""), rangeCondition.EntityTag);
|
||||
Assert.Null(rangeCondition.LastModified);
|
||||
|
||||
EntityTagHeaderValue input = null;
|
||||
Assert.Throws<ArgumentNullException>(() => new RangeConditionHeaderValue(input));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Ctor_EntityTagStringOverload_MatchExpectation()
|
||||
{
|
||||
var rangeCondition = new RangeConditionHeaderValue("\"y\"");
|
||||
Assert.Equal(new EntityTagHeaderValue("\"y\""), rangeCondition.EntityTag);
|
||||
Assert.Null(rangeCondition.LastModified);
|
||||
|
||||
Assert.Throws<ArgumentException>(() => new RangeConditionHeaderValue((string)null));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Ctor_DateOverload_MatchExpectation()
|
||||
{
|
||||
var rangeCondition = new RangeConditionHeaderValue(
|
||||
new DateTimeOffset(2010, 7, 15, 12, 33, 57, TimeSpan.Zero));
|
||||
Assert.Null(rangeCondition.EntityTag);
|
||||
Assert.Equal(new DateTimeOffset(2010, 7, 15, 12, 33, 57, TimeSpan.Zero), rangeCondition.LastModified);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ToString_UseDifferentrangeConditions_AllSerializedCorrectly()
|
||||
{
|
||||
var rangeCondition = new RangeConditionHeaderValue(new EntityTagHeaderValue("\"x\""));
|
||||
Assert.Equal("\"x\"", rangeCondition.ToString());
|
||||
|
||||
rangeCondition = new RangeConditionHeaderValue(new DateTimeOffset(2010, 7, 15, 12, 33, 57, TimeSpan.Zero));
|
||||
Assert.Equal("Thu, 15 Jul 2010 12:33:57 GMT", rangeCondition.ToString());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetHashCode_UseSameAndDifferentrangeConditions_SameOrDifferentHashCodes()
|
||||
{
|
||||
var rangeCondition1 = new RangeConditionHeaderValue("\"x\"");
|
||||
var rangeCondition2 = new RangeConditionHeaderValue(new EntityTagHeaderValue("\"x\""));
|
||||
var rangeCondition3 = new RangeConditionHeaderValue(
|
||||
new DateTimeOffset(2010, 7, 15, 12, 33, 57, TimeSpan.Zero));
|
||||
var rangeCondition4 = new RangeConditionHeaderValue(
|
||||
new DateTimeOffset(2008, 8, 16, 13, 44, 10, TimeSpan.Zero));
|
||||
var rangeCondition5 = new RangeConditionHeaderValue(
|
||||
new DateTimeOffset(2010, 7, 15, 12, 33, 57, TimeSpan.Zero));
|
||||
var rangeCondition6 = new RangeConditionHeaderValue(
|
||||
new EntityTagHeaderValue("\"x\"", true));
|
||||
|
||||
Assert.Equal(rangeCondition1.GetHashCode(), rangeCondition2.GetHashCode());
|
||||
Assert.NotEqual(rangeCondition1.GetHashCode(), rangeCondition3.GetHashCode());
|
||||
Assert.NotEqual(rangeCondition3.GetHashCode(), rangeCondition4.GetHashCode());
|
||||
Assert.Equal(rangeCondition3.GetHashCode(), rangeCondition5.GetHashCode());
|
||||
Assert.NotEqual(rangeCondition1.GetHashCode(), rangeCondition6.GetHashCode());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Equals_UseSameAndDifferentRanges_EqualOrNotEqualNoExceptions()
|
||||
{
|
||||
var rangeCondition1 = new RangeConditionHeaderValue("\"x\"");
|
||||
var rangeCondition2 = new RangeConditionHeaderValue(new EntityTagHeaderValue("\"x\""));
|
||||
var rangeCondition3 = new RangeConditionHeaderValue(
|
||||
new DateTimeOffset(2010, 7, 15, 12, 33, 57, TimeSpan.Zero));
|
||||
var rangeCondition4 = new RangeConditionHeaderValue(
|
||||
new DateTimeOffset(2008, 8, 16, 13, 44, 10, TimeSpan.Zero));
|
||||
var rangeCondition5 = new RangeConditionHeaderValue(
|
||||
new DateTimeOffset(2010, 7, 15, 12, 33, 57, TimeSpan.Zero));
|
||||
var rangeCondition6 = new RangeConditionHeaderValue(
|
||||
new EntityTagHeaderValue("\"x\"", true));
|
||||
|
||||
Assert.False(rangeCondition1.Equals(null), "\"x\" vs. <null>");
|
||||
Assert.True(rangeCondition1.Equals(rangeCondition2), "\"x\" vs. \"x\"");
|
||||
Assert.False(rangeCondition1.Equals(rangeCondition3), "\"x\" vs. date");
|
||||
Assert.False(rangeCondition3.Equals(rangeCondition1), "date vs. \"x\"");
|
||||
Assert.False(rangeCondition3.Equals(rangeCondition4), "date vs. different date");
|
||||
Assert.True(rangeCondition3.Equals(rangeCondition5), "date vs. date");
|
||||
Assert.False(rangeCondition1.Equals(rangeCondition6), "\"x\" vs. W/\"x\"");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Parse_SetOfValidValueStrings_ParsedCorrectly()
|
||||
{
|
||||
CheckValidParse(" \"x\" ", new RangeConditionHeaderValue("\"x\""));
|
||||
CheckValidParse(" Sun, 06 Nov 1994 08:49:37 GMT ",
|
||||
new RangeConditionHeaderValue(new DateTimeOffset(1994, 11, 6, 8, 49, 37, TimeSpan.Zero)));
|
||||
CheckValidParse("Wed, 09 Nov 1994 08:49:37 GMT",
|
||||
new RangeConditionHeaderValue(new DateTimeOffset(1994, 11, 9, 8, 49, 37, TimeSpan.Zero)));
|
||||
CheckValidParse(" W/ \"tag\" ", new RangeConditionHeaderValue(new EntityTagHeaderValue("\"tag\"", true)));
|
||||
CheckValidParse(" w/\"tag\"", new RangeConditionHeaderValue(new EntityTagHeaderValue("\"tag\"", true)));
|
||||
CheckValidParse("\"tag\"", new RangeConditionHeaderValue(new EntityTagHeaderValue("\"tag\"")));
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("\"x\" ,")] // no delimiter allowed
|
||||
[InlineData("Sun, 06 Nov 1994 08:49:37 GMT ,")] // no delimiter allowed
|
||||
[InlineData("\"x\" Sun, 06 Nov 1994 08:49:37 GMT")]
|
||||
[InlineData("Sun, 06 Nov 1994 08:49:37 GMT \"x\"")]
|
||||
[InlineData(null)]
|
||||
[InlineData("")]
|
||||
[InlineData(" Wed 09 Nov 1994 08:49:37 GMT")]
|
||||
[InlineData("\"x")]
|
||||
[InlineData("Wed, 09 Nov")]
|
||||
[InlineData("W/Wed 09 Nov 1994 08:49:37 GMT")]
|
||||
[InlineData("\"x\",")]
|
||||
[InlineData("Wed 09 Nov 1994 08:49:37 GMT,")]
|
||||
public void Parse_SetOfInvalidValueStrings_Throws(string input)
|
||||
{
|
||||
Assert.Throws<FormatException>(() => RangeConditionHeaderValue.Parse(input));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TryParse_SetOfValidValueStrings_ParsedCorrectly()
|
||||
{
|
||||
CheckValidTryParse(" \"x\" ", new RangeConditionHeaderValue("\"x\""));
|
||||
CheckValidTryParse(" Sun, 06 Nov 1994 08:49:37 GMT ",
|
||||
new RangeConditionHeaderValue(new DateTimeOffset(1994, 11, 6, 8, 49, 37, TimeSpan.Zero)));
|
||||
CheckValidTryParse(" W/ \"tag\" ", new RangeConditionHeaderValue(new EntityTagHeaderValue("\"tag\"", true)));
|
||||
CheckValidTryParse(" w/\"tag\"", new RangeConditionHeaderValue(new EntityTagHeaderValue("\"tag\"", true)));
|
||||
CheckValidTryParse("\"tag\"", new RangeConditionHeaderValue(new EntityTagHeaderValue("\"tag\"")));
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("\"x\" ,")] // no delimiter allowed
|
||||
[InlineData("Sun, 06 Nov 1994 08:49:37 GMT ,")] // no delimiter allowed
|
||||
[InlineData("\"x\" Sun, 06 Nov 1994 08:49:37 GMT")]
|
||||
[InlineData("Sun, 06 Nov 1994 08:49:37 GMT \"x\"")]
|
||||
[InlineData(null)]
|
||||
[InlineData("")]
|
||||
[InlineData(" Wed 09 Nov 1994 08:49:37 GMT")]
|
||||
[InlineData("\"x")]
|
||||
[InlineData("Wed, 09 Nov")]
|
||||
[InlineData("W/Wed 09 Nov 1994 08:49:37 GMT")]
|
||||
[InlineData("\"x\",")]
|
||||
[InlineData("Wed 09 Nov 1994 08:49:37 GMT,")]
|
||||
public void TryParse_SetOfInvalidValueStrings_ReturnsFalse(string input)
|
||||
{
|
||||
RangeConditionHeaderValue result = null;
|
||||
Assert.False(RangeConditionHeaderValue.TryParse(input, out result));
|
||||
Assert.Null(result);
|
||||
}
|
||||
|
||||
#region Helper methods
|
||||
|
||||
private void CheckValidParse(string input, RangeConditionHeaderValue expectedResult)
|
||||
{
|
||||
var result = RangeConditionHeaderValue.Parse(input);
|
||||
Assert.Equal(expectedResult, result);
|
||||
}
|
||||
|
||||
private void CheckValidTryParse(string input, RangeConditionHeaderValue expectedResult)
|
||||
{
|
||||
RangeConditionHeaderValue result = null;
|
||||
Assert.True(RangeConditionHeaderValue.TryParse(input, out result));
|
||||
Assert.Equal(expectedResult, result);
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,183 @@
|
|||
// Copyright (c) Microsoft Open Technologies, Inc. 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.Linq;
|
||||
using Xunit;
|
||||
|
||||
namespace Microsoft.Net.Http.Headers
|
||||
{
|
||||
public class RangeHeaderValueTest
|
||||
{
|
||||
[Fact]
|
||||
public void Ctor_InvalidRange_Throw()
|
||||
{
|
||||
Assert.Throws<ArgumentOutOfRangeException>(() => new RangeHeaderValue(5, 2));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Unit_GetAndSetValidAndInvalidValues_MatchExpectation()
|
||||
{
|
||||
var range = new RangeHeaderValue();
|
||||
range.Unit = "myunit";
|
||||
Assert.Equal("myunit", range.Unit);
|
||||
|
||||
Assert.Throws<ArgumentException>(() => range.Unit = null);
|
||||
Assert.Throws<ArgumentException>(() => range.Unit = "");
|
||||
Assert.Throws<FormatException>(() => range.Unit = " x");
|
||||
Assert.Throws<FormatException>(() => range.Unit = "x ");
|
||||
Assert.Throws<FormatException>(() => range.Unit = "x y");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ToString_UseDifferentRanges_AllSerializedCorrectly()
|
||||
{
|
||||
var range = new RangeHeaderValue();
|
||||
range.Unit = "myunit";
|
||||
range.Ranges.Add(new RangeItemHeaderValue(1, 3));
|
||||
Assert.Equal("myunit=1-3", range.ToString());
|
||||
|
||||
range.Ranges.Add(new RangeItemHeaderValue(5, null));
|
||||
range.Ranges.Add(new RangeItemHeaderValue(null, 17));
|
||||
Assert.Equal("myunit=1-3, 5-, -17", range.ToString());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetHashCode_UseSameAndDifferentRanges_SameOrDifferentHashCodes()
|
||||
{
|
||||
var range1 = new RangeHeaderValue(1, 2);
|
||||
var range2 = new RangeHeaderValue(1, 2);
|
||||
range2.Unit = "BYTES";
|
||||
var range3 = new RangeHeaderValue(1, null);
|
||||
var range4 = new RangeHeaderValue(null, 2);
|
||||
var range5 = new RangeHeaderValue();
|
||||
range5.Ranges.Add(new RangeItemHeaderValue(1, 2));
|
||||
range5.Ranges.Add(new RangeItemHeaderValue(3, 4));
|
||||
var range6 = new RangeHeaderValue();
|
||||
range6.Ranges.Add(new RangeItemHeaderValue(3, 4)); // reverse order of range5
|
||||
range6.Ranges.Add(new RangeItemHeaderValue(1, 2));
|
||||
|
||||
Assert.Equal(range1.GetHashCode(), range2.GetHashCode());
|
||||
Assert.NotEqual(range1.GetHashCode(), range3.GetHashCode());
|
||||
Assert.NotEqual(range1.GetHashCode(), range4.GetHashCode());
|
||||
Assert.NotEqual(range1.GetHashCode(), range5.GetHashCode());
|
||||
Assert.Equal(range5.GetHashCode(), range6.GetHashCode());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Equals_UseSameAndDifferentRanges_EqualOrNotEqualNoExceptions()
|
||||
{
|
||||
var range1 = new RangeHeaderValue(1, 2);
|
||||
var range2 = new RangeHeaderValue(1, 2);
|
||||
range2.Unit = "BYTES";
|
||||
var range3 = new RangeHeaderValue(1, null);
|
||||
var range4 = new RangeHeaderValue(null, 2);
|
||||
var range5 = new RangeHeaderValue();
|
||||
range5.Ranges.Add(new RangeItemHeaderValue(1, 2));
|
||||
range5.Ranges.Add(new RangeItemHeaderValue(3, 4));
|
||||
var range6 = new RangeHeaderValue();
|
||||
range6.Ranges.Add(new RangeItemHeaderValue(3, 4)); // reverse order of range5
|
||||
range6.Ranges.Add(new RangeItemHeaderValue(1, 2));
|
||||
var range7 = new RangeHeaderValue(1, 2);
|
||||
range7.Unit = "other";
|
||||
|
||||
Assert.False(range1.Equals(null), "bytes=1-2 vs. <null>");
|
||||
Assert.True(range1.Equals(range2), "bytes=1-2 vs. BYTES=1-2");
|
||||
Assert.False(range1.Equals(range3), "bytes=1-2 vs. bytes=1-");
|
||||
Assert.False(range1.Equals(range4), "bytes=1-2 vs. bytes=-2");
|
||||
Assert.False(range1.Equals(range5), "bytes=1-2 vs. bytes=1-2,3-4");
|
||||
Assert.True(range5.Equals(range6), "bytes=1-2,3-4 vs. bytes=3-4,1-2");
|
||||
Assert.False(range1.Equals(range7), "bytes=1-2 vs. other=1-2");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Parse_SetOfValidValueStrings_ParsedCorrectly()
|
||||
{
|
||||
CheckValidParse(" bytes=1-2 ", new RangeHeaderValue(1, 2));
|
||||
|
||||
var expected = new RangeHeaderValue();
|
||||
expected.Unit = "custom";
|
||||
expected.Ranges.Add(new RangeItemHeaderValue(null, 5));
|
||||
expected.Ranges.Add(new RangeItemHeaderValue(1, 4));
|
||||
CheckValidParse("custom = - 5 , 1 - 4 ,,", expected);
|
||||
|
||||
expected = new RangeHeaderValue();
|
||||
expected.Unit = "custom";
|
||||
expected.Ranges.Add(new RangeItemHeaderValue(1, 2));
|
||||
CheckValidParse(" custom = 1 - 2", expected);
|
||||
|
||||
expected = new RangeHeaderValue();
|
||||
expected.Ranges.Add(new RangeItemHeaderValue(1, 2));
|
||||
expected.Ranges.Add(new RangeItemHeaderValue(3, null));
|
||||
expected.Ranges.Add(new RangeItemHeaderValue(null, 4));
|
||||
CheckValidParse("bytes =1-2,,3-, , ,-4,,", expected);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Parse_SetOfInvalidValueStrings_Throws()
|
||||
{
|
||||
CheckInvalidParse("bytes=1-2x"); // only delimiter ',' allowed after last range
|
||||
CheckInvalidParse("x bytes=1-2");
|
||||
CheckInvalidParse("bytes=1-2.4");
|
||||
CheckInvalidParse(null);
|
||||
CheckInvalidParse(string.Empty);
|
||||
|
||||
CheckInvalidParse("bytes=1");
|
||||
CheckInvalidParse("bytes=");
|
||||
CheckInvalidParse("bytes");
|
||||
CheckInvalidParse("bytes 1-2");
|
||||
CheckInvalidParse("bytes= ,,, , ,,");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TryParse_SetOfValidValueStrings_ParsedCorrectly()
|
||||
{
|
||||
CheckValidTryParse(" bytes=1-2 ", new RangeHeaderValue(1, 2));
|
||||
|
||||
var expected = new RangeHeaderValue();
|
||||
expected.Unit = "custom";
|
||||
expected.Ranges.Add(new RangeItemHeaderValue(null, 5));
|
||||
expected.Ranges.Add(new RangeItemHeaderValue(1, 4));
|
||||
CheckValidTryParse("custom = - 5 , 1 - 4 ,,", expected);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TryParse_SetOfInvalidValueStrings_ReturnsFalse()
|
||||
{
|
||||
CheckInvalidTryParse("bytes=1-2x"); // only delimiter ',' allowed after last range
|
||||
CheckInvalidTryParse("x bytes=1-2");
|
||||
CheckInvalidTryParse("bytes=1-2.4");
|
||||
CheckInvalidTryParse(null);
|
||||
CheckInvalidTryParse(string.Empty);
|
||||
}
|
||||
|
||||
#region Helper methods
|
||||
|
||||
private void CheckValidParse(string input, RangeHeaderValue expectedResult)
|
||||
{
|
||||
var result = RangeHeaderValue.Parse(input);
|
||||
Assert.Equal(expectedResult, result);
|
||||
}
|
||||
|
||||
private void CheckInvalidParse(string input)
|
||||
{
|
||||
Assert.Throws<FormatException>(() => RangeHeaderValue.Parse(input));
|
||||
}
|
||||
|
||||
private void CheckValidTryParse(string input, RangeHeaderValue expectedResult)
|
||||
{
|
||||
RangeHeaderValue result = null;
|
||||
Assert.True(RangeHeaderValue.TryParse(input, out result));
|
||||
Assert.Equal(expectedResult, result);
|
||||
}
|
||||
|
||||
private void CheckInvalidTryParse(string input)
|
||||
{
|
||||
RangeHeaderValue result = null;
|
||||
Assert.False(RangeHeaderValue.TryParse(input, out result));
|
||||
Assert.Null(result);
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,162 @@
|
|||
// Copyright (c) Microsoft Open Technologies, Inc. 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.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Xunit;
|
||||
|
||||
namespace Microsoft.Net.Http.Headers
|
||||
{
|
||||
public class RangeItemHeaderValueTest
|
||||
{
|
||||
[Fact]
|
||||
public void Ctor_BothValuesNull_Throw()
|
||||
{
|
||||
Assert.Throws<ArgumentException>(() => new RangeItemHeaderValue(null, null));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Ctor_FromValueNegative_Throw()
|
||||
{
|
||||
Assert.Throws<ArgumentOutOfRangeException>(() => new RangeItemHeaderValue(-1, null));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Ctor_FromGreaterThanToValue_Throw()
|
||||
{
|
||||
Assert.Throws<ArgumentOutOfRangeException>(() => new RangeItemHeaderValue(2, 1));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Ctor_ToValueNegative_Throw()
|
||||
{
|
||||
Assert.Throws<ArgumentOutOfRangeException>(() => new RangeItemHeaderValue(null, -1));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Ctor_ValidFormat_SuccessfullyCreated()
|
||||
{
|
||||
var rangeItem = new RangeItemHeaderValue(1, 2);
|
||||
Assert.Equal(1, rangeItem.From);
|
||||
Assert.Equal(2, rangeItem.To);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ToString_UseDifferentRangeItems_AllSerializedCorrectly()
|
||||
{
|
||||
// Make sure ToString() doesn't add any separators.
|
||||
var rangeItem = new RangeItemHeaderValue(1000000000, 2000000000);
|
||||
Assert.Equal("1000000000-2000000000", rangeItem.ToString());
|
||||
|
||||
rangeItem = new RangeItemHeaderValue(5, null);
|
||||
Assert.Equal("5-", rangeItem.ToString());
|
||||
|
||||
rangeItem = new RangeItemHeaderValue(null, 10);
|
||||
Assert.Equal("-10", rangeItem.ToString());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetHashCode_UseSameAndDifferentRangeItems_SameOrDifferentHashCodes()
|
||||
{
|
||||
var rangeItem1 = new RangeItemHeaderValue(1, 2);
|
||||
var rangeItem2 = new RangeItemHeaderValue(1, null);
|
||||
var rangeItem3 = new RangeItemHeaderValue(null, 2);
|
||||
var rangeItem4 = new RangeItemHeaderValue(2, 2);
|
||||
var rangeItem5 = new RangeItemHeaderValue(1, 2);
|
||||
|
||||
Assert.NotEqual(rangeItem1.GetHashCode(), rangeItem2.GetHashCode());
|
||||
Assert.NotEqual(rangeItem1.GetHashCode(), rangeItem3.GetHashCode());
|
||||
Assert.NotEqual(rangeItem1.GetHashCode(), rangeItem4.GetHashCode());
|
||||
Assert.Equal(rangeItem1.GetHashCode(), rangeItem5.GetHashCode());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Equals_UseSameAndDifferentRanges_EqualOrNotEqualNoExceptions()
|
||||
{
|
||||
var rangeItem1 = new RangeItemHeaderValue(1, 2);
|
||||
var rangeItem2 = new RangeItemHeaderValue(1, null);
|
||||
var rangeItem3 = new RangeItemHeaderValue(null, 2);
|
||||
var rangeItem4 = new RangeItemHeaderValue(2, 2);
|
||||
var rangeItem5 = new RangeItemHeaderValue(1, 2);
|
||||
|
||||
Assert.False(rangeItem1.Equals(rangeItem2), "1-2 vs. 1-.");
|
||||
Assert.False(rangeItem2.Equals(rangeItem1), "1- vs. 1-2.");
|
||||
Assert.False(rangeItem1.Equals(null), "1-2 vs. null.");
|
||||
Assert.False(rangeItem1.Equals(rangeItem3), "1-2 vs. -2.");
|
||||
Assert.False(rangeItem3.Equals(rangeItem1), "-2 vs. 1-2.");
|
||||
Assert.False(rangeItem1.Equals(rangeItem4), "1-2 vs. 2-2.");
|
||||
Assert.True(rangeItem1.Equals(rangeItem5), "1-2 vs. 1-2.");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TryParse_DifferentValidScenarios_AllReturnNonZero()
|
||||
{
|
||||
CheckValidTryParse("1-2", 1, 2);
|
||||
CheckValidTryParse(" 1-2", 1, 2);
|
||||
CheckValidTryParse("0-0", 0, 0);
|
||||
CheckValidTryParse(" 1-", 1, null);
|
||||
CheckValidTryParse(" -2", null, 2);
|
||||
|
||||
CheckValidTryParse(" 684684 - 123456789012345 ", 684684, 123456789012345);
|
||||
|
||||
// The separator doesn't matter. It only parses until the first non-whitespace
|
||||
CheckValidTryParse(" 1 - 2 ,", 1, 2);
|
||||
|
||||
CheckValidTryParse(",,1-2, 3 - , , -6 , ,,", new Tuple<long?, long?>(1, 2), new Tuple<long?, long?>(3, null),
|
||||
new Tuple<long?, long?>(null, 6));
|
||||
CheckValidTryParse("1-2,", new Tuple<long?, long?>(1, 2));
|
||||
CheckValidTryParse("1-", new Tuple<long?, long?>(1, null));
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(null)]
|
||||
[InlineData("")]
|
||||
[InlineData(",,")]
|
||||
[InlineData("1")]
|
||||
[InlineData("1-2,3")]
|
||||
[InlineData("1--2")]
|
||||
[InlineData("1,-2")]
|
||||
[InlineData("-")]
|
||||
[InlineData("--")]
|
||||
[InlineData("2-1")]
|
||||
[InlineData("12345678901234567890123-")] // >>Int64.MaxValue
|
||||
[InlineData("-12345678901234567890123")] // >>Int64.MaxValue
|
||||
[InlineData("9999999999999999999-")] // 19-digit numbers outside the Int64 range.
|
||||
[InlineData("-9999999999999999999")] // 19-digit numbers outside the Int64 range.
|
||||
public void TryParse_DifferentInvalidScenarios_AllReturnFalse(string input)
|
||||
{
|
||||
RangeHeaderValue result;
|
||||
Assert.False(RangeHeaderValue.TryParse("byte=" + input, out result));
|
||||
}
|
||||
|
||||
private static void CheckValidTryParse(string input, long? expectedFrom, long? expectedTo)
|
||||
{
|
||||
RangeHeaderValue result;
|
||||
Assert.True(RangeHeaderValue.TryParse("byte=" + input, out result), input);
|
||||
|
||||
var ranges = result.Ranges.ToArray();
|
||||
Assert.Equal(1, ranges.Length);
|
||||
|
||||
var range = ranges.First();
|
||||
|
||||
Assert.Equal(expectedFrom, range.From);
|
||||
Assert.Equal(expectedTo, range.To);
|
||||
}
|
||||
|
||||
private static void CheckValidTryParse(string input, params Tuple<long?, long?>[] expectedRanges)
|
||||
{
|
||||
RangeHeaderValue result;
|
||||
Assert.True(RangeHeaderValue.TryParse("byte=" + input, out result), input);
|
||||
|
||||
var ranges = result.Ranges.ToArray();
|
||||
Assert.Equal(expectedRanges.Length, ranges.Length);
|
||||
|
||||
for (int i = 0; i < expectedRanges.Length; i++)
|
||||
{
|
||||
Assert.Equal(expectedRanges[i].Item1, ranges[i].From);
|
||||
Assert.Equal(expectedRanges[i].Item2, ranges[i].To);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,259 @@
|
|||
// Copyright (c) Microsoft Open Technologies, Inc. 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.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Xunit;
|
||||
|
||||
namespace Microsoft.Net.Http.Headers
|
||||
{
|
||||
public class SetCookieHeaderValueTest
|
||||
{
|
||||
public static TheoryData<SetCookieHeaderValue, string> SetCookieHeaderDataSet
|
||||
{
|
||||
get
|
||||
{
|
||||
var dataset = new TheoryData<SetCookieHeaderValue, string>();
|
||||
var header1 = new SetCookieHeaderValue("name1", "n1=v1&n2=v2&n3=v3")
|
||||
{
|
||||
Domain = "domain1",
|
||||
Expires = new DateTimeOffset(1994, 11, 6, 8, 49, 37, TimeSpan.Zero),
|
||||
HttpOnly = true,
|
||||
MaxAge = TimeSpan.FromDays(1),
|
||||
Path = "path1",
|
||||
Secure = true
|
||||
};
|
||||
dataset.Add(header1, "name1=n1=v1&n2=v2&n3=v3; expires=Sun, 06 Nov 1994 08:49:37 GMT; max-age=86400; domain=domain1; path=path1; secure; httponly");
|
||||
|
||||
var header2 = new SetCookieHeaderValue("name2", "");
|
||||
dataset.Add(header2, "name2=");
|
||||
|
||||
var header3 = new SetCookieHeaderValue("name2", "value2");
|
||||
dataset.Add(header3, "name2=value2");
|
||||
|
||||
var header4 = new SetCookieHeaderValue("name4", "value4")
|
||||
{
|
||||
MaxAge = TimeSpan.FromDays(1),
|
||||
};
|
||||
dataset.Add(header4, "name4=value4; max-age=86400");
|
||||
|
||||
var header5 = new SetCookieHeaderValue("name5", "value5")
|
||||
{
|
||||
Domain = "domain1",
|
||||
Expires = new DateTimeOffset(1994, 11, 6, 8, 49, 37, TimeSpan.Zero),
|
||||
};
|
||||
dataset.Add(header5, "name5=value5; expires=Sun, 06 Nov 1994 08:49:37 GMT; domain=domain1");
|
||||
|
||||
return dataset;
|
||||
}
|
||||
}
|
||||
|
||||
public static TheoryData<string> InvalidSetCookieHeaderDataSet
|
||||
{
|
||||
get
|
||||
{
|
||||
return new TheoryData<string>
|
||||
{
|
||||
"expires=Sun, 06 Nov 1994 08:49:37 GMT; max-age=86400; domain=domain1",
|
||||
"name=value; expires=Sun, 06 Nov 1994 08:49:37 ZZZ; max-age=86400; domain=domain1",
|
||||
"name=value; expires=Sun, 06 Nov 1994 08:49:37 GMT; max-age=-86400; domain=domain1",
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
public static TheoryData<string> InvalidCookieNames
|
||||
{
|
||||
get
|
||||
{
|
||||
return new TheoryData<string>
|
||||
{
|
||||
"<acb>",
|
||||
"{acb}",
|
||||
"[acb]",
|
||||
"\"acb\"",
|
||||
"a,b",
|
||||
"a;b",
|
||||
"a\\b",
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
public static TheoryData<string> InvalidCookieValues
|
||||
{
|
||||
get
|
||||
{
|
||||
return new TheoryData<string>
|
||||
{
|
||||
{ "\"" },
|
||||
{ "a,b" },
|
||||
{ "a;b" },
|
||||
{ "a\\b" },
|
||||
{ "\"abc" },
|
||||
{ "a\"bc" },
|
||||
{ "abc\"" },
|
||||
};
|
||||
}
|
||||
}
|
||||
public static TheoryData<IList<SetCookieHeaderValue>, string[]> ListOfSetCookieHeaderDataSet
|
||||
{
|
||||
get
|
||||
{
|
||||
var dataset = new TheoryData<IList<SetCookieHeaderValue>, string[]>();
|
||||
var header1 = new SetCookieHeaderValue("name1", "n1=v1&n2=v2&n3=v3")
|
||||
{
|
||||
Domain = "domain1",
|
||||
Expires = new DateTimeOffset(1994, 11, 6, 8, 49, 37, TimeSpan.Zero),
|
||||
HttpOnly = true,
|
||||
MaxAge = TimeSpan.FromDays(1),
|
||||
Path = "path1",
|
||||
Secure = true
|
||||
};
|
||||
var string1 = "name1=n1=v1&n2=v2&n3=v3; expires=Sun, 06 Nov 1994 08:49:37 GMT; max-age=86400; domain=domain1; path=path1; secure; httponly";
|
||||
|
||||
var header2 = new SetCookieHeaderValue("name2", "value2");
|
||||
var string2 = "name2=value2";
|
||||
|
||||
var header3 = new SetCookieHeaderValue("name3", "value3")
|
||||
{
|
||||
MaxAge = TimeSpan.FromDays(1),
|
||||
};
|
||||
var string3 = "name3=value3; max-age=86400";
|
||||
|
||||
var header4 = new SetCookieHeaderValue("name4", "value4")
|
||||
{
|
||||
Domain = "domain1",
|
||||
Expires = new DateTimeOffset(1994, 11, 6, 8, 49, 37, TimeSpan.Zero),
|
||||
};
|
||||
var string4 = "name4=value4; expires=Sun, 06 Nov 1994 08:49:37 GMT; domain=domain1";
|
||||
|
||||
dataset.Add(new[] { header1 }.ToList(), new[] { string1 });
|
||||
dataset.Add(new[] { header1, header1 }.ToList(), new[] { string1, string1 });
|
||||
dataset.Add(new[] { header1, header1 }.ToList(), new[] { string1, null, "", " ", ",", " , ", string1 });
|
||||
dataset.Add(new[] { header2 }.ToList(), new[] { string2 });
|
||||
dataset.Add(new[] { header1, header2 }.ToList(), new[] { string1, string2 });
|
||||
dataset.Add(new[] { header1, header2 }.ToList(), new[] { string1 + ", " + string2 });
|
||||
dataset.Add(new[] { header2, header1 }.ToList(), new[] { string2 + ", " + string1 });
|
||||
dataset.Add(new[] { header1, header2, header3, header4 }.ToList(), new[] { string1, string2, string3, string4 });
|
||||
dataset.Add(new[] { header1, header2, header3, header4 }.ToList(), new[] { string.Join(",", string1, string2, string3, string4) });
|
||||
|
||||
return dataset;
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: [Fact]
|
||||
public void SetCookieHeaderValue_CtorThrowsOnNullName()
|
||||
{
|
||||
Assert.Throws<ArgumentNullException>(() => new SetCookieHeaderValue(null, "value"));
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[MemberData(nameof(InvalidCookieNames))]
|
||||
public void SetCookieHeaderValue_CtorThrowsOnInvalidName(string name)
|
||||
{
|
||||
Assert.Throws<ArgumentException>(() => new SetCookieHeaderValue(name, "value"));
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[MemberData(nameof(InvalidCookieValues))]
|
||||
public void SetCookieHeaderValue_CtorThrowsOnInvalidValue(string value)
|
||||
{
|
||||
Assert.Throws<ArgumentException>(() => new SetCookieHeaderValue("name", value));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SetCookieHeaderValue_Ctor1_InitializesCorrectly()
|
||||
{
|
||||
var header = new SetCookieHeaderValue("cookie");
|
||||
Assert.Equal("cookie", header.Name);
|
||||
Assert.Equal(string.Empty, header.Value);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("name", "")]
|
||||
[InlineData("name", "value")]
|
||||
[InlineData("name", "\"acb\"")]
|
||||
public void SetCookieHeaderValue_Ctor2InitializesCorrectly(string name, string value)
|
||||
{
|
||||
var header = new SetCookieHeaderValue(name, value);
|
||||
Assert.Equal(name, header.Name);
|
||||
Assert.Equal(value, header.Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SetCookieHeaderValue_Value()
|
||||
{
|
||||
var cookie = new SetCookieHeaderValue("name");
|
||||
Assert.Equal(String.Empty, cookie.Value);
|
||||
|
||||
cookie.Value = "value1";
|
||||
Assert.Equal("value1", cookie.Value);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[MemberData(nameof(SetCookieHeaderDataSet))]
|
||||
public void SetCookieHeaderValue_ToString(SetCookieHeaderValue input, string expectedValue)
|
||||
{
|
||||
Assert.Equal(expectedValue, input.ToString());
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[MemberData(nameof(SetCookieHeaderDataSet))]
|
||||
public void SetCookieHeaderValue_Parse_AcceptsValidValues(SetCookieHeaderValue cookie, string expectedValue)
|
||||
{
|
||||
var header = SetCookieHeaderValue.Parse(expectedValue);
|
||||
|
||||
Assert.Equal(cookie, header);
|
||||
Assert.Equal(expectedValue, header.ToString());
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[MemberData(nameof(SetCookieHeaderDataSet))]
|
||||
public void SetCookieHeaderValue_TryParse_AcceptsValidValues(SetCookieHeaderValue cookie, string expectedValue)
|
||||
{
|
||||
SetCookieHeaderValue header;
|
||||
bool result = SetCookieHeaderValue.TryParse(expectedValue, out header);
|
||||
Assert.True(result);
|
||||
|
||||
Assert.Equal(cookie, header);
|
||||
Assert.Equal(expectedValue, header.ToString());
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[MemberData(nameof(InvalidSetCookieHeaderDataSet))]
|
||||
public void SetCookieHeaderValue_Parse_RejectsInvalidValues(string value)
|
||||
{
|
||||
Assert.Throws<FormatException>(() => SetCookieHeaderValue.Parse(value));
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[MemberData(nameof(InvalidSetCookieHeaderDataSet))]
|
||||
public void SetCookieHeaderValue_TryParse_RejectsInvalidValues(string value)
|
||||
{
|
||||
SetCookieHeaderValue header;
|
||||
bool result = SetCookieHeaderValue.TryParse(value, out header);
|
||||
|
||||
Assert.False(result);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[MemberData(nameof(ListOfSetCookieHeaderDataSet))]
|
||||
public void SetCookieHeaderValue_ParseList_AcceptsValidValues(IList<SetCookieHeaderValue> cookies, string[] input)
|
||||
{
|
||||
var results = SetCookieHeaderValue.ParseList(input);
|
||||
|
||||
Assert.Equal(cookies, results);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[MemberData(nameof(ListOfSetCookieHeaderDataSet))]
|
||||
public void SetCookieHeaderValue_TryParseList_AcceptsValidValues(IList<SetCookieHeaderValue> cookies, string[] input)
|
||||
{
|
||||
IList<SetCookieHeaderValue> results;
|
||||
bool result = SetCookieHeaderValue.TryParseList(input, out results);
|
||||
Assert.True(result);
|
||||
|
||||
Assert.Equal(cookies, results);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,64 @@
|
|||
// Copyright (c) Microsoft Open Technologies, Inc. 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.Linq;
|
||||
using Xunit;
|
||||
|
||||
namespace Microsoft.Net.Http.Headers
|
||||
{
|
||||
public class StringWithQualityHeaderValueComparerTest
|
||||
{
|
||||
public static TheoryData<string[], string[]> StringWithQualityHeaderValueComparerTestsBeforeAfterSortedValues
|
||||
{
|
||||
get
|
||||
{
|
||||
return new TheoryData<string[], string[]>
|
||||
{
|
||||
{
|
||||
new string[]
|
||||
{
|
||||
"text",
|
||||
"text;q=1.0",
|
||||
"text",
|
||||
"text;q=0",
|
||||
"*;q=0.8",
|
||||
"*;q=1",
|
||||
"text;q=0.8",
|
||||
"*;q=0.6",
|
||||
"text;q=1.0",
|
||||
"*;q=0.4",
|
||||
"text;q=0.6",
|
||||
},
|
||||
new string[]
|
||||
{
|
||||
"text",
|
||||
"text;q=1.0",
|
||||
"text",
|
||||
"text;q=1.0",
|
||||
"*;q=1",
|
||||
"text;q=0.8",
|
||||
"*;q=0.8",
|
||||
"text;q=0.6",
|
||||
"*;q=0.6",
|
||||
"*;q=0.4",
|
||||
"text;q=0",
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[MemberData(nameof(StringWithQualityHeaderValueComparerTestsBeforeAfterSortedValues))]
|
||||
public void SortStringWithQualityHeaderValuesByQFactor_SortsCorrectly(IEnumerable<string> unsorted, IEnumerable<string> expectedSorted)
|
||||
{
|
||||
var unsortedValues = StringWithQualityHeaderValue.ParseList(unsorted.ToList());
|
||||
var expectedSortedValues = StringWithQualityHeaderValue.ParseList(expectedSorted.ToList());
|
||||
|
||||
var actualSorted = unsortedValues.OrderByDescending(k => k, StringWithQualityHeaderValueComparer.QualityComparer).ToList();
|
||||
|
||||
Assert.True(expectedSortedValues.SequenceEqual(actualSorted));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,339 @@
|
|||
// Copyright (c) Microsoft Open Technologies, Inc. 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.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Xunit;
|
||||
|
||||
namespace Microsoft.Net.Http.Headers
|
||||
{
|
||||
public class StringWithQualityHeaderValueTest
|
||||
{
|
||||
[Fact]
|
||||
public void Ctor_StringOnlyOverload_MatchExpectation()
|
||||
{
|
||||
var value = new StringWithQualityHeaderValue("token");
|
||||
Assert.Equal("token", value.Value);
|
||||
Assert.Null(value.Quality);
|
||||
|
||||
Assert.Throws<ArgumentException>(() => new StringWithQualityHeaderValue(null));
|
||||
Assert.Throws<ArgumentException>(() => new StringWithQualityHeaderValue(""));
|
||||
Assert.Throws<FormatException>(() => new StringWithQualityHeaderValue("in valid"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Ctor_StringWithQualityOverload_MatchExpectation()
|
||||
{
|
||||
var value = new StringWithQualityHeaderValue("token", 0.5);
|
||||
Assert.Equal("token", value.Value);
|
||||
Assert.Equal(0.5, value.Quality);
|
||||
|
||||
Assert.Throws<ArgumentException>(() => new StringWithQualityHeaderValue(null, 0.1));
|
||||
Assert.Throws<ArgumentException>(() => new StringWithQualityHeaderValue("", 0.1));
|
||||
Assert.Throws<FormatException>(() => new StringWithQualityHeaderValue("in valid", 0.1));
|
||||
|
||||
Assert.Throws<ArgumentOutOfRangeException>(() => new StringWithQualityHeaderValue("t", 1.1));
|
||||
Assert.Throws<ArgumentOutOfRangeException>(() => new StringWithQualityHeaderValue("t", -0.1));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ToString_UseDifferentValues_AllSerializedCorrectly()
|
||||
{
|
||||
var value = new StringWithQualityHeaderValue("token");
|
||||
Assert.Equal("token", value.ToString());
|
||||
|
||||
value = new StringWithQualityHeaderValue("token", 0.1);
|
||||
Assert.Equal("token; q=0.1", value.ToString());
|
||||
|
||||
value = new StringWithQualityHeaderValue("token", 0);
|
||||
Assert.Equal("token; q=0.0", value.ToString());
|
||||
|
||||
value = new StringWithQualityHeaderValue("token", 1);
|
||||
Assert.Equal("token; q=1.0", value.ToString());
|
||||
|
||||
// Note that the quality value gets rounded
|
||||
value = new StringWithQualityHeaderValue("token", 0.56789);
|
||||
Assert.Equal("token; q=0.568", value.ToString());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetHashCode_UseSameAndDifferentValues_SameOrDifferentHashCodes()
|
||||
{
|
||||
var value1 = new StringWithQualityHeaderValue("t", 0.123);
|
||||
var value2 = new StringWithQualityHeaderValue("t", 0.123);
|
||||
var value3 = new StringWithQualityHeaderValue("T", 0.123);
|
||||
var value4 = new StringWithQualityHeaderValue("t");
|
||||
var value5 = new StringWithQualityHeaderValue("x", 0.123);
|
||||
var value6 = new StringWithQualityHeaderValue("t", 0.5);
|
||||
var value7 = new StringWithQualityHeaderValue("t", 0.1234);
|
||||
var value8 = new StringWithQualityHeaderValue("T");
|
||||
var value9 = new StringWithQualityHeaderValue("x");
|
||||
|
||||
Assert.Equal(value1.GetHashCode(), value2.GetHashCode());
|
||||
Assert.Equal(value1.GetHashCode(), value3.GetHashCode());
|
||||
Assert.NotEqual(value1.GetHashCode(), value4.GetHashCode());
|
||||
Assert.NotEqual(value1.GetHashCode(), value5.GetHashCode());
|
||||
Assert.NotEqual(value1.GetHashCode(), value6.GetHashCode());
|
||||
Assert.NotEqual(value1.GetHashCode(), value7.GetHashCode());
|
||||
Assert.Equal(value4.GetHashCode(), value8.GetHashCode());
|
||||
Assert.NotEqual(value4.GetHashCode(), value9.GetHashCode());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Equals_UseSameAndDifferentRanges_EqualOrNotEqualNoExceptions()
|
||||
{
|
||||
var value1 = new StringWithQualityHeaderValue("t", 0.123);
|
||||
var value2 = new StringWithQualityHeaderValue("t", 0.123);
|
||||
var value3 = new StringWithQualityHeaderValue("T", 0.123);
|
||||
var value4 = new StringWithQualityHeaderValue("t");
|
||||
var value5 = new StringWithQualityHeaderValue("x", 0.123);
|
||||
var value6 = new StringWithQualityHeaderValue("t", 0.5);
|
||||
var value7 = new StringWithQualityHeaderValue("t", 0.1234);
|
||||
var value8 = new StringWithQualityHeaderValue("T");
|
||||
var value9 = new StringWithQualityHeaderValue("x");
|
||||
|
||||
Assert.False(value1.Equals(null), "t; q=0.123 vs. <null>");
|
||||
Assert.True(value1.Equals(value2), "t; q=0.123 vs. t; q=0.123");
|
||||
Assert.True(value1.Equals(value3), "t; q=0.123 vs. T; q=0.123");
|
||||
Assert.False(value1.Equals(value4), "t; q=0.123 vs. t");
|
||||
Assert.False(value4.Equals(value1), "t vs. t; q=0.123");
|
||||
Assert.False(value1.Equals(value5), "t; q=0.123 vs. x; q=0.123");
|
||||
Assert.False(value1.Equals(value6), "t; q=0.123 vs. t; q=0.5");
|
||||
Assert.False(value1.Equals(value7), "t; q=0.123 vs. t; q=0.1234");
|
||||
Assert.True(value4.Equals(value8), "t vs. T");
|
||||
Assert.False(value4.Equals(value9), "t vs. T");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Parse_SetOfValidValueStrings_ParsedCorrectly()
|
||||
{
|
||||
CheckValidParse("text", new StringWithQualityHeaderValue("text"));
|
||||
CheckValidParse("text;q=0.5", new StringWithQualityHeaderValue("text", 0.5));
|
||||
CheckValidParse("text ; q = 0.5", new StringWithQualityHeaderValue("text", 0.5));
|
||||
CheckValidParse("\r\n text ; q = 0.5 ", new StringWithQualityHeaderValue("text", 0.5));
|
||||
CheckValidParse(" text ", new StringWithQualityHeaderValue("text"));
|
||||
CheckValidParse(" \r\n text \r\n ; \r\n q = 0.123", new StringWithQualityHeaderValue("text", 0.123));
|
||||
CheckValidParse(" text ; q = 0.123 ", new StringWithQualityHeaderValue("text", 0.123));
|
||||
CheckValidParse("text;q=1 ", new StringWithQualityHeaderValue("text", 1));
|
||||
CheckValidParse("*", new StringWithQualityHeaderValue("*"));
|
||||
CheckValidParse("*;q=0.7", new StringWithQualityHeaderValue("*", 0.7));
|
||||
CheckValidParse(" t", new StringWithQualityHeaderValue("t"));
|
||||
CheckValidParse("t;q=0.", new StringWithQualityHeaderValue("t", 0));
|
||||
CheckValidParse("t;q=1.", new StringWithQualityHeaderValue("t", 1));
|
||||
CheckValidParse("t ; q = 0", new StringWithQualityHeaderValue("t", 0));
|
||||
CheckValidParse("iso-8859-5", new StringWithQualityHeaderValue("iso-8859-5"));
|
||||
CheckValidParse("unicode-1-1; q=0.8", new StringWithQualityHeaderValue("unicode-1-1", 0.8));
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("text,")]
|
||||
[InlineData("\r\n text ; q = 0.5, next_text ")]
|
||||
[InlineData(" text,next_text ")]
|
||||
[InlineData(" ,, text, , ,next")]
|
||||
[InlineData(" ,, text, , ,")]
|
||||
[InlineData(", \r\n text \r\n ; \r\n q = 0.123")]
|
||||
[InlineData("teäxt")]
|
||||
[InlineData("text会")]
|
||||
[InlineData("会")]
|
||||
[InlineData("t;q=会")]
|
||||
[InlineData("t;q=")]
|
||||
[InlineData("t;q")]
|
||||
[InlineData("t;会=1")]
|
||||
[InlineData("t;q会=1")]
|
||||
[InlineData("t y")]
|
||||
[InlineData("t;q=1 y")]
|
||||
[InlineData(null)]
|
||||
[InlineData("")]
|
||||
[InlineData(" ")]
|
||||
[InlineData(" ,,")]
|
||||
[InlineData("t;q=-1")]
|
||||
[InlineData("t;q=1.00001")]
|
||||
[InlineData("t;")]
|
||||
[InlineData("t;;q=1")]
|
||||
[InlineData("t;q=a")]
|
||||
[InlineData("t;qa")]
|
||||
[InlineData("t;q1")]
|
||||
public void Parse_SetOfInvalidValueStrings_Throws(string input)
|
||||
{
|
||||
Assert.Throws<FormatException>(() => StringWithQualityHeaderValue.Parse(input));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TryParse_SetOfValidValueStrings_ParsedCorrectly()
|
||||
{
|
||||
CheckValidTryParse("text", new StringWithQualityHeaderValue("text"));
|
||||
CheckValidTryParse("text;q=0.5", new StringWithQualityHeaderValue("text", 0.5));
|
||||
CheckValidTryParse("text ; q = 0.5", new StringWithQualityHeaderValue("text", 0.5));
|
||||
CheckValidTryParse("\r\n text ; q = 0.5 ", new StringWithQualityHeaderValue("text", 0.5));
|
||||
CheckValidTryParse(" text ", new StringWithQualityHeaderValue("text"));
|
||||
CheckValidTryParse(" \r\n text \r\n ; \r\n q = 0.123", new StringWithQualityHeaderValue("text", 0.123));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TryParse_SetOfInvalidValueStrings_ReturnsFalse()
|
||||
{
|
||||
CheckInvalidTryParse("text,");
|
||||
CheckInvalidTryParse("\r\n text ; q = 0.5, next_text ");
|
||||
CheckInvalidTryParse(" text,next_text ");
|
||||
CheckInvalidTryParse(" ,, text, , ,next");
|
||||
CheckInvalidTryParse(" ,, text, , ,");
|
||||
CheckInvalidTryParse(", \r\n text \r\n ; \r\n q = 0.123");
|
||||
CheckInvalidTryParse("teäxt");
|
||||
CheckInvalidTryParse("text会");
|
||||
CheckInvalidTryParse("会");
|
||||
CheckInvalidTryParse("t;q=会");
|
||||
CheckInvalidTryParse("t;q=");
|
||||
CheckInvalidTryParse("t;q");
|
||||
CheckInvalidTryParse("t;会=1");
|
||||
CheckInvalidTryParse("t;q会=1");
|
||||
CheckInvalidTryParse("t y");
|
||||
CheckInvalidTryParse("t;q=1 y");
|
||||
|
||||
CheckInvalidTryParse(null);
|
||||
CheckInvalidTryParse(string.Empty);
|
||||
CheckInvalidTryParse(" ");
|
||||
CheckInvalidTryParse(" ,,");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ParseList_SetOfValidValueStrings_ParsedCorrectly()
|
||||
{
|
||||
var inputs = new[]
|
||||
{
|
||||
"",
|
||||
"text1",
|
||||
"text2,",
|
||||
"textA,textB",
|
||||
"text3;q=0.5",
|
||||
"text4;q=0.5,",
|
||||
" text5 ; q = 0.50 ",
|
||||
"\r\n text6 ; q = 0.05 ",
|
||||
"text7,text8;q=0.5",
|
||||
" text9 , text10 ; q = 0.5 ",
|
||||
};
|
||||
IList<StringWithQualityHeaderValue> results = StringWithQualityHeaderValue.ParseList(inputs);
|
||||
|
||||
var expectedResults = new[]
|
||||
{
|
||||
new StringWithQualityHeaderValue("text1"),
|
||||
new StringWithQualityHeaderValue("text2"),
|
||||
new StringWithQualityHeaderValue("textA"),
|
||||
new StringWithQualityHeaderValue("textB"),
|
||||
new StringWithQualityHeaderValue("text3", 0.5),
|
||||
new StringWithQualityHeaderValue("text4", 0.5),
|
||||
new StringWithQualityHeaderValue("text5", 0.5),
|
||||
new StringWithQualityHeaderValue("text6", 0.05),
|
||||
new StringWithQualityHeaderValue("text7"),
|
||||
new StringWithQualityHeaderValue("text8", 0.5),
|
||||
new StringWithQualityHeaderValue("text9"),
|
||||
new StringWithQualityHeaderValue("text10", 0.5),
|
||||
}.ToList();
|
||||
|
||||
Assert.Equal(expectedResults, results);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TryParseList_SetOfValidValueStrings_ParsedCorrectly()
|
||||
{
|
||||
var inputs = new[]
|
||||
{
|
||||
"",
|
||||
"text1",
|
||||
"text2,",
|
||||
"textA,textB",
|
||||
"text3;q=0.5",
|
||||
"text4;q=0.5,",
|
||||
" text5 ; q = 0.50 ",
|
||||
"\r\n text6 ; q = 0.05 ",
|
||||
"text7,text8;q=0.5",
|
||||
" text9 , text10 ; q = 0.5 ",
|
||||
};
|
||||
IList<StringWithQualityHeaderValue> results;
|
||||
Assert.True(StringWithQualityHeaderValue.TryParseList(inputs, out results));
|
||||
|
||||
var expectedResults = new[]
|
||||
{
|
||||
new StringWithQualityHeaderValue("text1"),
|
||||
new StringWithQualityHeaderValue("text2"),
|
||||
new StringWithQualityHeaderValue("textA"),
|
||||
new StringWithQualityHeaderValue("textB"),
|
||||
new StringWithQualityHeaderValue("text3", 0.5),
|
||||
new StringWithQualityHeaderValue("text4", 0.5),
|
||||
new StringWithQualityHeaderValue("text5", 0.5),
|
||||
new StringWithQualityHeaderValue("text6", 0.05),
|
||||
new StringWithQualityHeaderValue("text7"),
|
||||
new StringWithQualityHeaderValue("text8", 0.5),
|
||||
new StringWithQualityHeaderValue("text9"),
|
||||
new StringWithQualityHeaderValue("text10", 0.5),
|
||||
}.ToList();
|
||||
|
||||
Assert.Equal(expectedResults, results);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ParseList_WithSomeInvlaidValues_Throws()
|
||||
{
|
||||
var inputs = new[]
|
||||
{
|
||||
"",
|
||||
"text1",
|
||||
"text 1",
|
||||
"text2",
|
||||
"\"text 2\",",
|
||||
"text3;q=0.5",
|
||||
"text4;q=0.5, extra stuff",
|
||||
" text5 ; q = 0.50 ",
|
||||
"\r\n text6 ; q = 0.05 ",
|
||||
"text7,text8;q=0.5",
|
||||
" text9 , text10 ; q = 0.5 ",
|
||||
};
|
||||
Assert.Throws<FormatException>(() => StringWithQualityHeaderValue.ParseList(inputs));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TryParseList_WithSomeInvlaidValues_ReturnsFalse()
|
||||
{
|
||||
var inputs = new[]
|
||||
{
|
||||
"",
|
||||
"text1",
|
||||
"text 1",
|
||||
"text2",
|
||||
"\"text 2\",",
|
||||
"text3;q=0.5",
|
||||
"text4;q=0.5, extra stuff",
|
||||
" text5 ; q = 0.50 ",
|
||||
"\r\n text6 ; q = 0.05 ",
|
||||
"text7,text8;q=0.5",
|
||||
" text9 , text10 ; q = 0.5 ",
|
||||
};
|
||||
IList<StringWithQualityHeaderValue> results;
|
||||
Assert.False(StringWithQualityHeaderValue.TryParseList(inputs, out results));
|
||||
}
|
||||
|
||||
#region Helper methods
|
||||
|
||||
private void CheckValidParse(string input, StringWithQualityHeaderValue expectedResult)
|
||||
{
|
||||
var result = StringWithQualityHeaderValue.Parse(input);
|
||||
Assert.Equal(expectedResult, result);
|
||||
}
|
||||
|
||||
private void CheckValidTryParse(string input, StringWithQualityHeaderValue expectedResult)
|
||||
{
|
||||
StringWithQualityHeaderValue result = null;
|
||||
Assert.True(StringWithQualityHeaderValue.TryParse(input, out result));
|
||||
Assert.Equal(expectedResult, result);
|
||||
}
|
||||
|
||||
private void CheckInvalidTryParse(string input)
|
||||
{
|
||||
StringWithQualityHeaderValue result = null;
|
||||
Assert.False(StringWithQualityHeaderValue.TryParse(input, out result));
|
||||
Assert.Null(result);
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,18 @@
|
|||
{
|
||||
"version": "1.0.0-*",
|
||||
"dependencies": {
|
||||
"Microsoft.Net.Http.Headers": "1.0.0-*",
|
||||
"xunit.runner.kre": "1.0.0-*"
|
||||
},
|
||||
"commands": {
|
||||
"test": "xunit.runner.kre"
|
||||
},
|
||||
"frameworks": {
|
||||
"aspnet50": { },
|
||||
"aspnetcore50": {
|
||||
"dependencies": {
|
||||
"System.Diagnostics.Debug": "4.0.10-beta-*"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue