Implement strongly typed headers.

This commit is contained in:
Chris Ross 2015-01-14 15:41:09 -08:00
parent 4377bb24ce
commit 4fb21644fc
75 changed files with 10676 additions and 173 deletions

View File

@ -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

View File

@ -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>

View File

@ -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;
}
}
}

View File

@ -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>

View File

@ -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>>

View File

@ -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);
}
}
}
}

View File

@ -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);
}
}
}
}

View File

@ -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(),

View File

@ -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" : {

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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))

View File

@ -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);
}
}
}

View File

@ -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>

View File

@ -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; }

View File

@ -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;
}

View File

@ -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);

View File

@ -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>

View File

@ -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": {},

View File

@ -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);

View File

@ -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>

View File

@ -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-*"
}
}
}

View File

@ -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;
}
}
}

View File

@ -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");
}
}
}

View File

@ -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)));
}
}
}

View File

@ -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;
}
}
}

View File

@ -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;
}
}
}

View File

@ -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();
}
}
}

View File

@ -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;
}
}
}

View File

@ -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);
}
}
}

View File

@ -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";
}
}

View File

@ -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;
}
}

View File

@ -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;
}
}
}

View File

@ -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();
}
}
}

View File

@ -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,
}
}

View File

@ -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;
}
}
}

View File

@ -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));
}
}
}
}

View File

@ -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;
}
}
}

View File

@ -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>

View File

@ -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();
}
}
}

View File

@ -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
{
}
}

View File

@ -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");
}
}
}
}

View File

@ -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;
}
}
}

View File

@ -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;
}
}
}

View File

@ -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;
}
}
}

View File

@ -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();
}
}
}

View File

@ -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;
}
}
}

View File

@ -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;
}
}
}

View File

@ -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-*"
}
}
}
}

View File

@ -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;
}
}
}
}

View File

@ -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>

View File

@ -5,7 +5,7 @@ using System;
using System.Collections.Generic;
using Xunit;
namespace Microsoft.AspNet.WebUtilities
namespace Microsoft.AspNet.Http.Extensions
{
public class QueryBuilderTests
{

View File

@ -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);
}
}
}

View File

@ -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()
{

View File

@ -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>

View File

@ -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": { }
}
}

View File

@ -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
}
}

View File

@ -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-&#xc3;&#xa4;.html""", new ContentDispositionHeaderValue("attachment") { FileName = @"""foo-&#xc3;&#xa4;.html""" } }, // 'attachment', specifying a filename of foo-&#xc3;&#xa4;.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-ä-&#x20ac;.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-ä-&#x20ac;.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-&#xc3;&#xa4;.html", // "'attachment', specifying a filename of foo-&#xc3;&#xa4;.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-ä-&#x20ac;.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));
}
}
}

View File

@ -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);
}
}
}

View File

@ -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);
}
}
}

View File

@ -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
}
}

View File

@ -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));
}
}
}

View File

@ -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);
}
}
}

View File

@ -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));
}
}
}

View File

@ -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>

View File

@ -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
}
}

View File

@ -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
}
}

View File

@ -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
}
}

View File

@ -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);
}
}
}
}

View File

@ -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);
}
}
}

View File

@ -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));
}
}
}

View File

@ -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
}
}

View File

@ -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-*"
}
}
}
}