From fd45ff532c7b11657269a8bbd889274f3d4e6426 Mon Sep 17 00:00:00 2001 From: Suhas Joshi Date: Mon, 8 Dec 2014 15:24:38 -0800 Subject: [PATCH 01/16] Updating to dev NuGet.config --- NuGet.Config | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/NuGet.Config b/NuGet.Config index 2d3b0cb857..f41e9c631d 100644 --- a/NuGet.Config +++ b/NuGet.Config @@ -1,7 +1,7 @@  - + From ae169aa794a3ddc10449020043fb71d6a8b880b1 Mon Sep 17 00:00:00 2001 From: Pranav K Date: Mon, 15 Dec 2014 14:42:21 -0800 Subject: [PATCH 02/16] Reacting to System.Threading version changes --- src/Microsoft.AspNet.FeatureModel/project.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Microsoft.AspNet.FeatureModel/project.json b/src/Microsoft.AspNet.FeatureModel/project.json index 20e2fae042..6845cd91a4 100644 --- a/src/Microsoft.AspNet.FeatureModel/project.json +++ b/src/Microsoft.AspNet.FeatureModel/project.json @@ -14,7 +14,7 @@ "System.Reflection.TypeExtensions": "4.0.0-beta-*", "System.Runtime": "4.0.20-beta-*", "System.Runtime.InteropServices": "4.0.20-beta-*", - "System.Threading": "4.0.0-beta-*" + "System.Threading": "4.0.10-beta-*" } } } From b7eb1a92bb567e4efa9dc485a2252023f2cdb947 Mon Sep 17 00:00:00 2001 From: Brennan Date: Mon, 15 Dec 2014 15:09:59 -0800 Subject: [PATCH 03/16] Update tests to use official xunit --- test/Microsoft.AspNet.FeatureModel.Tests/project.json | 4 ++-- test/Microsoft.AspNet.Http.Extensions.Tests/project.json | 4 ++-- test/Microsoft.AspNet.Http.Tests/project.json | 4 ++-- test/Microsoft.AspNet.Owin.Tests/project.json | 4 ++-- test/Microsoft.AspNet.PipelineCore.Tests/project.json | 4 ++-- test/Microsoft.AspNet.WebUtilities.Tests/project.json | 4 ++-- 6 files changed, 12 insertions(+), 12 deletions(-) diff --git a/test/Microsoft.AspNet.FeatureModel.Tests/project.json b/test/Microsoft.AspNet.FeatureModel.Tests/project.json index bf587219ea..867e8889e6 100644 --- a/test/Microsoft.AspNet.FeatureModel.Tests/project.json +++ b/test/Microsoft.AspNet.FeatureModel.Tests/project.json @@ -3,10 +3,10 @@ "Microsoft.AspNet.FeatureModel": "1.0.0-*", "Microsoft.AspNet.Http": "1.0.0-*", "Microsoft.AspNet.HttpFeature": "1.0.0-*", - "Xunit.KRunner": "1.0.0-*" + "xunit.runner.kre": "1.0.0-*" }, "commands": { - "test": "Xunit.KRunner" + "test": "xunit.runner.kre" }, "frameworks": { "aspnet50": { diff --git a/test/Microsoft.AspNet.Http.Extensions.Tests/project.json b/test/Microsoft.AspNet.Http.Extensions.Tests/project.json index 1e7c64bbe6..85c20ce28b 100644 --- a/test/Microsoft.AspNet.Http.Extensions.Tests/project.json +++ b/test/Microsoft.AspNet.Http.Extensions.Tests/project.json @@ -4,10 +4,10 @@ "Microsoft.AspNet.Http.Extensions": "1.0.0-*", "Microsoft.AspNet.HttpFeature": "1.0.0-*", "Microsoft.AspNet.PipelineCore": "1.0.0-*", - "Xunit.KRunner": "1.0.0-*" + "xunit.runner.kre": "1.0.0-*" }, "commands": { - "test": "Xunit.KRunner" + "test": "xunit.runner.kre" }, "frameworks": { "aspnet50": { diff --git a/test/Microsoft.AspNet.Http.Tests/project.json b/test/Microsoft.AspNet.Http.Tests/project.json index e85c177c16..ff303ab74d 100644 --- a/test/Microsoft.AspNet.Http.Tests/project.json +++ b/test/Microsoft.AspNet.Http.Tests/project.json @@ -4,10 +4,10 @@ "Microsoft.AspNet.HttpFeature": "1.0.0-*", "Microsoft.AspNet.PipelineCore": "1.0.0-*", "Microsoft.AspNet.Testing": "1.0.0-*", - "Xunit.KRunner": "1.0.0-*" + "xunit.runner.kre": "1.0.0-*" }, "commands": { - "test": "Xunit.KRunner" + "test": "xunit.runner.kre" }, "frameworks": { "aspnet50": { diff --git a/test/Microsoft.AspNet.Owin.Tests/project.json b/test/Microsoft.AspNet.Owin.Tests/project.json index 9893f86030..16f501140e 100644 --- a/test/Microsoft.AspNet.Owin.Tests/project.json +++ b/test/Microsoft.AspNet.Owin.Tests/project.json @@ -5,10 +5,10 @@ "Microsoft.AspNet.HttpFeature": "1.0.0-*", "Microsoft.AspNet.Owin": "1.0.0-*", "Microsoft.AspNet.PipelineCore": "1.0.0-*", - "Xunit.KRunner": "1.0.0-*" + "xunit.runner.kre": "1.0.0-*" }, "commands": { - "test": "Xunit.KRunner" + "test": "xunit.runner.kre" }, "frameworks": { "aspnet50": { diff --git a/test/Microsoft.AspNet.PipelineCore.Tests/project.json b/test/Microsoft.AspNet.PipelineCore.Tests/project.json index 1c3bc620fe..fda938bb96 100644 --- a/test/Microsoft.AspNet.PipelineCore.Tests/project.json +++ b/test/Microsoft.AspNet.PipelineCore.Tests/project.json @@ -4,10 +4,10 @@ "Microsoft.AspNet.Http": "1.0.0-*", "Microsoft.AspNet.HttpFeature": "1.0.0-*", "Microsoft.AspNet.PipelineCore": "1.0.0-*", - "Xunit.KRunner": "1.0.0-*" + "xunit.runner.kre": "1.0.0-*" }, "commands": { - "test": "Xunit.KRunner" + "test": "xunit.runner.kre" }, "frameworks": { "aspnet50": { diff --git a/test/Microsoft.AspNet.WebUtilities.Tests/project.json b/test/Microsoft.AspNet.WebUtilities.Tests/project.json index 868b37110c..c3fcb3e97c 100644 --- a/test/Microsoft.AspNet.WebUtilities.Tests/project.json +++ b/test/Microsoft.AspNet.WebUtilities.Tests/project.json @@ -2,10 +2,10 @@ "dependencies": { "Microsoft.AspNet.Http": "1.0.0-*", "Microsoft.AspNet.WebUtilities": "1.0.0-*", - "Xunit.KRunner": "1.0.0-*" + "xunit.runner.kre": "1.0.0-*" }, "commands": { - "test": "Xunit.KRunner" + "test": "xunit.runner.kre" }, "frameworks": { "aspnet50": { } From 5872feb224f731f2d64af0b1e6c8193b583031b1 Mon Sep 17 00:00:00 2001 From: Chris Ross Date: Tue, 4 Nov 2014 16:31:18 -0800 Subject: [PATCH 04/16] #139 - Mime multipart request parsing. --- .../project.json | 2 +- src/Microsoft.AspNet.Http/FragmentString.cs | 6 +- src/Microsoft.AspNet.Http/HostString.cs | 6 +- src/Microsoft.AspNet.Http/HttpRequest.cs | 22 +- src/Microsoft.AspNet.Http/IFormCollection.cs | 3 + src/Microsoft.AspNet.Http/IFormFile.cs | 20 + .../IFormFileCollection.cs | 16 + src/Microsoft.AspNet.Http/PathString.cs | 6 +- src/Microsoft.AspNet.Http/QueryString.cs | 6 +- .../Security/AuthenticateResult.cs | 10 +- .../Security/AuthenticationDescription.cs | 6 +- src/Microsoft.AspNet.Owin/NotNullAttribute.cs | 12 + .../OwinFeatureCollection.cs | 6 +- .../WebSockets/WebSocketAdapter.cs | 6 +- .../BufferingHelper.cs | 47 +++ .../Collections/FormCollection.cs | 20 +- .../Collections/FormFileCollection.cs | 36 ++ .../Collections/HeaderDictionary.cs | 7 +- .../Collections/ItemsDictionary.cs | 1 - .../Collections/ReadableStringCollection.cs | 21 +- .../Collections/ResponseCookies.cs | 21 +- .../DefaultHttpContext.cs | 12 +- .../DefaultHttpRequest.cs | 28 +- .../DefaultHttpResponse.cs | 19 +- .../FormFeature.cs | 193 +++++++-- src/Microsoft.AspNet.PipelineCore/FormFile.cs | 46 ++ .../IFormFeature.cs | 24 +- .../Infrastructure/ParsingHelpers.cs | 70 +--- .../QueryFeature.cs | 4 +- .../ReferenceReadStream.cs | 199 +++++++++ .../RequestCookiesFeature.cs | 1 - .../Security/AuthenticateContext.cs | 6 +- .../Security/ChallengeContext.cs | 8 +- .../Security/SignInContext.cs | 6 +- .../Security/SignOutContext.cs | 6 +- .../BufferedReadStream.cs | 396 ++++++++++++++++++ .../FileBufferingReadStream.cs | 248 +++++++++++ .../FormHelpers.cs | 21 - .../FormReader.cs | 188 +++++++++ .../KeyValueAccumulator.cs | 42 ++ .../MultipartReader.cs | 92 ++++ .../MultipartReaderStream.cs | 320 ++++++++++++++ .../MultipartSection.cs | 47 +++ .../ParsingHelpers.cs | 111 ----- .../QueryHelpers.cs | 45 +- .../StreamHelperExtensions.cs | 23 + .../project.json | 1 + .../FormFeatureTests.cs | 266 ++++++++++-- .../MultipartReaderTests.cs | 185 ++++++++ .../QueryHelpersTests.cs | 19 +- 50 files changed, 2487 insertions(+), 419 deletions(-) create mode 100644 src/Microsoft.AspNet.Http/IFormFile.cs create mode 100644 src/Microsoft.AspNet.Http/IFormFileCollection.cs create mode 100644 src/Microsoft.AspNet.Owin/NotNullAttribute.cs create mode 100644 src/Microsoft.AspNet.PipelineCore/BufferingHelper.cs rename src/{Microsoft.AspNet.WebUtilities => Microsoft.AspNet.PipelineCore}/Collections/FormCollection.cs (53%) create mode 100644 src/Microsoft.AspNet.PipelineCore/Collections/FormFileCollection.cs rename src/{Microsoft.AspNet.WebUtilities => Microsoft.AspNet.PipelineCore}/Collections/ReadableStringCollection.cs (86%) create mode 100644 src/Microsoft.AspNet.PipelineCore/FormFile.cs create mode 100644 src/Microsoft.AspNet.PipelineCore/ReferenceReadStream.cs create mode 100644 src/Microsoft.AspNet.WebUtilities/BufferedReadStream.cs create mode 100644 src/Microsoft.AspNet.WebUtilities/FileBufferingReadStream.cs delete mode 100644 src/Microsoft.AspNet.WebUtilities/FormHelpers.cs create mode 100644 src/Microsoft.AspNet.WebUtilities/FormReader.cs create mode 100644 src/Microsoft.AspNet.WebUtilities/KeyValueAccumulator.cs create mode 100644 src/Microsoft.AspNet.WebUtilities/MultipartReader.cs create mode 100644 src/Microsoft.AspNet.WebUtilities/MultipartReaderStream.cs create mode 100644 src/Microsoft.AspNet.WebUtilities/MultipartSection.cs delete mode 100644 src/Microsoft.AspNet.WebUtilities/ParsingHelpers.cs create mode 100644 src/Microsoft.AspNet.WebUtilities/StreamHelperExtensions.cs create mode 100644 test/Microsoft.AspNet.WebUtilities.Tests/MultipartReaderTests.cs diff --git a/src/Microsoft.AspNet.Http.Extensions/project.json b/src/Microsoft.AspNet.Http.Extensions/project.json index 0254711dd8..cbc45a6ddc 100644 --- a/src/Microsoft.AspNet.Http.Extensions/project.json +++ b/src/Microsoft.AspNet.Http.Extensions/project.json @@ -8,7 +8,7 @@ "frameworks" : { "aspnet50" : { }, - "aspnetcore50" : { + "aspnetcore50" : { "dependencies": { "System.Reflection.TypeExtensions": "4.0.0-beta-*", "System.Runtime": "4.0.20-beta-*" diff --git a/src/Microsoft.AspNet.Http/FragmentString.cs b/src/Microsoft.AspNet.Http/FragmentString.cs index 1d725e742c..f40ba626a1 100644 --- a/src/Microsoft.AspNet.Http/FragmentString.cs +++ b/src/Microsoft.AspNet.Http/FragmentString.cs @@ -90,12 +90,8 @@ namespace Microsoft.AspNet.Http /// /// The Uri object /// The resulting FragmentString - public static FragmentString FromUriComponent(Uri uri) + public static FragmentString FromUriComponent([NotNull] Uri uri) { - if (uri == null) - { - throw new ArgumentNullException("uri"); - } string fragmentValue = uri.GetComponents(UriComponents.Fragment, UriFormat.UriEscaped); if (!string.IsNullOrEmpty(fragmentValue)) { diff --git a/src/Microsoft.AspNet.Http/HostString.cs b/src/Microsoft.AspNet.Http/HostString.cs index ae72f819cc..c91ce65a5b 100644 --- a/src/Microsoft.AspNet.Http/HostString.cs +++ b/src/Microsoft.AspNet.Http/HostString.cs @@ -134,12 +134,8 @@ namespace Microsoft.AspNet.Http /// /// /// - public static HostString FromUriComponent(Uri uri) + public static HostString FromUriComponent([NotNull] Uri uri) { - if (uri == null) - { - throw new ArgumentNullException("uri"); - } return new HostString(uri.GetComponents( UriComponents.NormalizedHost | // Always convert punycode to Unicode. UriComponents.HostAndPort, UriFormat.Unescaped)); diff --git a/src/Microsoft.AspNet.Http/HttpRequest.cs b/src/Microsoft.AspNet.Http/HttpRequest.cs index be5e1e14b1..8048152a08 100644 --- a/src/Microsoft.AspNet.Http/HttpRequest.cs +++ b/src/Microsoft.AspNet.Http/HttpRequest.cs @@ -64,12 +64,6 @@ namespace Microsoft.AspNet.Http /// The query value collection parsed from owin.RequestQueryString. public abstract IReadableStringCollection Query { get; } - /// - /// Gets the form collection. - /// - /// The form collection parsed from the request body. - public abstract Task GetFormAsync(CancellationToken cancellationToken = default(CancellationToken)); - /// /// Gets or set the owin.RequestProtocol. /// @@ -128,5 +122,21 @@ namespace Microsoft.AspNet.Http /// /// The owin.RequestBody Stream. public abstract Stream Body { get; set; } + + /// + /// Checks the content-type header for form types. + /// + public abstract bool HasFormContentType { get; } + + /// + /// Gets or sets the request body as a form. + /// + public abstract IFormCollection Form { get; set; } + + /// + /// Reads the request body if it is a form. + /// + /// + public abstract Task ReadFormAsync(CancellationToken cancellationToken = new CancellationToken()); } } diff --git a/src/Microsoft.AspNet.Http/IFormCollection.cs b/src/Microsoft.AspNet.Http/IFormCollection.cs index a69162fa4d..4ec7437877 100644 --- a/src/Microsoft.AspNet.Http/IFormCollection.cs +++ b/src/Microsoft.AspNet.Http/IFormCollection.cs @@ -1,6 +1,8 @@ // 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; + namespace Microsoft.AspNet.Http { /// @@ -8,5 +10,6 @@ namespace Microsoft.AspNet.Http /// public interface IFormCollection : IReadableStringCollection { + IFormFileCollection Files { get; } } } diff --git a/src/Microsoft.AspNet.Http/IFormFile.cs b/src/Microsoft.AspNet.Http/IFormFile.cs new file mode 100644 index 0000000000..a77a495d5e --- /dev/null +++ b/src/Microsoft.AspNet.Http/IFormFile.cs @@ -0,0 +1,20 @@ +// 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.IO; + +namespace Microsoft.AspNet.Http +{ + public interface IFormFile + { + string ContentType { get; } + + string ContentDisposition { get; } + + IHeaderDictionary Headers { get; } + + long Length { get; } + + Stream OpenReadStream(); + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNet.Http/IFormFileCollection.cs b/src/Microsoft.AspNet.Http/IFormFileCollection.cs new file mode 100644 index 0000000000..56f1d5879d --- /dev/null +++ b/src/Microsoft.AspNet.Http/IFormFileCollection.cs @@ -0,0 +1,16 @@ +// 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; + +namespace Microsoft.AspNet.Http +{ + public interface IFormFileCollection : IList + { + IFormFile this[string name] { get; } + + IFormFile GetFile(string name); + + IList GetFiles(string name); + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNet.Http/PathString.cs b/src/Microsoft.AspNet.Http/PathString.cs index 05dfa6d816..2963c64d8f 100644 --- a/src/Microsoft.AspNet.Http/PathString.cs +++ b/src/Microsoft.AspNet.Http/PathString.cs @@ -86,12 +86,8 @@ namespace Microsoft.AspNet.Http /// /// The Uri object /// The resulting PathString - public static PathString FromUriComponent(Uri uri) + public static PathString FromUriComponent([NotNull] Uri uri) { - if (uri == null) - { - throw new ArgumentNullException("uri"); - } // REVIEW: what is the exactly correct thing to do? return new PathString("/" + uri.GetComponents(UriComponents.Path, UriFormat.Unescaped)); } diff --git a/src/Microsoft.AspNet.Http/QueryString.cs b/src/Microsoft.AspNet.Http/QueryString.cs index 5ba16151c7..c32e3bb037 100644 --- a/src/Microsoft.AspNet.Http/QueryString.cs +++ b/src/Microsoft.AspNet.Http/QueryString.cs @@ -102,12 +102,8 @@ namespace Microsoft.AspNet.Http /// /// The Uri object /// The resulting QueryString - public static QueryString FromUriComponent(Uri uri) + public static QueryString FromUriComponent([NotNull] Uri uri) { - if (uri == null) - { - throw new ArgumentNullException("uri"); - } string queryValue = uri.GetComponents(UriComponents.Query, UriFormat.UriEscaped); if (!string.IsNullOrEmpty(queryValue)) { diff --git a/src/Microsoft.AspNet.Http/Security/AuthenticateResult.cs b/src/Microsoft.AspNet.Http/Security/AuthenticateResult.cs index 5a1998f336..38b48dcf0e 100644 --- a/src/Microsoft.AspNet.Http/Security/AuthenticateResult.cs +++ b/src/Microsoft.AspNet.Http/Security/AuthenticateResult.cs @@ -18,16 +18,8 @@ namespace Microsoft.AspNet.Http.Security /// Assigned to Identity. May be null. /// Assigned to Properties. Contains extra information carried along with the identity. /// Assigned to Description. Contains information describing the authentication provider. - public AuthenticationResult(IIdentity identity, AuthenticationProperties properties, AuthenticationDescription description) + public AuthenticationResult(IIdentity identity, [NotNull] AuthenticationProperties properties, [NotNull] AuthenticationDescription description) { - if (properties == null) - { - throw new ArgumentNullException("properties"); - } - if (description == null) - { - throw new ArgumentNullException("description"); - } if (identity != null) { Identity = identity as ClaimsIdentity ?? new ClaimsIdentity(identity); diff --git a/src/Microsoft.AspNet.Http/Security/AuthenticationDescription.cs b/src/Microsoft.AspNet.Http/Security/AuthenticationDescription.cs index d6423f34fd..2d7d4f0c73 100644 --- a/src/Microsoft.AspNet.Http/Security/AuthenticationDescription.cs +++ b/src/Microsoft.AspNet.Http/Security/AuthenticationDescription.cs @@ -27,12 +27,8 @@ namespace Microsoft.AspNet.Http.Security /// Initializes a new instance of the class /// /// - public AuthenticationDescription(IDictionary properties) + public AuthenticationDescription([NotNull] IDictionary properties) { - if (properties == null) - { - throw new ArgumentNullException("properties"); - } Dictionary = properties; } diff --git a/src/Microsoft.AspNet.Owin/NotNullAttribute.cs b/src/Microsoft.AspNet.Owin/NotNullAttribute.cs new file mode 100644 index 0000000000..a42aa58d4a --- /dev/null +++ b/src/Microsoft.AspNet.Owin/NotNullAttribute.cs @@ -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.AspNet.Owin +{ + [AttributeUsage(AttributeTargets.Parameter, AllowMultiple = false)] + internal sealed class NotNullAttribute : Attribute + { + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNet.Owin/OwinFeatureCollection.cs b/src/Microsoft.AspNet.Owin/OwinFeatureCollection.cs index 23ca6df7bf..434817129d 100644 --- a/src/Microsoft.AspNet.Owin/OwinFeatureCollection.cs +++ b/src/Microsoft.AspNet.Owin/OwinFeatureCollection.cs @@ -399,12 +399,8 @@ namespace Microsoft.AspNet.Owin return TryGetValue(item.Key, out result) && result.Equals(item.Value); } - public void CopyTo(KeyValuePair[] array, int arrayIndex) + public void CopyTo([NotNull] KeyValuePair[] array, int arrayIndex) { - if (array == null) - { - throw new ArgumentNullException("array"); - } if (arrayIndex < 0 || arrayIndex > array.Length) { throw new ArgumentOutOfRangeException("arrayIndex", arrayIndex, string.Empty); diff --git a/src/Microsoft.AspNet.Owin/WebSockets/WebSocketAdapter.cs b/src/Microsoft.AspNet.Owin/WebSockets/WebSocketAdapter.cs index 79f8c0bf2e..9577785639 100644 --- a/src/Microsoft.AspNet.Owin/WebSockets/WebSocketAdapter.cs +++ b/src/Microsoft.AspNet.Owin/WebSockets/WebSocketAdapter.cs @@ -113,7 +113,7 @@ namespace Microsoft.AspNet.Owin } else { - throw new ArgumentOutOfRangeException("buffer"); + throw new ArgumentOutOfRangeException(nameof(buffer)); } } @@ -149,7 +149,7 @@ namespace Microsoft.AspNet.Owin case 0x8: return WebSocketMessageType.Close; default: - throw new ArgumentOutOfRangeException("messageType", messageType, string.Empty); + throw new ArgumentOutOfRangeException(nameof(messageType), messageType, string.Empty); } } @@ -164,7 +164,7 @@ namespace Microsoft.AspNet.Owin case WebSocketMessageType.Close: return 0x8; default: - throw new ArgumentOutOfRangeException("webSocketMessageType", webSocketMessageType, string.Empty); + throw new ArgumentOutOfRangeException(nameof(webSocketMessageType), webSocketMessageType, string.Empty); } } } diff --git a/src/Microsoft.AspNet.PipelineCore/BufferingHelper.cs b/src/Microsoft.AspNet.PipelineCore/BufferingHelper.cs new file mode 100644 index 0000000000..b10c017c36 --- /dev/null +++ b/src/Microsoft.AspNet.PipelineCore/BufferingHelper.cs @@ -0,0 +1,47 @@ +// 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.IO; +using Microsoft.AspNet.Http; +using Microsoft.AspNet.WebUtilities; + +namespace Microsoft.AspNet.PipelineCore +{ + public static class BufferingHelper + { + internal const int DefaultBufferThreshold = 1024 * 30; + + public static string TempDirectory + { + get + { + var temp = Environment.GetEnvironmentVariable("ASPNET_TEMP"); + if (string.IsNullOrEmpty(temp)) + { + temp = Environment.GetEnvironmentVariable("TEMP"); + } + + if (!Directory.Exists(temp)) + { + // TODO: ??? + throw new DirectoryNotFoundException(temp); + } + + return temp; + } + } + + public static HttpRequest EnableRewind([NotNull] this HttpRequest request, int bufferThreshold = DefaultBufferThreshold) + { + var body = request.Body; + if (!body.CanSeek) + { + // TODO: Register this buffer for disposal at the end of the request to ensure the temp file is deleted. + // Otherwise it won't get deleted until GC closes the stream. + request.Body = new FileBufferingReadStream(body, bufferThreshold, TempDirectory); + } + return request; + } + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNet.WebUtilities/Collections/FormCollection.cs b/src/Microsoft.AspNet.PipelineCore/Collections/FormCollection.cs similarity index 53% rename from src/Microsoft.AspNet.WebUtilities/Collections/FormCollection.cs rename to src/Microsoft.AspNet.PipelineCore/Collections/FormCollection.cs index a9d1df8529..a80fea12d1 100644 --- a/src/Microsoft.AspNet.WebUtilities/Collections/FormCollection.cs +++ b/src/Microsoft.AspNet.PipelineCore/Collections/FormCollection.cs @@ -1,23 +1,27 @@ // 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 Microsoft.AspNet.Http; using System.Collections.Generic; +using Microsoft.AspNet.Http; -namespace Microsoft.AspNet.WebUtilities.Collections +namespace Microsoft.AspNet.PipelineCore.Collections { /// /// Contains the parsed form values. /// public class FormCollection : ReadableStringCollection, IFormCollection { - /// - /// Initializes a new instance of the class. - /// - /// The store for the form. - public FormCollection(IDictionary store) - : base(store) + public FormCollection([NotNull] IDictionary store) + : this(store, new FormFileCollection()) { } + + public FormCollection([NotNull] IDictionary store, [NotNull] IFormFileCollection files) + : base(store) + { + Files = files; + } + + public IFormFileCollection Files { get; private set; } } } diff --git a/src/Microsoft.AspNet.PipelineCore/Collections/FormFileCollection.cs b/src/Microsoft.AspNet.PipelineCore/Collections/FormFileCollection.cs new file mode 100644 index 0000000000..10aad2bfa2 --- /dev/null +++ b/src/Microsoft.AspNet.PipelineCore/Collections/FormFileCollection.cs @@ -0,0 +1,36 @@ +// 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; + +namespace Microsoft.AspNet.PipelineCore.Collections +{ + public class FormFileCollection : List, IFormFileCollection + { + public IFormFile this[string name] + { + get { return GetFile(name); } + } + + public IFormFile GetFile(string name) + { + return Find(file => string.Equals(name, GetName(file.ContentDisposition))); + } + + public IList GetFiles(string name) + { + return FindAll(file => string.Equals(name, GetName(file.ContentDisposition))); + } + + 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; + } + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNet.PipelineCore/Collections/HeaderDictionary.cs b/src/Microsoft.AspNet.PipelineCore/Collections/HeaderDictionary.cs index 22d62c5989..dfd54393cc 100644 --- a/src/Microsoft.AspNet.PipelineCore/Collections/HeaderDictionary.cs +++ b/src/Microsoft.AspNet.PipelineCore/Collections/HeaderDictionary.cs @@ -20,13 +20,8 @@ namespace Microsoft.AspNet.PipelineCore.Collections /// Initializes a new instance of the class. /// /// The underlying data store. - public HeaderDictionary(IDictionary store) + public HeaderDictionary([NotNull] IDictionary store) { - if (store == null) - { - throw new ArgumentNullException("store"); - } - Store = store; } diff --git a/src/Microsoft.AspNet.PipelineCore/Collections/ItemsDictionary.cs b/src/Microsoft.AspNet.PipelineCore/Collections/ItemsDictionary.cs index e9c21c835d..dc5216d117 100644 --- a/src/Microsoft.AspNet.PipelineCore/Collections/ItemsDictionary.cs +++ b/src/Microsoft.AspNet.PipelineCore/Collections/ItemsDictionary.cs @@ -3,7 +3,6 @@ using System.Collections; using System.Collections.Generic; -using Microsoft.AspNet.Http; namespace Microsoft.AspNet.PipelineCore { diff --git a/src/Microsoft.AspNet.WebUtilities/Collections/ReadableStringCollection.cs b/src/Microsoft.AspNet.PipelineCore/Collections/ReadableStringCollection.cs similarity index 86% rename from src/Microsoft.AspNet.WebUtilities/Collections/ReadableStringCollection.cs rename to src/Microsoft.AspNet.PipelineCore/Collections/ReadableStringCollection.cs index 97c03b0df2..1e12bb6dc1 100644 --- a/src/Microsoft.AspNet.WebUtilities/Collections/ReadableStringCollection.cs +++ b/src/Microsoft.AspNet.PipelineCore/Collections/ReadableStringCollection.cs @@ -6,7 +6,7 @@ using System.Collections; using System.Collections.Generic; using Microsoft.AspNet.Http; -namespace Microsoft.AspNet.WebUtilities.Collections +namespace Microsoft.AspNet.PipelineCore.Collections { /// /// Accessors for query, forms, etc. @@ -17,13 +17,8 @@ namespace Microsoft.AspNet.WebUtilities.Collections /// Create a new wrapper /// /// - public ReadableStringCollection(IDictionary store) + public ReadableStringCollection([NotNull] IDictionary store) { - if (store == null) - { - throw new ArgumentNullException("store"); - } - Store = store; } @@ -75,7 +70,7 @@ namespace Microsoft.AspNet.WebUtilities.Collections /// public string Get(string key) { - return ParsingHelpers.GetJoinedValue(Store, key); + return GetJoinedValue(Store, key); } /// @@ -108,5 +103,15 @@ namespace Microsoft.AspNet.WebUtilities.Collections { return GetEnumerator(); } + + private static string GetJoinedValue(IDictionary store, string key) + { + string[] values; + if (store.TryGetValue(key, out values)) + { + return string.Join(",", values); + } + return null; + } } } diff --git a/src/Microsoft.AspNet.PipelineCore/Collections/ResponseCookies.cs b/src/Microsoft.AspNet.PipelineCore/Collections/ResponseCookies.cs index 27bfbbd38e..2412b54a83 100644 --- a/src/Microsoft.AspNet.PipelineCore/Collections/ResponseCookies.cs +++ b/src/Microsoft.AspNet.PipelineCore/Collections/ResponseCookies.cs @@ -19,13 +19,8 @@ namespace Microsoft.AspNet.PipelineCore.Collections /// Create a new wrapper /// /// - public ResponseCookies(IHeaderDictionary headers) + public ResponseCookies([NotNull] IHeaderDictionary headers) { - if (headers == null) - { - throw new ArgumentNullException("headers"); - } - Headers = headers; } @@ -47,13 +42,8 @@ namespace Microsoft.AspNet.PipelineCore.Collections /// /// /// - public void Append(string key, string value, CookieOptions options) + public void Append(string key, string value, [NotNull] CookieOptions options) { - if (options == null) - { - throw new ArgumentNullException("options"); - } - bool domainHasValue = !string.IsNullOrEmpty(options.Domain); bool pathHasValue = !string.IsNullOrEmpty(options.Path); bool expiresHasValue = options.Expires.HasValue; @@ -98,13 +88,8 @@ namespace Microsoft.AspNet.PipelineCore.Collections /// /// /// - public void Delete(string key, CookieOptions options) + public void Delete(string key, [NotNull] CookieOptions options) { - if (options == null) - { - throw new ArgumentNullException("options"); - } - bool domainHasValue = !string.IsNullOrEmpty(options.Domain); bool pathHasValue = !string.IsNullOrEmpty(options.Path); diff --git a/src/Microsoft.AspNet.PipelineCore/DefaultHttpContext.cs b/src/Microsoft.AspNet.PipelineCore/DefaultHttpContext.cs index 308d896564..3b445fefbe 100644 --- a/src/Microsoft.AspNet.PipelineCore/DefaultHttpContext.cs +++ b/src/Microsoft.AspNet.PipelineCore/DefaultHttpContext.cs @@ -214,12 +214,8 @@ namespace Microsoft.AspNet.PipelineCore return authTypeContext.Results; } - public override IEnumerable Authenticate(IEnumerable authenticationTypes) + public override IEnumerable Authenticate([NotNull] IEnumerable authenticationTypes) { - if (authenticationTypes == null) - { - throw new ArgumentNullException(); - } var handler = HttpAuthenticationFeature.Handler; var authenticateContext = new AuthenticateContext(authenticationTypes); @@ -238,12 +234,8 @@ namespace Microsoft.AspNet.PipelineCore return authenticateContext.Results; } - public override async Task> AuthenticateAsync(IEnumerable authenticationTypes) + public override async Task> AuthenticateAsync([NotNull] IEnumerable authenticationTypes) { - if (authenticationTypes == null) - { - throw new ArgumentNullException(); - } var handler = HttpAuthenticationFeature.Handler; var authenticateContext = new AuthenticateContext(authenticationTypes); diff --git a/src/Microsoft.AspNet.PipelineCore/DefaultHttpRequest.cs b/src/Microsoft.AspNet.PipelineCore/DefaultHttpRequest.cs index eac94536d6..da839c1f16 100644 --- a/src/Microsoft.AspNet.PipelineCore/DefaultHttpRequest.cs +++ b/src/Microsoft.AspNet.PipelineCore/DefaultHttpRequest.cs @@ -2,13 +2,12 @@ // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System; -using System.Globalization; using System.IO; using System.Threading; using System.Threading.Tasks; +using Microsoft.AspNet.FeatureModel; using Microsoft.AspNet.Http; using Microsoft.AspNet.Http.Infrastructure; -using Microsoft.AspNet.FeatureModel; using Microsoft.AspNet.HttpFeature; using Microsoft.AspNet.PipelineCore.Collections; using Microsoft.AspNet.PipelineCore.Infrastructure; @@ -55,7 +54,7 @@ namespace Microsoft.AspNet.PipelineCore private IFormFeature FormFeature { - get { return _form.Fetch(_features) ?? _form.Update(_features, new FormFeature(_features)); } + get { return _form.Fetch(_features) ?? _form.Update(_features, new FormFeature(this)); } } private IRequestCookiesFeature RequestCookiesFeature @@ -83,7 +82,7 @@ namespace Microsoft.AspNet.PipelineCore set { HttpRequestFeature.QueryString = value.Value; } } - public override long? ContentLength + public override long? ContentLength { get { @@ -129,11 +128,6 @@ namespace Microsoft.AspNet.PipelineCore get { return QueryFeature.Query; } } - public override Task GetFormAsync(CancellationToken cancellationToken = default(CancellationToken)) - { - return FormFeature.GetFormAsync(cancellationToken); - } - public override string Protocol { get { return HttpRequestFeature.Protocol; } @@ -167,5 +161,21 @@ namespace Microsoft.AspNet.PipelineCore get { return Headers[Constants.Headers.AcceptCharset]; } set { Headers[Constants.Headers.AcceptCharset] = value; } } + + public override bool HasFormContentType + { + get { return FormFeature.HasFormContentType; } + } + + public override IFormCollection Form + { + get { return FormFeature.ReadForm(); } + set { FormFeature.Form = value; } + } + + public override Task ReadFormAsync(CancellationToken cancellationToken) + { + return FormFeature.ReadFormAsync(cancellationToken); + } } } \ No newline at end of file diff --git a/src/Microsoft.AspNet.PipelineCore/DefaultHttpResponse.cs b/src/Microsoft.AspNet.PipelineCore/DefaultHttpResponse.cs index 03841751ec..16d187450d 100644 --- a/src/Microsoft.AspNet.PipelineCore/DefaultHttpResponse.cs +++ b/src/Microsoft.AspNet.PipelineCore/DefaultHttpResponse.cs @@ -129,12 +129,8 @@ namespace Microsoft.AspNet.PipelineCore Headers.Set(Constants.Headers.Location, location); } - public override void Challenge(AuthenticationProperties properties, IEnumerable authenticationTypes) + public override void Challenge(AuthenticationProperties properties, [NotNull] IEnumerable authenticationTypes) { - if (authenticationTypes == null) - { - throw new ArgumentNullException(); - } HttpResponseFeature.StatusCode = 401; var handler = HttpAuthenticationFeature.Handler; @@ -152,13 +148,8 @@ namespace Microsoft.AspNet.PipelineCore } } - public override void SignIn(AuthenticationProperties properties, IEnumerable identities) + public override void SignIn(AuthenticationProperties properties, [NotNull] IEnumerable identities) { - if (identities == null) - { - throw new ArgumentNullException(); - } - var handler = HttpAuthenticationFeature.Handler; var signInContext = new SignInContext(identities, properties == null ? null : properties.Dictionary); @@ -175,12 +166,8 @@ namespace Microsoft.AspNet.PipelineCore } } - public override void SignOut(IEnumerable authenticationTypes) + public override void SignOut([NotNull] IEnumerable authenticationTypes) { - if (authenticationTypes == null) - { - throw new ArgumentNullException(); - } var handler = HttpAuthenticationFeature.Handler; var signOutContext = new SignOutContext(authenticationTypes); diff --git a/src/Microsoft.AspNet.PipelineCore/FormFeature.cs b/src/Microsoft.AspNet.PipelineCore/FormFeature.cs index 271264b870..8d20871688 100644 --- a/src/Microsoft.AspNet.PipelineCore/FormFeature.cs +++ b/src/Microsoft.AspNet.PipelineCore/FormFeature.cs @@ -1,71 +1,192 @@ // 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.IO; +using System.Linq; using System.Text; using System.Threading; using System.Threading.Tasks; -using Microsoft.AspNet.FeatureModel; using Microsoft.AspNet.Http; -using Microsoft.AspNet.HttpFeature; -using Microsoft.AspNet.PipelineCore.Infrastructure; +using Microsoft.AspNet.PipelineCore.Collections; using Microsoft.AspNet.WebUtilities; -using Microsoft.AspNet.WebUtilities.Collections; namespace Microsoft.AspNet.PipelineCore { public class FormFeature : IFormFeature { - private readonly IFeatureCollection _features; - private readonly FeatureReference _request = FeatureReference.Default; - private Stream _bodyStream; - private IReadableStringCollection _form; + private readonly HttpRequest _request; - public FormFeature([NotNull] IDictionary form) - : this (new ReadableStringCollection(form)) + public FormFeature([NotNull] IFormCollection form) { + Form = form; } - public FormFeature([NotNull] IReadableStringCollection form) + public FormFeature([NotNull] HttpRequest request) { - _form = form; + _request = request; } - public FormFeature([NotNull] IFeatureCollection features) + public bool HasFormContentType { - _features = features; - } - - public async Task GetFormAsync(CancellationToken cancellationToken) - { - if (_features == null) + get { - return _form; + // Set directly + if (Form != null) + { + return true; + } + + return HasApplicationFormContentType() || HasMultipartFormContentType(); + } + } + + public IFormCollection Form { get; set; } + + public IFormCollection ReadForm() + { + if (Form != null) + { + return Form; } - var body = _request.Fetch(_features).Body; - - if (_bodyStream == null || _bodyStream != body) + if (!HasFormContentType) { - _bodyStream = body; - if (!_bodyStream.CanSeek) + throw new InvalidOperationException("Incorrect Content-Type: " + _request.ContentType); + } + + // TODO: How do we prevent thread exhaustion? + return ReadFormAsync(CancellationToken.None).GetAwaiter().GetResult(); + } + + public async Task ReadFormAsync(CancellationToken cancellationToken) + { + if (Form != null) + { + return Form; + } + + if (!HasFormContentType) + { + throw new InvalidOperationException("Incorrect Content-Type: " + _request.ContentType); + } + + cancellationToken.ThrowIfCancellationRequested(); + + _request.EnableRewind(); + + IDictionary formFields = null; + var files = new FormFileCollection(); + + // Some of these code paths use StreamReader which does not support cancellation tokens. + using (cancellationToken.Register(_request.HttpContext.Abort)) + { + // Check the content-type + if (HasApplicationFormContentType()) { - var buffer = new MemoryStream(); - await _bodyStream.CopyToAsync(buffer, 4096, cancellationToken); - _bodyStream = buffer; - _request.Fetch(_features).Body = _bodyStream; - _bodyStream.Seek(0, SeekOrigin.Begin); + // TODO: Read the charset from the content-type header after we get strongly typed headers + formFields = await FormReader.ReadFormAsync(_request.Body, cancellationToken); } - using (var streamReader = new StreamReader(_bodyStream, Encoding.UTF8, - detectEncodingFromByteOrderMarks: true, - bufferSize: 1024, leaveOpen: true)) + else if (HasMultipartFormContentType()) { - string form = await streamReader.ReadToEndAsync(); - _form = FormHelpers.ParseForm(form); + var formAccumulator = new KeyValueAccumulator(StringComparer.OrdinalIgnoreCase); + + var boundary = GetBoundary(_request.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"]; + if (HasFileContentDisposition(contentDisposition)) + { + // Find the end + await section.Body.DrainAsync(cancellationToken); + + var file = new FormFile(_request.Body, section.BaseStreamOffset.Value, section.Body.Length) + { + Headers = headers, + }; + files.Add(file); + } + else if (HasFormDataContentDisposition(contentDisposition)) + { + // Content-Disposition: form-data; name="key" + // + // 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 value = await reader.ReadToEndAsync(); + formAccumulator.Append(key, value); + } + } + else + { + System.Diagnostics.Debug.Assert(false, "Unrecognized content-disposition for this section: " + contentDisposition); + } + + section = await multipartReader.ReadNextSectionAsync(cancellationToken); + } + + formFields = formAccumulator.GetResults(); } } - return _form; + + Form = new FormCollection(formFields, files); + return Form; + } + + private bool HasApplicationFormContentType() + { + // 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] == '"') + { + boundary = boundary.Substring(1, boundary.Length - 2); + } + return boundary; } } } diff --git a/src/Microsoft.AspNet.PipelineCore/FormFile.cs b/src/Microsoft.AspNet.PipelineCore/FormFile.cs new file mode 100644 index 0000000000..19ea07466f --- /dev/null +++ b/src/Microsoft.AspNet.PipelineCore/FormFile.cs @@ -0,0 +1,46 @@ +// 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.IO; +using Microsoft.AspNet.Http; + +namespace Microsoft.AspNet.PipelineCore +{ + public class FormFile : IFormFile + { + private Stream _baseStream; + private long _baseStreamOffset; + private long _length; + + public FormFile(Stream baseStream, long baseStreamOffset, long length) + { + _baseStream = baseStream; + _baseStreamOffset = baseStreamOffset; + _length = length; + } + + public string ContentDisposition + { + get { return Headers["Content-Disposition"]; } + set { Headers["Content-Disposition"] = value; } + } + + public string ContentType + { + get { return Headers["Content-Type"]; } + set { Headers["Content-Type"] = value; } + } + + public IHeaderDictionary Headers { get; set; } + + public long Length + { + get { return _length; } + } + + public Stream OpenReadStream() + { + return new ReferenceReadStream(_baseStream, _baseStreamOffset, _length); + } + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNet.PipelineCore/IFormFeature.cs b/src/Microsoft.AspNet.PipelineCore/IFormFeature.cs index 07debf7870..cc84d32a4b 100644 --- a/src/Microsoft.AspNet.PipelineCore/IFormFeature.cs +++ b/src/Microsoft.AspNet.PipelineCore/IFormFeature.cs @@ -4,11 +4,33 @@ using System.Threading; using System.Threading.Tasks; using Microsoft.AspNet.Http; +using Microsoft.Framework.Runtime; namespace Microsoft.AspNet.PipelineCore { public interface IFormFeature { - Task GetFormAsync(CancellationToken cancellationToken); + /// + /// Indicates if the request has a supported form content-type. + /// + bool HasFormContentType { get; } + + /// + /// The parsed form, if any. + /// + IFormCollection Form { get; set; } + + /// + /// Parses the request body as a form. + /// + /// + IFormCollection ReadForm(); + + /// + /// Parses the request body as a form. + /// + /// + /// + Task ReadFormAsync(CancellationToken cancellationToken); } } diff --git a/src/Microsoft.AspNet.PipelineCore/Infrastructure/ParsingHelpers.cs b/src/Microsoft.AspNet.PipelineCore/Infrastructure/ParsingHelpers.cs index 10c9ef3672..e0ed2c7333 100644 --- a/src/Microsoft.AspNet.PipelineCore/Infrastructure/ParsingHelpers.cs +++ b/src/Microsoft.AspNet.PipelineCore/Infrastructure/ParsingHelpers.cs @@ -445,12 +445,8 @@ namespace Microsoft.AspNet.PipelineCore.Infrastructure #endregion - public bool StartsWith(string text, StringComparison comparisonType) + public bool StartsWith([NotNull] string text, StringComparison comparisonType) { - if (text == null) - { - throw new ArgumentNullException("text"); - } int textLength = text.Length; if (!HasValue || _count < textLength) { @@ -460,12 +456,8 @@ namespace Microsoft.AspNet.PipelineCore.Infrastructure return string.Compare(_buffer, _offset, text, 0, textLength, comparisonType) == 0; } - public bool EndsWith(string text, StringComparison comparisonType) + public bool EndsWith([NotNull] string text, StringComparison comparisonType) { - if (text == null) - { - throw new ArgumentNullException("text"); - } int textLength = text.Length; if (!HasValue || _count < textLength) { @@ -475,12 +467,8 @@ namespace Microsoft.AspNet.PipelineCore.Infrastructure return string.Compare(_buffer, _offset + _count - textLength, text, 0, textLength, comparisonType) == 0; } - public bool Equals(string text, StringComparison comparisonType) + public bool Equals([NotNull] string text, StringComparison comparisonType) { - if (text == null) - { - throw new ArgumentNullException("text"); - } int textLength = text.Length; if (!HasValue || _count != textLength) { @@ -615,25 +603,17 @@ namespace Microsoft.AspNet.PipelineCore.Infrastructure } } - public static string[] GetHeaderUnmodified(IDictionary headers, string key) + public static string[] GetHeaderUnmodified([NotNull] IDictionary headers, string key) { - if (headers == null) - { - throw new ArgumentNullException("headers"); - } string[] values; return headers.TryGetValue(key, out values) ? values : null; } - public static void SetHeader(IDictionary headers, string key, string value) + public static void SetHeader([NotNull] IDictionary headers, [NotNull] string key, string value) { - if (headers == null) - { - throw new ArgumentNullException("headers"); - } if (string.IsNullOrWhiteSpace(key)) { - throw new ArgumentNullException("key"); + throw new ArgumentNullException(nameof(key)); } if (string.IsNullOrWhiteSpace(value)) { @@ -645,15 +625,11 @@ namespace Microsoft.AspNet.PipelineCore.Infrastructure } } - public static void SetHeaderJoined(IDictionary headers, string key, params string[] values) + public static void SetHeaderJoined([NotNull] IDictionary headers, [NotNull] string key, params string[] values) { - if (headers == null) - { - throw new ArgumentNullException("headers"); - } if (string.IsNullOrWhiteSpace(key)) { - throw new ArgumentNullException("key"); + throw new ArgumentNullException(nameof(key)); } if (values == null || values.Length == 0) { @@ -697,15 +673,11 @@ namespace Microsoft.AspNet.PipelineCore.Infrastructure return value; } - public static void SetHeaderUnmodified(IDictionary headers, string key, params string[] values) + public static void SetHeaderUnmodified([NotNull] IDictionary headers, [NotNull] string key, params string[] values) { - if (headers == null) - { - throw new ArgumentNullException("headers"); - } if (string.IsNullOrWhiteSpace(key)) { - throw new ArgumentNullException("key"); + throw new ArgumentNullException(nameof(key)); } if (values == null || values.Length == 0) { @@ -717,16 +689,12 @@ namespace Microsoft.AspNet.PipelineCore.Infrastructure } } - public static void SetHeaderUnmodified(IDictionary headers, string key, IEnumerable values) + public static void SetHeaderUnmodified([NotNull] IDictionary headers, [NotNull] string key, [NotNull] IEnumerable values) { - if (headers == null) - { - throw new ArgumentNullException("headers"); - } headers[key] = values.ToArray(); } - public static void AppendHeader(IDictionary headers, string key, string values) + public static void AppendHeader([NotNull] IDictionary headers, [NotNull] string key, string values) { if (string.IsNullOrWhiteSpace(values)) { @@ -744,7 +712,7 @@ namespace Microsoft.AspNet.PipelineCore.Infrastructure } } - public static void AppendHeaderJoined(IDictionary headers, string key, params string[] values) + public static void AppendHeaderJoined([NotNull] IDictionary headers, [NotNull] string key, params string[] values) { if (values == null || values.Length == 0) { @@ -762,7 +730,7 @@ namespace Microsoft.AspNet.PipelineCore.Infrastructure } } - public static void AppendHeaderUnmodified(IDictionary headers, string key, params string[] values) + public static void AppendHeaderUnmodified([NotNull] IDictionary headers, [NotNull] string key, params string[] values) { if (values == null || values.Length == 0) { @@ -801,12 +769,8 @@ namespace Microsoft.AspNet.PipelineCore.Infrastructure return values == null ? null : string.Join(",", values); } - internal static string[] GetUnmodifiedValues(IDictionary store, string key) + internal static string[] GetUnmodifiedValues([NotNull] IDictionary store, string key) { - if (store == null) - { - throw new ArgumentNullException("store"); - } string[] values; return store.TryGetValue(key, out values) ? values : null; } @@ -826,7 +790,7 @@ namespace Microsoft.AspNet.PipelineCore.Infrastructure // return string.IsNullOrWhiteSpace(localPort) ? localIpAddress : (localIpAddress + ":" + localPort); //} - public static long? GetContentLength(IHeaderDictionary headers) + public static long? GetContentLength([NotNull] IHeaderDictionary headers) { const NumberStyles styles = NumberStyles.AllowLeadingWhite | NumberStyles.AllowTrailingWhite; long value; @@ -840,7 +804,7 @@ namespace Microsoft.AspNet.PipelineCore.Infrastructure return null; } - public static void SetContentLength(IHeaderDictionary headers, long? value) + public static void SetContentLength([NotNull] IHeaderDictionary headers, long? value) { if (value.HasValue) { diff --git a/src/Microsoft.AspNet.PipelineCore/QueryFeature.cs b/src/Microsoft.AspNet.PipelineCore/QueryFeature.cs index 5106808d03..e57b406153 100644 --- a/src/Microsoft.AspNet.PipelineCore/QueryFeature.cs +++ b/src/Microsoft.AspNet.PipelineCore/QueryFeature.cs @@ -5,9 +5,9 @@ using System.Collections.Generic; using Microsoft.AspNet.FeatureModel; using Microsoft.AspNet.Http; using Microsoft.AspNet.HttpFeature; +using Microsoft.AspNet.PipelineCore.Collections; using Microsoft.AspNet.PipelineCore.Infrastructure; using Microsoft.AspNet.WebUtilities; -using Microsoft.AspNet.WebUtilities.Collections; namespace Microsoft.AspNet.PipelineCore { @@ -46,7 +46,7 @@ namespace Microsoft.AspNet.PipelineCore if (_query == null || _queryString != queryString) { _queryString = queryString; - _query = QueryHelpers.ParseQuery(queryString); + _query = new ReadableStringCollection(QueryHelpers.ParseQuery(queryString)); } return _query; } diff --git a/src/Microsoft.AspNet.PipelineCore/ReferenceReadStream.cs b/src/Microsoft.AspNet.PipelineCore/ReferenceReadStream.cs new file mode 100644 index 0000000000..aaad97ae92 --- /dev/null +++ b/src/Microsoft.AspNet.PipelineCore/ReferenceReadStream.cs @@ -0,0 +1,199 @@ +// 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.IO; +using System.Threading; +using System.Threading.Tasks; + +namespace Microsoft.AspNet.PipelineCore +{ + /// + /// A Stream that wraps another stream starting at a certain offset and reading for the given length. + /// + internal class ReferenceReadStream : Stream + { + private readonly Stream _inner; + private readonly long _innerOffset; + private readonly long _length; + private long _position; + + private bool _disposed; + + public ReferenceReadStream([NotNull] Stream inner, long offset, long length) + { + _inner = inner; + _innerOffset = offset; + _length = length; + _inner.Position = offset; + } + + public override bool CanRead + { + get { return true; } + } + + public override bool CanSeek + { + get { return _inner.CanSeek; } + } + + public override bool CanWrite + { + get { return false; } + } + + public override long Length + { + get { return _length; } + } + + public override long Position + { + get { return _position; } + set + { + ThrowIfDisposed(); + if (value < 0 || value > Length) + { + throw new ArgumentOutOfRangeException("value", value, "The Position must be within the length of the Stream: " + Length); + } + VerifyPosition(); + _position = value; + _inner.Position = _innerOffset + _position; + } + } + + // Throws if the position in the underlying stream has changed without our knowledge, indicating someone else is trying + // to use the stream at the same time which could lead to data corruption. + private void VerifyPosition() + { + if (_inner.Position != _innerOffset + _position) + { + throw new InvalidOperationException("The inner stream position has changed unexpectedly."); + } + } + + public override long Seek(long offset, SeekOrigin origin) + { + if (origin == SeekOrigin.Begin) + { + Position = offset; + } + else if (origin == SeekOrigin.End) + { + Position = Length + offset; + } + else // if (origin == SeekOrigin.Current) + { + Position = Position + offset; + } + return Position; + } + + public override int Read(byte[] buffer, int offset, int count) + { + ThrowIfDisposed(); + VerifyPosition(); + var toRead = Math.Min(count, _length - _position); + var read = _inner.Read(buffer, offset, (int)toRead); + _position += read; + return read; + } + + public override async Task ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) + { + ThrowIfDisposed(); + VerifyPosition(); + var toRead = Math.Min(count, _length - _position); + var read = await _inner.ReadAsync(buffer, offset, (int)toRead, cancellationToken); + _position += read; + return read; + } +#if ASPNET50 + public override IAsyncResult BeginRead(byte[] buffer, int offset, int count, AsyncCallback callback, object state) + { + ThrowIfDisposed(); + VerifyPosition(); + var tcs = new TaskCompletionSource(state); + BeginRead(buffer, offset, count, callback, tcs); + return tcs.Task; + } + + private async void BeginRead(byte[] buffer, int offset, int count, AsyncCallback callback, TaskCompletionSource tcs) + { + try + { + var read = await ReadAsync(buffer, offset, count); + tcs.TrySetResult(read); + } + catch (Exception ex) + { + tcs.TrySetException(ex); + } + + if (callback != null) + { + try + { + callback(tcs.Task); + } + catch (Exception) + { + // Suppress exceptions on background threads. + } + } + } + + public override int EndRead(IAsyncResult asyncResult) + { + var task = (Task)asyncResult; + return task.GetAwaiter().GetResult(); + } +#endif + public override void Write(byte[] buffer, int offset, int count) + { + throw new NotSupportedException(); + } + public override Task WriteAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) + { + throw new NotSupportedException(); + } +#if ASPNET50 + public override IAsyncResult BeginWrite(byte[] buffer, int offset, int count, AsyncCallback callback, object state) + { + throw new NotSupportedException(); + } + + public override void EndWrite(IAsyncResult asyncResult) + { + throw new NotSupportedException(); + } +#endif + public override void SetLength(long value) + { + throw new NotSupportedException(); + } + + public override void Flush() + { + throw new NotSupportedException(); + } + + protected override void Dispose(bool disposing) + { + if (disposing) + { + _disposed = true; + } + } + + private void ThrowIfDisposed() + { + if (_disposed) + { + throw new ObjectDisposedException(nameof(ReferenceReadStream)); + } + } + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNet.PipelineCore/RequestCookiesFeature.cs b/src/Microsoft.AspNet.PipelineCore/RequestCookiesFeature.cs index 8a3ae99e72..952bfed0f6 100644 --- a/src/Microsoft.AspNet.PipelineCore/RequestCookiesFeature.cs +++ b/src/Microsoft.AspNet.PipelineCore/RequestCookiesFeature.cs @@ -9,7 +9,6 @@ using Microsoft.AspNet.Http.Infrastructure; using Microsoft.AspNet.HttpFeature; using Microsoft.AspNet.PipelineCore.Collections; using Microsoft.AspNet.PipelineCore.Infrastructure; -using Microsoft.AspNet.WebUtilities.Collections; namespace Microsoft.AspNet.PipelineCore { diff --git a/src/Microsoft.AspNet.PipelineCore/Security/AuthenticateContext.cs b/src/Microsoft.AspNet.PipelineCore/Security/AuthenticateContext.cs index d975333fdf..1e27a006c4 100644 --- a/src/Microsoft.AspNet.PipelineCore/Security/AuthenticateContext.cs +++ b/src/Microsoft.AspNet.PipelineCore/Security/AuthenticateContext.cs @@ -17,12 +17,8 @@ namespace Microsoft.AspNet.PipelineCore.Security private List _results; private List _accepted; - public AuthenticateContext(IEnumerable authenticationTypes) + public AuthenticateContext([NotNull] IEnumerable authenticationTypes) { - if (authenticationTypes == null) - { - throw new ArgumentNullException("authenticationType"); - } AuthenticationTypes = authenticationTypes; _results = new List(); _accepted = new List(); diff --git a/src/Microsoft.AspNet.PipelineCore/Security/ChallengeContext.cs b/src/Microsoft.AspNet.PipelineCore/Security/ChallengeContext.cs index 45afd34594..ea87d06599 100644 --- a/src/Microsoft.AspNet.PipelineCore/Security/ChallengeContext.cs +++ b/src/Microsoft.AspNet.PipelineCore/Security/ChallengeContext.cs @@ -14,12 +14,8 @@ namespace Microsoft.AspNet.PipelineCore.Security { private List _accepted; - public ChallengeContext(IEnumerable authenticationTypes, IDictionary properties) + public ChallengeContext([NotNull] IEnumerable authenticationTypes, IDictionary properties) { - if (authenticationTypes == null) - { - throw new ArgumentNullException(); - } AuthenticationTypes = authenticationTypes; Properties = properties ?? new Dictionary(StringComparer.Ordinal); _accepted = new List(); @@ -33,7 +29,7 @@ namespace Microsoft.AspNet.PipelineCore.Security { get { return _accepted; } } - + public void Accept(string authenticationType, IDictionary description) { _accepted.Add(authenticationType); diff --git a/src/Microsoft.AspNet.PipelineCore/Security/SignInContext.cs b/src/Microsoft.AspNet.PipelineCore/Security/SignInContext.cs index b24a14758a..43a88a82de 100644 --- a/src/Microsoft.AspNet.PipelineCore/Security/SignInContext.cs +++ b/src/Microsoft.AspNet.PipelineCore/Security/SignInContext.cs @@ -12,12 +12,8 @@ namespace Microsoft.AspNet.PipelineCore.Security { private List _accepted; - public SignInContext(IEnumerable identities, IDictionary dictionary) + public SignInContext([NotNull] IEnumerable identities, IDictionary dictionary) { - if (identities == null) - { - throw new ArgumentNullException("identities"); - } Identities = identities; Properties = dictionary ?? new Dictionary(StringComparer.Ordinal); _accepted = new List(); diff --git a/src/Microsoft.AspNet.PipelineCore/Security/SignOutContext.cs b/src/Microsoft.AspNet.PipelineCore/Security/SignOutContext.cs index 6ce2a64fa5..546c5bca83 100644 --- a/src/Microsoft.AspNet.PipelineCore/Security/SignOutContext.cs +++ b/src/Microsoft.AspNet.PipelineCore/Security/SignOutContext.cs @@ -11,12 +11,8 @@ namespace Microsoft.AspNet.PipelineCore.Security { private List _accepted; - public SignOutContext(IEnumerable authenticationTypes) + public SignOutContext([NotNull] IEnumerable authenticationTypes) { - if (authenticationTypes == null) - { - throw new ArgumentNullException("authenticationTypes"); - } AuthenticationTypes = authenticationTypes; _accepted = new List(); } diff --git a/src/Microsoft.AspNet.WebUtilities/BufferedReadStream.cs b/src/Microsoft.AspNet.WebUtilities/BufferedReadStream.cs new file mode 100644 index 0000000000..944f126b93 --- /dev/null +++ b/src/Microsoft.AspNet.WebUtilities/BufferedReadStream.cs @@ -0,0 +1,396 @@ +// 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.IO; +using System.Text; +using System.Threading; +using System.Threading.Tasks; + +namespace Microsoft.AspNet.WebUtilities +{ + internal class BufferedReadStream : Stream + { + private const char CR = '\r'; + private const char LF = '\n'; + + private readonly Stream _inner; + private readonly byte[] _buffer; + private int _bufferOffset = 0; + private int _bufferCount = 0; + private bool _disposed; + + public BufferedReadStream([NotNull] Stream inner, int bufferSize) + { + _inner = inner; + _buffer = new byte[bufferSize]; + } + + public ArraySegment BufferedData + { + get { return new ArraySegment(_buffer, _bufferOffset, _bufferCount); } + } + + public override bool CanRead + { + get { return _inner.CanRead || _bufferCount > 0; } + } + + public override bool CanSeek + { + get { return _inner.CanSeek; } + } + + public override bool CanTimeout + { + get { return _inner.CanTimeout; } + } + + public override bool CanWrite + { + get { return _inner.CanWrite; } + } + + public override long Length + { + get { return _inner.Length; } + } + + public override long Position + { + get { return _inner.Position - _bufferCount; } + set + { + if (value < 0) + { + throw new ArgumentOutOfRangeException("value", value, "Position must be positive."); + } + if (value == Position) + { + return; + } + + // Backwards? + if (value <= _inner.Position) + { + // Forward within the buffer? + var innerOffset = (int)(_inner.Position - value); + if (innerOffset <= _bufferCount) + { + // Yes, just skip some of the buffered data + _bufferOffset += innerOffset; + _bufferCount -= innerOffset; + } + else + { + // No, reset the buffer + _bufferOffset = 0; + _bufferCount = 0; + _inner.Position = value; + } + } + else + { + // Forward, reset the buffer + _bufferOffset = 0; + _bufferCount = 0; + _inner.Position = value; + } + } + } + + public override long Seek(long offset, SeekOrigin origin) + { + if (origin == SeekOrigin.Begin) + { + Position = offset; + } + else if (origin == SeekOrigin.Current) + { + Position = Position + offset; + } + else // if (origin == SeekOrigin.End) + { + Position = Length + offset; + } + return Position; + } + + public override void SetLength(long value) + { + _inner.SetLength(value); + } + + protected override void Dispose(bool disposing) + { + _disposed = true; + if (disposing) + { + _inner.Dispose(); + } + } + + public override void Flush() + { + _inner.Flush(); + } + + public override Task FlushAsync(CancellationToken cancellationToken) + { + return _inner.FlushAsync(cancellationToken); + } + + public override void Write(byte[] buffer, int offset, int count) + { + _inner.Write(buffer, offset, count); + } +#if ASPNET50 + public override IAsyncResult BeginWrite(byte[] buffer, int offset, int count, AsyncCallback callback, object state) + { + return _inner.BeginWrite(buffer, offset, count, callback, state); + } + + public override void EndWrite(IAsyncResult asyncResult) + { + _inner.EndWrite(asyncResult); + } +#endif + public override Task WriteAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) + { + return _inner.WriteAsync(buffer, offset, count, cancellationToken); + } + + public override int Read(byte[] buffer, int offset, int count) + { + ValidateBuffer(buffer, offset, count); + + // Drain buffer + if (_bufferCount > 0) + { + int toCopy = Math.Min(_bufferCount, count); + Buffer.BlockCopy(_buffer, _bufferOffset, buffer, offset, toCopy); + _bufferOffset += toCopy; + _bufferCount -= toCopy; + return toCopy; + } + + return _inner.Read(buffer, offset, count); + } + + public async override Task ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) + { + ValidateBuffer(buffer, offset, count); + + // Drain buffer + if (_bufferCount > 0) + { + int toCopy = Math.Min(_bufferCount, count); + Buffer.BlockCopy(_buffer, _bufferOffset, buffer, offset, toCopy); + _bufferOffset += toCopy; + _bufferCount -= toCopy; + return toCopy; + } + + return await _inner.ReadAsync(buffer, offset, count, cancellationToken); + } +#if ASPNET50 + // We only anticipate using ReadAsync + public override IAsyncResult BeginRead(byte[] buffer, int offset, int count, AsyncCallback callback, object state) + { + ValidateBuffer(buffer, offset, count); + + // Drain buffer + if (_bufferCount > 0) + { + int toCopy = Math.Min(_bufferCount, count); + Buffer.BlockCopy(_buffer, _bufferOffset, buffer, offset, toCopy); + _bufferOffset += toCopy; + _bufferCount -= toCopy; + + TaskCompletionSource tcs = new TaskCompletionSource(state); + tcs.TrySetResult(toCopy); + if (callback != null) + { + callback(tcs.Task); + } + return tcs.Task; + } + + return _inner.BeginRead(buffer, offset, count, callback, state); + } + + public override int EndRead(IAsyncResult asyncResult) + { + Task task = asyncResult as Task; + if (task != null) + { + return task.GetAwaiter().GetResult(); + } + return _inner.EndRead(asyncResult); + } +#endif + public bool EnsureBuffered() + { + if (_bufferCount > 0) + { + return true; + } + // Downshift to make room + _bufferOffset = 0; + _bufferCount = _inner.Read(_buffer, 0, _buffer.Length); + return _bufferCount > 0; + } + + public async Task EnsureBufferedAsync(CancellationToken cancellationToken) + { + if (_bufferCount > 0) + { + return true; + } + // Downshift to make room + _bufferOffset = 0; + _bufferCount = await _inner.ReadAsync(_buffer, 0, _buffer.Length, cancellationToken); + return _bufferCount > 0; + } + + public bool EnsureBuffered(int minCount) + { + if (minCount > _buffer.Length) + { + throw new ArgumentOutOfRangeException(nameof(minCount), minCount, "The value must be smaller than the buffer size: " + _buffer.Length); + } + while (_bufferCount < minCount) + { + // Downshift to make room + if (_bufferOffset > 0) + { + if (_bufferCount > 0) + { + Buffer.BlockCopy(_buffer, _bufferOffset, _buffer, 0, _bufferCount); + } + _bufferOffset = 0; + } + int read = _inner.Read(_buffer, _bufferOffset + _bufferCount, _buffer.Length - _bufferCount - _bufferOffset); + _bufferCount += read; + if (read == 0) + { + return false; + } + } + return true; + } + + public async Task EnsureBufferedAsync(int minCount, CancellationToken cancellationToken) + { + if (minCount > _buffer.Length) + { + throw new ArgumentOutOfRangeException(nameof(minCount), minCount, "The value must be smaller than the buffer size: " + _buffer.Length); + } + while (_bufferCount < minCount) + { + // Downshift to make room + if (_bufferOffset > 0) + { + if (_bufferCount > 0) + { + Buffer.BlockCopy(_buffer, _bufferOffset, _buffer, 0, _bufferCount); + } + _bufferOffset = 0; + } + int read = await _inner.ReadAsync(_buffer, _bufferOffset + _bufferCount, _buffer.Length - _bufferCount - _bufferOffset, cancellationToken); + _bufferCount += read; + if (read == 0) + { + return false; + } + } + return true; + } + + public string ReadLine(int lengthLimit) + { + CheckDisposed(); + StringBuilder builder = new StringBuilder(); + bool foundCR = false, foundCRLF = false; + while (!foundCRLF && EnsureBuffered()) + { + if (builder.Length > lengthLimit) + { + throw new InvalidOperationException("Line length limit exceeded: " + lengthLimit); + } + ProcessLineChar(builder, ref foundCR, ref foundCRLF); + } + + if (foundCRLF) + { + return builder.ToString(0, builder.Length - 2); // Drop the CRLF + } + // Stream ended with no CRLF. + return builder.ToString(); + } + + public async Task ReadLineAsync(int lengthLimit, CancellationToken cancellationToken) + { + CheckDisposed(); + StringBuilder builder = new StringBuilder(); + bool foundCR = false, foundCRLF = false; + while (!foundCRLF && await EnsureBufferedAsync(cancellationToken)) + { + if (builder.Length > lengthLimit) + { + throw new InvalidOperationException("Line length limit exceeded: " + lengthLimit); + } + + ProcessLineChar(builder, ref foundCR, ref foundCRLF); + } + + if (foundCRLF) + { + return builder.ToString(0, builder.Length - 2); // Drop the CRLF + } + // Stream ended with no CRLF. + return builder.ToString(); + } + + private void ProcessLineChar(StringBuilder builder, ref bool foundCR, ref bool foundCRLF) + { + char ch = (char)_buffer[_bufferOffset]; // TODO: Encoding enforcement + builder.Append(ch); + _bufferOffset++; + _bufferCount--; + if (ch == CR) + { + foundCR = true; + } + else if (ch == LF) + { + if (foundCR) + { + foundCRLF = true; + } + else + { + foundCR = false; + } + } + } + + private void CheckDisposed() + { + if (_disposed) + { + throw new ObjectDisposedException(nameof(BufferedReadStream)); + } + } + + private void ValidateBuffer(byte[] buffer, int offset, int count) + { + // Delegate most of our validation. + var ignored = new ArraySegment(buffer, offset, count); + if (count == 0) + { + throw new ArgumentOutOfRangeException(nameof(count), "The value must be greater than zero."); + } + } + } +} diff --git a/src/Microsoft.AspNet.WebUtilities/FileBufferingReadStream.cs b/src/Microsoft.AspNet.WebUtilities/FileBufferingReadStream.cs new file mode 100644 index 0000000000..2f23ab5247 --- /dev/null +++ b/src/Microsoft.AspNet.WebUtilities/FileBufferingReadStream.cs @@ -0,0 +1,248 @@ +// 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.IO; +using System.Threading; +using System.Threading.Tasks; + +namespace Microsoft.AspNet.WebUtilities +{ + /// + /// A Stream that wraps another stream and enables rewinding by buffering the content as it is read. + /// The content is buffered in memory up to a certain size and then spooled to a temp file on disk. + /// The temp file will be deleted on Dispose. + /// + public class FileBufferingReadStream : Stream + { + private readonly Stream _inner; + private readonly int _memoryThreshold; + private readonly string _tempFileDirectory; + + private Stream _buffer = new MemoryStream(); // TODO: We could have a more efficiently expanding buffer stream. + private bool _inMemory = true; + private bool _completelyBuffered; + + private bool _disposed; + + // TODO: allow for an optional buffer size limit to prevent filling hard disks. 1gb? + public FileBufferingReadStream([NotNull] Stream inner, int memoryThreshold, [NotNull] string tempFileDirectory) + { + _inner = inner; + _memoryThreshold = memoryThreshold; + _tempFileDirectory = tempFileDirectory; + } + + public override bool CanRead + { + get { return true; } + } + + public override bool CanSeek + { + get { return true; } + } + + public override bool CanWrite + { + get { return false; } + } + + public override long Length + { + get { return _buffer.Length; } + } + + public override long Position + { + get { return _buffer.Position; } + // Note this will not allow seeking forward beyond the end of the buffer. + set + { + ThrowIfDisposed(); + _buffer.Position = value; + } + } + + public override long Seek(long offset, SeekOrigin origin) + { + ThrowIfDisposed(); + if (!_completelyBuffered && origin == SeekOrigin.End) + { + // Can't seek from the end until we've finished consuming the inner stream + throw new NotSupportedException("The content has not been fully buffered yet."); + } + else if (!_completelyBuffered && origin == SeekOrigin.Current && offset + Position > Length) + { + // Can't seek past the end of the buffer until we've finished consuming the inner stream + throw new NotSupportedException("The content has not been fully buffered yet."); + } + else if (!_completelyBuffered && origin == SeekOrigin.Begin && offset > Length) + { + // Can't seek past the end of the buffer until we've finished consuming the inner stream + throw new NotSupportedException("The content has not been fully buffered yet."); + } + return _buffer.Seek(offset, origin); + } + + private Stream CreateTempFile() + { + var fileName = Path.Combine(_tempFileDirectory, "ASPNET_" + Guid.NewGuid().ToString() + ".tmp"); + return new FileStream(fileName, FileMode.Create, FileAccess.ReadWrite, FileShare.Delete, 1024 * 16, + FileOptions.Asynchronous | FileOptions.DeleteOnClose | FileOptions.SequentialScan); + } + + public override int Read(byte[] buffer, int offset, int count) + { + ThrowIfDisposed(); + if (_buffer.Position < _buffer.Length || _completelyBuffered) + { + // Just read from the buffer + return _buffer.Read(buffer, offset, (int)Math.Min(count, _buffer.Length - _buffer.Position)); + } + + int read = _inner.Read(buffer, offset, count); + + if (_inMemory && _buffer.Length + read > _memoryThreshold) + { + var oldBuffer = _buffer; + _buffer = CreateTempFile(); + _inMemory = false; + oldBuffer.Position = 0; + oldBuffer.CopyTo(_buffer, 1024 * 16); + } + + if (read > 0) + { + _buffer.Write(buffer, offset, read); + } + else + { + _completelyBuffered = true; + } + + return read; + } +#if ASPNET50 + public override IAsyncResult BeginRead(byte[] buffer, int offset, int count, AsyncCallback callback, object state) + { + ThrowIfDisposed(); + var tcs = new TaskCompletionSource(state); + BeginRead(buffer, offset, count, callback, tcs); + return tcs.Task; + } + + private async void BeginRead(byte[] buffer, int offset, int count, AsyncCallback callback, TaskCompletionSource tcs) + { + try + { + var read = await ReadAsync(buffer, offset, count); + tcs.TrySetResult(read); + } + catch (Exception ex) + { + tcs.TrySetException(ex); + } + + if (callback != null) + { + try + { + callback(tcs.Task); + } + catch (Exception) + { + // Suppress exceptions on background threads. + } + } + } + + public override int EndRead(IAsyncResult asyncResult) + { + var task = (Task)asyncResult; + return task.GetAwaiter().GetResult(); + } +#endif + public override async Task ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) + { + ThrowIfDisposed(); + if (_buffer.Position < _buffer.Length || _completelyBuffered) + { + // Just read from the buffer + return await _buffer.ReadAsync(buffer, offset, (int)Math.Min(count, _buffer.Length - _buffer.Position), cancellationToken); + } + + int read = await _inner.ReadAsync(buffer, offset, count, cancellationToken); + + if (_inMemory && _buffer.Length + read > _memoryThreshold) + { + var oldBuffer = _buffer; + _buffer = CreateTempFile(); + _inMemory = false; + oldBuffer.Position = 0; + await oldBuffer.CopyToAsync(_buffer, 1024 * 16, cancellationToken); + } + + if (read > 0) + { + await _buffer.WriteAsync(buffer, offset, read, cancellationToken); + } + else + { + _completelyBuffered = true; + } + + return read; + } + + public override void Write(byte[] buffer, int offset, int count) + { + throw new NotSupportedException(); + } +#if ASPNET50 + public override IAsyncResult BeginWrite(byte[] buffer, int offset, int count, AsyncCallback callback, object state) + { + throw new NotSupportedException(); + } + + public override void EndWrite(IAsyncResult asyncResult) + { + throw new NotSupportedException(); + } +#endif + public override Task WriteAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) + { + throw new NotSupportedException(); + } + + public override void SetLength(long value) + { + throw new NotSupportedException(); + } + + public override void Flush() + { + throw new NotSupportedException(); + } + + protected override void Dispose(bool disposing) + { + if (!_disposed) + { + _disposed = true; + if (disposing) + { + _buffer.Dispose(); + } + } + } + + private void ThrowIfDisposed() + { + if (_disposed) + { + throw new ObjectDisposedException(nameof(FileBufferingReadStream)); + } + } + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNet.WebUtilities/FormHelpers.cs b/src/Microsoft.AspNet.WebUtilities/FormHelpers.cs deleted file mode 100644 index 4c16485eb0..0000000000 --- a/src/Microsoft.AspNet.WebUtilities/FormHelpers.cs +++ /dev/null @@ -1,21 +0,0 @@ -// 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 Microsoft.AspNet.Http; - -namespace Microsoft.AspNet.WebUtilities -{ - public static class FormHelpers - { - /// - /// Parses an HTTP form body. - /// - /// The HTTP form body to parse. - /// The object containing the parsed HTTP form body. - public static IFormCollection ParseForm(string text) - { - return ParsingHelpers.GetForm(text); - } - } -} \ No newline at end of file diff --git a/src/Microsoft.AspNet.WebUtilities/FormReader.cs b/src/Microsoft.AspNet.WebUtilities/FormReader.cs new file mode 100644 index 0000000000..7c6a034f78 --- /dev/null +++ b/src/Microsoft.AspNet.WebUtilities/FormReader.cs @@ -0,0 +1,188 @@ +// 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.IO; +using System.Text; +using System.Threading; +using System.Threading.Tasks; + +namespace Microsoft.AspNet.WebUtilities +{ + /// + /// Used to read an 'application/x-www-form-urlencoded' form. + /// + public class FormReader + { + private readonly TextReader _reader; + private readonly char[] _buffer = new char[1024]; + private readonly StringBuilder _builder = new StringBuilder(); + private int _bufferOffset; + private int _bufferCount; + + public FormReader([NotNull] string data) + { + _reader = new StringReader(data); + } + + // TODO: Encoding + public FormReader([NotNull] Stream stream) + { + _reader = new StreamReader(stream, Encoding.UTF8, detectEncodingFromByteOrderMarks: true, bufferSize: 1024 * 2, leaveOpen: true); + } + + // Format: key1=value1&key2=value2 + /// + /// Reads the next key value pair from the form. + /// For unbuffered data use the async overload instead. + /// + /// The next key value pair, or null when the end of the form is reached. + public KeyValuePair? ReadNextPair() + { + var key = ReadWord('='); + if (string.IsNullOrEmpty(key) && _bufferCount == 0) + { + return null; + } + var value = ReadWord('&'); + return new KeyValuePair(key, value); + } + + // Format: key1=value1&key2=value2 + /// + /// Asynchronously reads the next key value pair from the form. + /// + /// + /// The next key value pair, or null when the end of the form is reached. + public async Task?> ReadNextPairAsync(CancellationToken cancellationToken) + { + var key = await ReadWordAsync('=', cancellationToken); + if (string.IsNullOrEmpty(key) && _bufferCount == 0) + { + return null; + } + var value = await ReadWordAsync('&', cancellationToken); + return new KeyValuePair(key, value); + } + + private string ReadWord(char seperator) + { + // TODO: Configurable value size limit + while (true) + { + // Empty + if (_bufferCount == 0) + { + Buffer(); + } + + // End + if (_bufferCount == 0) + { + return BuildWord(); + } + + var c = _buffer[_bufferOffset++]; + _bufferCount--; + + if (c == seperator) + { + return BuildWord(); + } + _builder.Append(c); + } + } + + private async Task ReadWordAsync(char seperator, CancellationToken cancellationToken) + { + // TODO: Configurable value size limit + while (true) + { + // Empty + if (_bufferCount == 0) + { + await BufferAsync(cancellationToken); + } + + // End + if (_bufferCount == 0) + { + return BuildWord(); + } + + var c = _buffer[_bufferOffset++]; + _bufferCount--; + + if (c == seperator) + { + return BuildWord(); + } + _builder.Append(c); + } + } + + // '+' un-escapes to ' ', %HH un-escapes as ASCII (or utf-8?) + private string BuildWord() + { + _builder.Replace('+', ' '); + var result = _builder.ToString(); + _builder.Clear(); + return Uri.UnescapeDataString(result); // TODO: Replace this, it's not completely accurate. + } + + private void Buffer() + { + _bufferOffset = 0; + _bufferCount = _reader.Read(_buffer, 0, _buffer.Length); + } + + private async Task BufferAsync(CancellationToken cancellationToken) + { + // TODO: StreamReader doesn't support cancellation? + cancellationToken.ThrowIfCancellationRequested(); + _bufferOffset = 0; + _bufferCount = await _reader.ReadAsync(_buffer, 0, _buffer.Length); + } + + /// + /// Parses text from an HTTP form body. + /// + /// The HTTP form body to parse. + /// The collection containing the parsed HTTP form body. + public static IDictionary ReadForm(string text) + { + var reader = new FormReader(text); + + var accumulator = new KeyValueAccumulator(StringComparer.OrdinalIgnoreCase); + var pair = reader.ReadNextPair(); + while (pair.HasValue) + { + accumulator.Append(pair.Value.Key, pair.Value.Value); + pair = reader.ReadNextPair(); + } + + return accumulator.GetResults(); + } + + /// + /// Parses an HTTP form body. + /// + /// The HTTP form body to parse. + /// The collection containing the parsed HTTP form body. + public static async Task> ReadFormAsync(Stream stream, CancellationToken cancellationToken = new CancellationToken()) + { + var reader = new FormReader(stream); + + var accumulator = new KeyValueAccumulator(StringComparer.OrdinalIgnoreCase); + var pair = await reader.ReadNextPairAsync(cancellationToken); + while (pair.HasValue) + { + accumulator.Append(pair.Value.Key, pair.Value.Value); + pair = await reader.ReadNextPairAsync(cancellationToken); + } + + return accumulator.GetResults(); + } + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNet.WebUtilities/KeyValueAccumulator.cs b/src/Microsoft.AspNet.WebUtilities/KeyValueAccumulator.cs new file mode 100644 index 0000000000..4c9d629a47 --- /dev/null +++ b/src/Microsoft.AspNet.WebUtilities/KeyValueAccumulator.cs @@ -0,0 +1,42 @@ +// 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; + +namespace Microsoft.AspNet.WebUtilities +{ + public class KeyValueAccumulator + { + private Dictionary> _accumulator; + IEqualityComparer _comparer; + + public KeyValueAccumulator([NotNull] IEqualityComparer comparer) + { + _comparer = comparer; + _accumulator = new Dictionary>(comparer); + } + + public void Append(TKey key, TValue value) + { + List values; + if (_accumulator.TryGetValue(key, out values)) + { + values.Add(value); + } + else + { + _accumulator[key] = new List(1) { value }; + } + } + + public IDictionary GetResults() + { + var results = new Dictionary(_comparer); + foreach (var kv in _accumulator) + { + results.Add(kv.Key, kv.Value.ToArray()); + } + return results; + } + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNet.WebUtilities/MultipartReader.cs b/src/Microsoft.AspNet.WebUtilities/MultipartReader.cs new file mode 100644 index 0000000000..766e5361dc --- /dev/null +++ b/src/Microsoft.AspNet.WebUtilities/MultipartReader.cs @@ -0,0 +1,92 @@ +// 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; +using System.IO; +using System.Threading; +using System.Threading.Tasks; + +namespace Microsoft.AspNet.WebUtilities +{ + // https://www.ietf.org/rfc/rfc2046.txt + public class MultipartReader + { + private const int DefaultBufferSize = 1024 * 4; + + private readonly BufferedReadStream _stream; + private readonly string _boundary; + private MultipartReaderStream _currentStream; + + public MultipartReader([NotNull] string boundary, [NotNull] Stream stream) + : this(boundary, stream, DefaultBufferSize) + { + } + + public MultipartReader([NotNull] string boundary, [NotNull] Stream stream, int bufferSize) + { + if (bufferSize < boundary.Length + 8) // Size of the boundary + leading and trailing CRLF + leading and trailing '--' markers. + { + throw new ArgumentOutOfRangeException(nameof(bufferSize), bufferSize, "Insufficient buffer space, the buffer must be larger than the boundary: " + boundary); + } + _stream = new BufferedReadStream(stream, bufferSize); + _boundary = boundary; + // This stream will drain any preamble data and remove the first boundary marker. + _currentStream = new MultipartReaderStream(_stream, _boundary, expectLeadingCrlf: false); + } + + /// + /// The limit for individual header lines inside a multipart section. + /// + public int HeaderLengthLimit { get; set; } = 1024 * 4; + + /// + /// The combined size limit for headers per multipart section. + /// + public int TotalHeaderSizeLimit { get; set; } = 1024 * 16; + + public async Task ReadNextSectionAsync(CancellationToken cancellationToken = new CancellationToken()) + { + // Drain the prior section. + await _currentStream.DrainAsync(cancellationToken); + // If we're at the end return null + if (_currentStream.FinalBoundaryFound) + { + // There may be trailer data after the last boundary. + await _stream.DrainAsync(cancellationToken); + return null; + } + var headers = await ReadHeadersAsync(cancellationToken); + _currentStream = new MultipartReaderStream(_stream, _boundary); + long? baseStreamOffset = _stream.CanSeek ? (long?)_stream.Position : null; + return new MultipartSection() { Headers = headers, Body = _currentStream, BaseStreamOffset = baseStreamOffset }; + } + + private async Task> ReadHeadersAsync(CancellationToken cancellationToken) + { + int totalSize = 0; + var accumulator = new KeyValueAccumulator(StringComparer.OrdinalIgnoreCase); + var line = await _stream.ReadLineAsync(HeaderLengthLimit, cancellationToken); + while (!string.IsNullOrEmpty(line)) + { + totalSize += line.Length; + if (totalSize > TotalHeaderSizeLimit) + { + throw new InvalidOperationException("Total header size limit exceeded: " + TotalHeaderSizeLimit); + } + int splitIndex = line.IndexOf(':'); + Debug.Assert(splitIndex > 0, "Invalid header line: " + line); + if (splitIndex >= 0) + { + var name = line.Substring(0, splitIndex); + var value = line.Substring(splitIndex + 1, line.Length - splitIndex - 1).Trim(); + accumulator.Append(name, value); + } + line = await _stream.ReadLineAsync(HeaderLengthLimit, cancellationToken); + } + + return accumulator.GetResults(); + } + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNet.WebUtilities/MultipartReaderStream.cs b/src/Microsoft.AspNet.WebUtilities/MultipartReaderStream.cs new file mode 100644 index 0000000000..d72809ef81 --- /dev/null +++ b/src/Microsoft.AspNet.WebUtilities/MultipartReaderStream.cs @@ -0,0 +1,320 @@ +// 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; +using System.IO; +using System.Text; +using System.Threading; +using System.Threading.Tasks; + +namespace Microsoft.AspNet.WebUtilities +{ + internal class MultipartReaderStream : Stream + { + private readonly BufferedReadStream _innerStream; + private readonly byte[] _boundaryBytes; + private readonly int _finalBoundaryLength; + private readonly long _innerOffset; + private long _position; + private long _observedLength; + private bool _finished; + + /// + /// Creates a stream that reads until it reaches the given boundary pattern. + /// + /// + /// + public MultipartReaderStream([NotNull] BufferedReadStream stream, [NotNull] string boundary, bool expectLeadingCrlf = true) + { + _innerStream = stream; + _innerOffset = _innerStream.CanSeek ? _innerStream.Position : 0; + if (expectLeadingCrlf) + { + _boundaryBytes = Encoding.UTF8.GetBytes("\r\n--" + boundary); + } + else + { + _boundaryBytes = Encoding.UTF8.GetBytes("--" + boundary); + } + _finalBoundaryLength = _boundaryBytes.Length + 2; // Include the final '--' terminator. + } + + public bool FinalBoundaryFound { get; private set; } + + public override bool CanRead + { + get { return true; } + } + + public override bool CanSeek + { + get { return _innerStream.CanSeek; } + } + + public override bool CanWrite + { + get { return false; } + } + + public override long Length + { + get { return _observedLength; } + } + + public override long Position + { + get { return _position; } + set + { + if (value < 0) + { + throw new ArgumentOutOfRangeException("value", value, "The Position must be positive."); + } + if (value > _observedLength) + { + throw new ArgumentOutOfRangeException("value", value, "The Position must be less than length."); + } + _position = value; + if (_position < _observedLength) + { + _finished = false; + } + } + } + + public override long Seek(long offset, SeekOrigin origin) + { + if (origin == SeekOrigin.Begin) + { + Position = offset; + } + else if (origin == SeekOrigin.Current) + { + Position = Position + offset; + } + else // if (origin == SeekOrigin.End) + { + Position = Length + offset; + } + return Position; + } + + public override void SetLength(long value) + { + throw new NotSupportedException(); + } + + public override void Write(byte[] buffer, int offset, int count) + { + throw new NotSupportedException(); + } +#if ASPNET50 + public override IAsyncResult BeginWrite(byte[] buffer, int offset, int size, AsyncCallback callback, object state) + { + throw new NotSupportedException(); + } + + public override void EndWrite(IAsyncResult asyncResult) + { + throw new NotSupportedException(); + } +#endif + public override Task WriteAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) + { + throw new NotSupportedException(); + } + + public override void Flush() + { + throw new NotSupportedException(); + } + + private void PositionInnerStream() + { + if (_innerStream.CanSeek && _innerStream.Position != (_innerOffset + _position)) + { + _innerStream.Position = _innerOffset + _position; + } + } + + private int UpdatePosition(int read) + { + _position += read; + if (_observedLength < _position) + { + _observedLength = _position; + } + return read; + } +#if ASPNET50 + public override IAsyncResult BeginRead(byte[] buffer, int offset, int size, AsyncCallback callback, object state) + { + var tcs = new TaskCompletionSource(state); + InternalReadAsync(buffer, offset, size, callback, tcs); + return tcs.Task; + } + + private async void InternalReadAsync(byte[] buffer, int offset, int size, AsyncCallback callback, TaskCompletionSource tcs) + { + try + { + int read = await ReadAsync(buffer, offset, size); + tcs.TrySetResult(read); + } + catch (Exception ex) + { + tcs.TrySetException(ex); + } + + if (callback != null) + { + try + { + callback(tcs.Task); + } + catch (Exception) + { + // Suppress exceptions on background threads. + } + } + } + + public override int EndRead(IAsyncResult asyncResult) + { + var task = (Task)asyncResult; + return task.GetAwaiter().GetResult(); + } +#endif + public override int Read(byte[] buffer, int offset, int count) + { + if (_finished) + { + return 0; + } + + PositionInnerStream(); + if (!_innerStream.EnsureBuffered(_finalBoundaryLength)) + { + throw new IOException("Unexpected end of stream."); + } + var bufferedData = _innerStream.BufferedData; + + // scan for a boundary match, full or partial. + int matchOffset; + int matchCount; + int read; + if (SubMatch(bufferedData, _boundaryBytes, out matchOffset, out matchCount)) + { + // We found a possible match, return any data before it. + if (matchOffset > bufferedData.Offset) + { + read = _innerStream.Read(buffer, offset, Math.Min(count, matchOffset - bufferedData.Offset)); + return UpdatePosition(read); + } + Debug.Assert(matchCount == _boundaryBytes.Length); + + // "The boundary may be followed by zero or more characters of + // linear whitespace. It is then terminated by either another CRLF" + // or -- for the final boundary. + byte[] boundary = new byte[_boundaryBytes.Length]; + read = _innerStream.Read(boundary, 0, boundary.Length); + Debug.Assert(read == boundary.Length); // It should have all been buffered + var remainder = _innerStream.ReadLine(lengthLimit: 100); // Whitespace may exceed the buffer. + remainder = remainder.Trim(); + if (string.Equals("--", remainder, StringComparison.Ordinal)) + { + FinalBoundaryFound = true; + } + Debug.Assert(FinalBoundaryFound || string.Equals(string.Empty, remainder, StringComparison.Ordinal), "Un-expected data found on the boundary line: " + remainder); + + _finished = true; + return 0; + } + + // No possible boundary match within the buffered data, return the data from the buffer. + read = _innerStream.Read(buffer, offset, Math.Min(count, bufferedData.Count)); + return UpdatePosition(read); + } + + public override async Task ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) + { + if (_finished) + { + return 0; + } + + PositionInnerStream(); + if (!await _innerStream.EnsureBufferedAsync(_finalBoundaryLength, cancellationToken)) + { + throw new IOException("Unexpected end of stream."); + } + var bufferedData = _innerStream.BufferedData; + + // scan for a boundary match, full or partial. + int matchOffset; + int matchCount; + int read; + if (SubMatch(bufferedData, _boundaryBytes, out matchOffset, out matchCount)) + { + // We found a possible match, return any data before it. + if (matchOffset > bufferedData.Offset) + { + // Sync, it's already buffered + read = _innerStream.Read(buffer, offset, Math.Min(count, matchOffset - bufferedData.Offset)); + return UpdatePosition(read); + } + Debug.Assert(matchCount == _boundaryBytes.Length); + + // "The boundary may be followed by zero or more characters of + // linear whitespace. It is then terminated by either another CRLF" + // or -- for the final boundary. + byte[] boundary = new byte[_boundaryBytes.Length]; + read = _innerStream.Read(boundary, 0, boundary.Length); + Debug.Assert(read == boundary.Length); // It should have all been buffered + var remainder = await _innerStream.ReadLineAsync(lengthLimit: 100, cancellationToken: cancellationToken); // Whitespace may exceed the buffer. + remainder = remainder.Trim(); + if (string.Equals("--", remainder, StringComparison.Ordinal)) + { + FinalBoundaryFound = true; + } + Debug.Assert(FinalBoundaryFound || string.Equals(string.Empty, remainder, StringComparison.Ordinal), "Un-expected data found on the boundary line: " + remainder); + + _finished = true; + return 0; + } + + // No possible boundary match within the buffered data, return the data from the buffer. + read = _innerStream.Read(buffer, offset, Math.Min(count, bufferedData.Count)); + return UpdatePosition(read); + } + + // Does Segment1 contain all of segment2, or does it end with the start of segment2? + // 1: AAAAABBBBBCCCCC + // 2: BBBBB + // Or: + // 1: AAAAABBB + // 2: BBBBB + private static bool SubMatch(ArraySegment segment1, byte[] matchBytes, out int matchOffset, out int matchCount) + { + matchCount = 0; + for (matchOffset = segment1.Offset; matchOffset < segment1.Offset + segment1.Count; matchOffset++) + { + int countLimit = segment1.Offset - matchOffset + segment1.Count; + for (matchCount = 0; matchCount < matchBytes.Length && matchCount < countLimit; matchCount++) + { + if (matchBytes[matchCount] != segment1.Array[matchOffset + matchCount]) + { + matchCount = 0; + break; + } + } + if (matchCount > 0) + { + break; + } + } + return matchCount > 0; + } + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNet.WebUtilities/MultipartSection.cs b/src/Microsoft.AspNet.WebUtilities/MultipartSection.cs new file mode 100644 index 0000000000..b35bcfee2a --- /dev/null +++ b/src/Microsoft.AspNet.WebUtilities/MultipartSection.cs @@ -0,0 +1,47 @@ +// 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.IO; + +namespace Microsoft.AspNet.WebUtilities +{ + public class MultipartSection + { + public string ContentType + { + get + { + string[] values; + if (Headers.TryGetValue("Content-Type", out values)) + { + return string.Join(", ", values); + } + return null; + } + } + + public string ContentDisposition + { + get + { + string[] values; + if (Headers.TryGetValue("Content-Disposition", out values)) + { + return string.Join(", ", values); + } + return null; + } + } + + public IDictionary Headers { get; set; } + + public Stream Body { get; set; } + + /// + /// The position where the body starts in the total multipart body. + /// This may not be available if the total multipart body is not seekable. + /// + public long? BaseStreamOffset { get; set; } + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNet.WebUtilities/ParsingHelpers.cs b/src/Microsoft.AspNet.WebUtilities/ParsingHelpers.cs deleted file mode 100644 index 257b206c6b..0000000000 --- a/src/Microsoft.AspNet.WebUtilities/ParsingHelpers.cs +++ /dev/null @@ -1,111 +0,0 @@ -// 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.Http; -using Microsoft.AspNet.WebUtilities.Collections; - -namespace Microsoft.AspNet.WebUtilities -{ - internal static class ParsingHelpers - { - internal static void ParseDelimited(string text, char[] delimiters, Action callback, object state) - { - int textLength = text.Length; - int equalIndex = text.IndexOf('='); - if (equalIndex == -1) - { - equalIndex = textLength; - } - int scanIndex = 0; - while (scanIndex < textLength) - { - int delimiterIndex = text.IndexOfAny(delimiters, scanIndex); - if (delimiterIndex == -1) - { - delimiterIndex = textLength; - } - if (equalIndex < delimiterIndex) - { - while (scanIndex != equalIndex && char.IsWhiteSpace(text[scanIndex])) - { - ++scanIndex; - } - string name = text.Substring(scanIndex, equalIndex - scanIndex); - string value = text.Substring(equalIndex + 1, delimiterIndex - equalIndex - 1); - callback( - Uri.UnescapeDataString(name.Replace('+', ' ')), - Uri.UnescapeDataString(value.Replace('+', ' ')), - state); - equalIndex = text.IndexOf('=', delimiterIndex); - if (equalIndex == -1) - { - equalIndex = textLength; - } - } - scanIndex = delimiterIndex + 1; - } - } - - private static readonly Action AppendItemCallback = (name, value, state) => - { - var dictionary = (IDictionary>)state; - - List existing; - if (!dictionary.TryGetValue(name, out existing)) - { - dictionary.Add(name, new List(1) { value }); - } - else - { - existing.Add(value); - } - }; - - internal static IFormCollection GetForm(string text) - { - IDictionary form = new Dictionary(StringComparer.OrdinalIgnoreCase); - var accumulator = new Dictionary>(StringComparer.OrdinalIgnoreCase); - ParseDelimited(text, Ampersand, AppendItemCallback, accumulator); - foreach (var kv in accumulator) - { - form.Add(kv.Key, kv.Value.ToArray()); - } - return new FormCollection(form); - } - - internal static string GetJoinedValue(IDictionary store, string key) - { - string[] values = GetUnmodifiedValues(store, key); - return values == null ? null : string.Join(",", values); - } - - internal static string[] GetUnmodifiedValues(IDictionary store, string key) - { - if (store == null) - { - throw new ArgumentNullException("store"); - } - string[] values; - return store.TryGetValue(key, out values) ? values : null; - } - - private static readonly char[] Ampersand = new[] { '&' }; - - internal static IReadableStringCollection GetQuery(string queryString) - { - if (!string.IsNullOrEmpty(queryString) && queryString[0] == '?') - { - queryString = queryString.Substring(1); - } - var accumulator = new Dictionary>(StringComparer.OrdinalIgnoreCase); - ParseDelimited(queryString, Ampersand, AppendItemCallback, accumulator); - return new ReadableStringCollection(accumulator.ToDictionary( - item => item.Key, - item => item.Value.ToArray(), - StringComparer.OrdinalIgnoreCase)); - } - } -} diff --git a/src/Microsoft.AspNet.WebUtilities/QueryHelpers.cs b/src/Microsoft.AspNet.WebUtilities/QueryHelpers.cs index f54ff50dea..1414ef8f17 100644 --- a/src/Microsoft.AspNet.WebUtilities/QueryHelpers.cs +++ b/src/Microsoft.AspNet.WebUtilities/QueryHelpers.cs @@ -4,7 +4,6 @@ using System; using System.Collections.Generic; using System.Text; -using Microsoft.AspNet.Http; namespace Microsoft.AspNet.WebUtilities { @@ -50,9 +49,49 @@ namespace Microsoft.AspNet.WebUtilities /// /// The raw query string value, with or without the leading '?'. /// A collection of parsed keys and values. - public static IReadableStringCollection ParseQuery(string text) + public static IDictionary ParseQuery(string queryString) { - return ParsingHelpers.GetQuery(text); + if (!string.IsNullOrEmpty(queryString) && queryString[0] == '?') + { + queryString = queryString.Substring(1); + } + var accumulator = new KeyValueAccumulator(StringComparer.OrdinalIgnoreCase); + + int textLength = queryString.Length; + int equalIndex = queryString.IndexOf('='); + if (equalIndex == -1) + { + equalIndex = textLength; + } + int scanIndex = 0; + while (scanIndex < textLength) + { + int delimiterIndex = queryString.IndexOf('&', scanIndex); + if (delimiterIndex == -1) + { + delimiterIndex = textLength; + } + if (equalIndex < delimiterIndex) + { + while (scanIndex != equalIndex && char.IsWhiteSpace(queryString[scanIndex])) + { + ++scanIndex; + } + string name = queryString.Substring(scanIndex, equalIndex - scanIndex); + string value = queryString.Substring(equalIndex + 1, delimiterIndex - equalIndex - 1); + accumulator.Append( + Uri.UnescapeDataString(name.Replace('+', ' ')), + Uri.UnescapeDataString(value.Replace('+', ' '))); + equalIndex = queryString.IndexOf('=', delimiterIndex); + if (equalIndex == -1) + { + equalIndex = textLength; + } + } + scanIndex = delimiterIndex + 1; + } + + return accumulator.GetResults(); } } } \ No newline at end of file diff --git a/src/Microsoft.AspNet.WebUtilities/StreamHelperExtensions.cs b/src/Microsoft.AspNet.WebUtilities/StreamHelperExtensions.cs new file mode 100644 index 0000000000..c5a2432db6 --- /dev/null +++ b/src/Microsoft.AspNet.WebUtilities/StreamHelperExtensions.cs @@ -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. + +using System.IO; +using System.Threading; +using System.Threading.Tasks; + +namespace Microsoft.AspNet.WebUtilities +{ + public static class StreamHelperExtensions + { + public static async Task DrainAsync(this Stream stream, CancellationToken cancellationToken) + { + byte[] buffer = new byte[1024]; + cancellationToken.ThrowIfCancellationRequested(); + while (await stream.ReadAsync(buffer, 0, buffer.Length, cancellationToken) > 0) + { + // Not all streams support cancellation directly. + cancellationToken.ThrowIfCancellationRequested(); + } + } + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNet.WebUtilities/project.json b/src/Microsoft.AspNet.WebUtilities/project.json index 0fb71d0a56..963fcc3178 100644 --- a/src/Microsoft.AspNet.WebUtilities/project.json +++ b/src/Microsoft.AspNet.WebUtilities/project.json @@ -9,6 +9,7 @@ "aspnetcore50": { "dependencies": { "System.Diagnostics.Debug": "4.0.10-beta-*", + "System.IO.FileSystem": "4.0.0-beta-*", "System.Runtime": "4.0.20-beta-*" } } diff --git a/test/Microsoft.AspNet.PipelineCore.Tests/FormFeatureTests.cs b/test/Microsoft.AspNet.PipelineCore.Tests/FormFeatureTests.cs index b7fdc8f563..b129adc0e1 100644 --- a/test/Microsoft.AspNet.PipelineCore.Tests/FormFeatureTests.cs +++ b/test/Microsoft.AspNet.PipelineCore.Tests/FormFeatureTests.cs @@ -1,72 +1,266 @@ // 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.IO; +using System.Linq; using System.Text; -using System.Threading; using System.Threading.Tasks; -using Microsoft.AspNet.FeatureModel; -using Microsoft.AspNet.HttpFeature; -using Moq; +using Microsoft.AspNet.WebUtilities; using Xunit; -namespace Microsoft.AspNet.PipelineCore.Tests +namespace Microsoft.AspNet.PipelineCore { public class FormFeatureTests { [Fact] - public async Task GetFormAsync_ReturnsParsedFormCollection() + public async Task ReadFormAsync_SimpleData_ReturnsParsedFormCollection() { // Arrange var formContent = Encoding.UTF8.GetBytes("foo=bar&baz=2"); - var features = new Mock(); - var request = new Mock(); - request.SetupGet(r => r.Body).Returns(new MemoryStream(formContent)); + var context = new DefaultHttpContext(); + context.Request.ContentType = "application/x-www-form-urlencoded; charset=utf-8"; + context.Request.Body = new MemoryStream(formContent); - object value = request.Object; - features.Setup(f => f.TryGetValue(typeof(IHttpRequestFeature), out value)) - .Returns(true); - - var provider = new FormFeature(features.Object); + // Not cached yet + var formFeature = context.GetFeature(); + Assert.Null(formFeature); // Act - var formCollection = await provider.GetFormAsync(CancellationToken.None); + var formCollection = await context.Request.ReadFormAsync(); // Assert Assert.Equal("bar", formCollection["foo"]); Assert.Equal("2", formCollection["baz"]); + + // Cached + formFeature = context.GetFeature(); + Assert.NotNull(formFeature); + Assert.NotNull(formFeature.Form); + Assert.Same(formFeature.Form, formCollection); } [Fact] - public async Task GetFormAsync_CachesFormCollectionPerBodyStream() + public async Task ReadFormAsync_EmptyKeyAtEndAllowed() { // Arrange - var formContent1 = Encoding.UTF8.GetBytes("foo=bar&baz=2"); - var formContent2 = Encoding.UTF8.GetBytes("collection2=value"); - var features = new Mock(); - var request = new Mock(); - request.SetupGet(r => r.Body).Returns(new MemoryStream(formContent1)); + var formContent = Encoding.UTF8.GetBytes("=bar"); + var body = new MemoryStream(formContent); - object value = request.Object; - features.Setup(f => f.TryGetValue(typeof(IHttpRequestFeature), out value)) - .Returns(true); + var formCollection = await FormReader.ReadFormAsync(body); - var provider = new FormFeature(features.Object); + // Assert + Assert.Equal("bar", formCollection[""].FirstOrDefault()); + } - // Act - 1 - var formCollection = await provider.GetFormAsync(CancellationToken.None); + [Fact] + public async Task ReadFormAsync_EmptyKeyWithAdditionalEntryAllowed() + { + // Arrange + var formContent = Encoding.UTF8.GetBytes("=bar&baz=2"); + var body = new MemoryStream(formContent); - // Assert - 1 - Assert.Equal("bar", formCollection["foo"]); - Assert.Equal("2", formCollection["baz"]); - Assert.Same(formCollection, await provider.GetFormAsync(CancellationToken.None)); + var formCollection = await FormReader.ReadFormAsync(body); - // Act - 2 - request.SetupGet(r => r.Body).Returns(new MemoryStream(formContent2)); - formCollection = await provider.GetFormAsync(CancellationToken.None); + // Assert + Assert.Equal("bar", formCollection[""].FirstOrDefault()); + Assert.Equal("2", formCollection["baz"].FirstOrDefault()); + } - // Assert - 2 - Assert.Equal("value", formCollection["collection2"]); + [Fact] + public async Task ReadFormAsync_EmptyValuedAtEndAllowed() + { + // Arrange + var formContent = Encoding.UTF8.GetBytes("foo="); + var body = new MemoryStream(formContent); + + var formCollection = await FormReader.ReadFormAsync(body); + + // Assert + Assert.Equal("", formCollection["foo"].FirstOrDefault()); + } + + [Fact] + public async Task ReadFormAsync_EmptyValuedWithAdditionalEntryAllowed() + { + // Arrange + var formContent = Encoding.UTF8.GetBytes("foo=&baz=2"); + var body = new MemoryStream(formContent); + + var formCollection = await FormReader.ReadFormAsync(body); + + // Assert + Assert.Equal("", formCollection["foo"].FirstOrDefault()); + Assert.Equal("2", formCollection["baz"].FirstOrDefault()); + } + + private const string MultipartContentType = "multipart/form-data; boundary=WebKitFormBoundary5pDRpGheQXaM8k3T"; + private const string EmptyMultipartForm = +@"--WebKitFormBoundary5pDRpGheQXaM8k3T--"; + private const string MultipartFormWithField = +@"--WebKitFormBoundary5pDRpGheQXaM8k3T +Content-Disposition: form-data; name=""description"" + +Foo +--WebKitFormBoundary5pDRpGheQXaM8k3T--"; + private const string MultipartFormWithFile = +@"--WebKitFormBoundary5pDRpGheQXaM8k3T +Content-Disposition: form-data; name=""myfile1""; filename=""temp.html"" +Content-Type: text/html + +Hello World +--WebKitFormBoundary5pDRpGheQXaM8k3T--"; + private const string MultipartFormWithFieldAndFile = +@"--WebKitFormBoundary5pDRpGheQXaM8k3T +Content-Disposition: form-data; name=""description"" + +Foo +--WebKitFormBoundary5pDRpGheQXaM8k3T +Content-Disposition: form-data; name=""myfile1""; filename=""temp.html"" +Content-Type: text/html + +Hello World +--WebKitFormBoundary5pDRpGheQXaM8k3T--"; + + [Fact] + public async Task ReadForm_EmptyMultipart_ReturnsParsedFormCollection() + { + var formContent = Encoding.UTF8.GetBytes(EmptyMultipartForm); + var context = new DefaultHttpContext(); + context.Request.ContentType = MultipartContentType; + context.Request.Body = new MemoryStream(formContent); + + // Not cached yet + var formFeature = context.GetFeature(); + Assert.Null(formFeature); + + var formCollection = context.Request.Form; + + Assert.NotNull(formCollection); + + // Cached + formFeature = context.GetFeature(); + Assert.NotNull(formFeature); + Assert.NotNull(formFeature.Form); + Assert.Same(formCollection, formFeature.Form); + Assert.Same(formCollection, await context.Request.ReadFormAsync()); + + // Content + Assert.Equal(0, formCollection.Count); + Assert.NotNull(formCollection.Files); + Assert.Equal(0, formCollection.Files.Count); + } + + [Fact] + public async Task ReadForm_MultipartWithField_ReturnsParsedFormCollection() + { + var formContent = Encoding.UTF8.GetBytes(MultipartFormWithField); + var context = new DefaultHttpContext(); + context.Request.ContentType = MultipartContentType; + context.Request.Body = new MemoryStream(formContent); + + // Not cached yet + var formFeature = context.GetFeature(); + Assert.Null(formFeature); + + var formCollection = context.Request.Form; + + Assert.NotNull(formCollection); + + // Cached + formFeature = context.GetFeature(); + Assert.NotNull(formFeature); + Assert.NotNull(formFeature.Form); + Assert.Same(formCollection, formFeature.Form); + Assert.Same(formCollection, await context.Request.ReadFormAsync()); + + // Content + Assert.Equal(1, formCollection.Count); + Assert.Equal("Foo", formCollection["description"]); + + Assert.NotNull(formCollection.Files); + Assert.Equal(0, formCollection.Files.Count); + } + + [Fact] + public async Task ReadFormAsync_MultipartWithFile_ReturnsParsedFormCollection() + { + var formContent = Encoding.UTF8.GetBytes(MultipartFormWithFile); + var context = new DefaultHttpContext(); + context.Request.ContentType = MultipartContentType; + context.Request.Body = new MemoryStream(formContent); + + // Not cached yet + var formFeature = context.GetFeature(); + Assert.Null(formFeature); + + var formCollection = await context.Request.ReadFormAsync(); + + Assert.NotNull(formCollection); + + // Cached + formFeature = context.GetFeature(); + Assert.NotNull(formFeature); + Assert.NotNull(formFeature.Form); + Assert.Same(formFeature.Form, formCollection); + Assert.Same(formCollection, context.Request.Form); + + // Content + Assert.Equal(0, formCollection.Count); + + Assert.NotNull(formCollection.Files); + Assert.Equal(1, formCollection.Files.Count); + + var file = formCollection.Files["myfile1"]; + Assert.Equal("text/html", file.ContentType); + Assert.Equal(@"form-data; name=""myfile1""; filename=""temp.html""", file.ContentDisposition); + var body = file.OpenReadStream(); + using (var reader = new StreamReader(body)) + { + var content = reader.ReadToEnd(); + Assert.Equal(content, "Hello World"); + } + } + + [Fact] + public async Task ReadFormAsync_MultipartWithFieldAndFile_ReturnsParsedFormCollection() + { + var formContent = Encoding.UTF8.GetBytes(MultipartFormWithFieldAndFile); + var context = new DefaultHttpContext(); + context.Request.ContentType = MultipartContentType; + context.Request.Body = new MemoryStream(formContent); + + // Not cached yet + var formFeature = context.GetFeature(); + Assert.Null(formFeature); + + var formCollection = await context.Request.ReadFormAsync(); + + Assert.NotNull(formCollection); + + // Cached + formFeature = context.GetFeature(); + Assert.NotNull(formFeature); + Assert.NotNull(formFeature.Form); + Assert.Same(formFeature.Form, formCollection); + Assert.Same(formCollection, context.Request.Form); + + // Content + Assert.Equal(1, formCollection.Count); + Assert.Equal("Foo", formCollection["description"]); + + Assert.NotNull(formCollection.Files); + Assert.Equal(1, formCollection.Files.Count); + + var file = formCollection.Files["myfile1"]; + Assert.Equal("text/html", file.ContentType); + Assert.Equal(@"form-data; name=""myfile1""; filename=""temp.html""", file.ContentDisposition); + var body = file.OpenReadStream(); + using (var reader = new StreamReader(body)) + { + var content = reader.ReadToEnd(); + Assert.Equal(content, "Hello World"); + } } } } diff --git a/test/Microsoft.AspNet.WebUtilities.Tests/MultipartReaderTests.cs b/test/Microsoft.AspNet.WebUtilities.Tests/MultipartReaderTests.cs new file mode 100644 index 0000000000..a642511a86 --- /dev/null +++ b/test/Microsoft.AspNet.WebUtilities.Tests/MultipartReaderTests.cs @@ -0,0 +1,185 @@ +// 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.IO; +using System.Text; +using System.Threading.Tasks; +using Xunit; + +namespace Microsoft.AspNet.WebUtilities +{ + public class MultipartReaderTests + { + private const string Boundary = "9051914041544843365972754266"; + private const string OnePartBody = +@"--9051914041544843365972754266 +Content-Disposition: form-data; name=""text"" + +text default +--9051914041544843365972754266-- +"; + private const string OnePartBodyWithTrailingWhitespace = +@"--9051914041544843365972754266 +Content-Disposition: form-data; name=""text"" + +text default +--9051914041544843365972754266-- +"; + // It's non-compliant but common to leave off the last CRLF. + private const string OnePartBodyWithoutFinalCRLF = +@"--9051914041544843365972754266 +Content-Disposition: form-data; name=""text"" + +text default +--9051914041544843365972754266--"; + private const string TwoPartBody = +@"--9051914041544843365972754266 +Content-Disposition: form-data; name=""text"" + +text default +--9051914041544843365972754266 +Content-Disposition: form-data; name=""file1""; filename=""a.txt"" +Content-Type: text/plain + +Content of a.txt. + +--9051914041544843365972754266-- +"; + private const string ThreePartBody = +@"--9051914041544843365972754266 +Content-Disposition: form-data; name=""text"" + +text default +--9051914041544843365972754266 +Content-Disposition: form-data; name=""file1""; filename=""a.txt"" +Content-Type: text/plain + +Content of a.txt. + +--9051914041544843365972754266 +Content-Disposition: form-data; name=""file2""; filename=""a.html"" +Content-Type: text/html + +Content of a.html. + +--9051914041544843365972754266-- +"; + + private static MemoryStream MakeStream(string text) + { + return new MemoryStream(Encoding.UTF8.GetBytes(text)); + } + + [Fact] + public async Task MutipartReader_ReadSinglePartBody_Success() + { + var stream = MakeStream(OnePartBody); + var reader = new MultipartReader(Boundary, stream); + + var section = await reader.ReadNextSectionAsync(); + Assert.NotNull(section); + Assert.Equal(1, section.Headers.Count); + Assert.Equal("form-data; name=\"text\"", section.Headers["Content-Disposition"][0]); + var buffer = new MemoryStream(); + await section.Body.CopyToAsync(buffer); + Assert.Equal("text default", Encoding.ASCII.GetString(buffer.ToArray())); + + Assert.Null(await reader.ReadNextSectionAsync()); + } + + [Fact] + public async Task MutipartReader_ReadSinglePartBodyWithTrailingWhitespace_Success() + { + var stream = MakeStream(OnePartBodyWithTrailingWhitespace); + var reader = new MultipartReader(Boundary, stream); + + var section = await reader.ReadNextSectionAsync(); + Assert.NotNull(section); + Assert.Equal(1, section.Headers.Count); + Assert.Equal("form-data; name=\"text\"", section.Headers["Content-Disposition"][0]); + var buffer = new MemoryStream(); + await section.Body.CopyToAsync(buffer); + Assert.Equal("text default", Encoding.ASCII.GetString(buffer.ToArray())); + + Assert.Null(await reader.ReadNextSectionAsync()); + } + + [Fact] + public async Task MutipartReader_ReadSinglePartBodyWithoutLastCRLF_Success() + { + var stream = MakeStream(OnePartBodyWithoutFinalCRLF); + var reader = new MultipartReader(Boundary, stream); + + var section = await reader.ReadNextSectionAsync(); + Assert.NotNull(section); + Assert.Equal(1, section.Headers.Count); + Assert.Equal("form-data; name=\"text\"", section.Headers["Content-Disposition"][0]); + var buffer = new MemoryStream(); + await section.Body.CopyToAsync(buffer); + Assert.Equal("text default", Encoding.ASCII.GetString(buffer.ToArray())); + + Assert.Null(await reader.ReadNextSectionAsync()); + } + + [Fact] + public async Task MutipartReader_ReadTwoPartBody_Success() + { + var stream = MakeStream(TwoPartBody); + var reader = new MultipartReader(Boundary, stream); + + var section = await reader.ReadNextSectionAsync(); + Assert.NotNull(section); + Assert.Equal(1, section.Headers.Count); + Assert.Equal("form-data; name=\"text\"", section.Headers["Content-Disposition"][0]); + var buffer = new MemoryStream(); + await section.Body.CopyToAsync(buffer); + Assert.Equal("text default", Encoding.ASCII.GetString(buffer.ToArray())); + + section = await reader.ReadNextSectionAsync(); + Assert.NotNull(section); + Assert.Equal(2, section.Headers.Count); + Assert.Equal("form-data; name=\"file1\"; filename=\"a.txt\"", section.Headers["Content-Disposition"][0]); + Assert.Equal("text/plain", section.Headers["Content-Type"][0]); + buffer = new MemoryStream(); + await section.Body.CopyToAsync(buffer); + Assert.Equal("Content of a.txt.\r\n", Encoding.ASCII.GetString(buffer.ToArray())); + + Assert.Null(await reader.ReadNextSectionAsync()); + } + + [Fact] + public async Task MutipartReader_ThreePartBody_Success() + { + var stream = MakeStream(ThreePartBody); + var reader = new MultipartReader(Boundary, stream); + + var section = await reader.ReadNextSectionAsync(); + Assert.NotNull(section); + Assert.Equal(1, section.Headers.Count); + Assert.Equal("form-data; name=\"text\"", section.Headers["Content-Disposition"][0]); + var buffer = new MemoryStream(); + await section.Body.CopyToAsync(buffer); + Assert.Equal("text default", Encoding.ASCII.GetString(buffer.ToArray())); + + section = await reader.ReadNextSectionAsync(); + Assert.NotNull(section); + Assert.Equal(2, section.Headers.Count); + Assert.Equal("form-data; name=\"file1\"; filename=\"a.txt\"", section.Headers["Content-Disposition"][0]); + Assert.Equal("text/plain", section.Headers["Content-Type"][0]); + buffer = new MemoryStream(); + await section.Body.CopyToAsync(buffer); + Assert.Equal("Content of a.txt.\r\n", Encoding.ASCII.GetString(buffer.ToArray())); + + section = await reader.ReadNextSectionAsync(); + Assert.NotNull(section); + Assert.Equal(2, section.Headers.Count); + Assert.Equal("form-data; name=\"file2\"; filename=\"a.html\"", section.Headers["Content-Disposition"][0]); + Assert.Equal("text/html", section.Headers["Content-Type"][0]); + buffer = new MemoryStream(); + await section.Body.CopyToAsync(buffer); + Assert.Equal("Content of a.html.\r\n", Encoding.ASCII.GetString(buffer.ToArray())); + + Assert.Null(await reader.ReadNextSectionAsync()); + } + } +} \ No newline at end of file diff --git a/test/Microsoft.AspNet.WebUtilities.Tests/QueryHelpersTests.cs b/test/Microsoft.AspNet.WebUtilities.Tests/QueryHelpersTests.cs index fea33d497a..e90c78305a 100644 --- a/test/Microsoft.AspNet.WebUtilities.Tests/QueryHelpersTests.cs +++ b/test/Microsoft.AspNet.WebUtilities.Tests/QueryHelpersTests.cs @@ -2,6 +2,7 @@ // 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.AspNet.WebUtilities @@ -13,8 +14,8 @@ namespace Microsoft.AspNet.WebUtilities { var collection = QueryHelpers.ParseQuery("?key1=value1&key2=value2"); Assert.Equal(2, collection.Count); - Assert.Equal("value1", collection["key1"]); - Assert.Equal("value2", collection["key2"]); + Assert.Equal("value1", collection["key1"].FirstOrDefault()); + Assert.Equal("value2", collection["key2"].FirstOrDefault()); } [Fact] @@ -22,8 +23,8 @@ namespace Microsoft.AspNet.WebUtilities { var collection = QueryHelpers.ParseQuery("key1=value1&key2=value2"); Assert.Equal(2, collection.Count); - Assert.Equal("value1", collection["key1"]); - Assert.Equal("value2", collection["key2"]); + Assert.Equal("value1", collection["key1"].FirstOrDefault()); + Assert.Equal("value2", collection["key2"].FirstOrDefault()); } [Fact] @@ -31,8 +32,8 @@ namespace Microsoft.AspNet.WebUtilities { var collection = QueryHelpers.ParseQuery("?key1=valueA&key2=valueB&key1=valueC"); Assert.Equal(2, collection.Count); - Assert.Equal("valueA,valueC", collection["key1"]); - Assert.Equal("valueB", collection["key2"]); + Assert.Equal(new[] { "valueA", "valueC" }, collection["key1"]); + Assert.Equal("valueB", collection["key2"].FirstOrDefault()); } [Fact] @@ -40,8 +41,8 @@ namespace Microsoft.AspNet.WebUtilities { var collection = QueryHelpers.ParseQuery("?key1=&key2="); Assert.Equal(2, collection.Count); - Assert.Equal(string.Empty, collection["key1"]); - Assert.Equal(string.Empty, collection["key2"]); + Assert.Equal(string.Empty, collection["key1"].FirstOrDefault()); + Assert.Equal(string.Empty, collection["key2"].FirstOrDefault()); } [Fact] @@ -49,7 +50,7 @@ namespace Microsoft.AspNet.WebUtilities { var collection = QueryHelpers.ParseQuery("?=value1&="); Assert.Equal(1, collection.Count); - Assert.Equal("value1,", collection[""]); + Assert.Equal(new[] { "value1", "" }, collection[""]); } } } \ No newline at end of file From 4377bb24ce59bc25d346fe5bc84e5fc0cfe97617 Mon Sep 17 00:00:00 2001 From: Ajay Bhargav Baaskaran Date: Fri, 9 Jan 2015 12:53:23 -0800 Subject: [PATCH 05/16] Added extension methods for FormFile --- .../FormFileExtensions.cs | 47 +++++++++++++++++++ 1 file changed, 47 insertions(+) create mode 100644 src/Microsoft.AspNet.Http.Extensions/FormFileExtensions.cs diff --git a/src/Microsoft.AspNet.Http.Extensions/FormFileExtensions.cs b/src/Microsoft.AspNet.Http.Extensions/FormFileExtensions.cs new file mode 100644 index 0000000000..a0c78ead9e --- /dev/null +++ b/src/Microsoft.AspNet.Http.Extensions/FormFileExtensions.cs @@ -0,0 +1,47 @@ +// 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.IO; +using System.Threading; +using System.Threading.Tasks; + +namespace Microsoft.AspNet.Http +{ + /// + /// Extension methods for . + /// + public static class FormFileExtensions + { + private static int DefaultBufferSize = 81920; + + /// + /// Saves the contents of an uploaded file. + /// + /// The . + /// The name of the file to create. + public static void SaveAs([NotNull] this IFormFile formFile, string filename) + { + using (var fileStream = new FileStream(filename, FileMode.Create)) + { + var inputStream = formFile.OpenReadStream(); + inputStream.CopyTo(fileStream); + } + } + + /// + /// Asynchronously saves the contents of an uploaded file. + /// + /// The . + /// The name of the file to create. + public async static Task SaveAsAsync([NotNull] this IFormFile formFile, + string filename, + CancellationToken cancellationToken = default(CancellationToken)) + { + using (var fileStream = new FileStream(filename, FileMode.Create)) + { + var inputStream = formFile.OpenReadStream(); + await inputStream.CopyToAsync(fileStream, DefaultBufferSize, cancellationToken); + } + } + } +} \ No newline at end of file From 4fb21644fc3b2387e07e3622f2361652d4598c1f Mon Sep 17 00:00:00 2001 From: Chris Ross Date: Wed, 14 Jan 2015 15:41:09 -0800 Subject: [PATCH 06/16] Implement strongly typed headers. --- HttpAbstractions.sln | 32 +- .../Microsoft.AspNet.FeatureModel.kproj | 9 +- .../HeaderDictionaryTypeExtensions.cs | 202 +++++ .../Microsoft.AspNet.Http.Extensions.kproj | 9 +- .../QueryBuilder.cs | 3 +- .../RequestHeaders.cs | 259 +++++++ .../ResponseHeaders.cs | 156 ++++ .../UriHelper.cs | 18 +- .../project.json | 3 +- src/Microsoft.AspNet.Http/HttpRequest.cs | 24 - .../Microsoft.AspNet.Http.kproj | 9 +- .../Microsoft.AspNet.HttpFeature.kproj | 9 +- .../Microsoft.AspNet.Owin.kproj | 9 +- .../OwinFeatureCollection.cs | 2 +- .../Collections/FormFileCollection.cs | 9 +- .../Collections/HeaderDictionary.cs | 4 + .../DefaultHttpRequest.cs | 12 - .../FormFeature.cs | 123 +-- .../Infrastructure/ParsingHelpers.cs | 19 - .../Microsoft.AspNet.PipelineCore.kproj | 9 +- .../project.json | 4 +- .../FormReader.cs | 21 +- .../Microsoft.AspNet.WebUtilities.kproj | 9 +- .../project.json | 6 +- .../BaseHeaderParser.cs | 70 ++ .../CacheControlHeaderValue.cs | 603 +++++++++++++++ .../ContentDispositionHeaderValue.cs | 702 ++++++++++++++++++ .../ContentRangeHeaderValue.cs | 397 ++++++++++ .../CookieHeaderParser.cs | 104 +++ .../CookieHeaderValue.cs | 256 +++++++ .../EntityTagHeaderValue.cs | 204 +++++ .../GenericHeaderParser.cs | 23 + src/Microsoft.Net.Http.Headers/HeaderNames.cs | 59 ++ .../HeaderQuality.cs | 18 + .../HeaderUtilities.cs | 232 ++++++ .../HttpHeaderParser.cs | 137 ++++ .../HttpParseResult.cs | 12 + .../HttpRuleParser.cs | 352 +++++++++ .../MediaTypeHeaderValue.cs | 408 ++++++++++ .../MediaTypeHeaderValueComparer.cs | 100 +++ .../Microsoft.Net.Http.Headers.kproj | 23 + .../NameValueHeaderValue.cs | 347 +++++++++ .../NotNullAttribute.cs | 12 + .../ObjectCollection.cs | 56 ++ .../RangeConditionHeaderValue.cs | 166 +++++ .../RangeHeaderValue.cs | 161 ++++ .../RangeItemHeaderValue.cs | 226 ++++++ .../SetCookieHeaderValue.cs | 364 +++++++++ .../StringWithQualityHeaderValue.cs | 225 ++++++ .../StringWithQualityHeaderValueComparer.cs | 71 ++ src/Microsoft.Net.Http.Headers/project.json | 19 + .../HeaderDictionaryTypeExtensionsTest.cs | 206 +++++ ...crosoft.AspNet.Http.Extensions.Tests.kproj | 9 +- .../QueryBuilderTests.cs | 2 +- .../UseWithServicesTests.cs | 5 +- .../DefaultHttpRequestTests.cs | 20 - .../Microsoft.AspNet.WebUtilities.Tests.kproj | 9 +- .../project.json | 4 +- .../CacheControlHeaderValueTest.cs | 599 +++++++++++++++ .../ContentDispositionHeaderValueTest.cs | 609 +++++++++++++++ .../ContentRangeHeaderValueTest.cs | 272 +++++++ .../CookieHeaderValueTest.cs | 229 ++++++ .../DateParserTest.cs | 58 ++ .../EntityTagHeaderValueTest.cs | 315 ++++++++ .../MediaTypeHeaderValueComparerTests.cs | 69 ++ .../MediaTypeHeaderValueTest.cs | 520 +++++++++++++ .../Microsoft.Net.Http.Headers.Tests.kproj | 23 + .../NameValueHeaderValueTest.cs | 394 ++++++++++ .../RangeConditionHeaderValueTest.cs | 174 +++++ .../RangeHeaderValueTest.cs | 183 +++++ .../RangeItemHeaderValueTest.cs | 162 ++++ .../SetCookieHeaderValueTest.cs | 259 +++++++ ...tringWithQualityHeaderValueComparerTest.cs | 64 ++ .../StringWithQualityHeaderValueTest.cs | 339 +++++++++ .../project.json | 18 + 75 files changed, 10676 insertions(+), 173 deletions(-) create mode 100644 src/Microsoft.AspNet.Http.Extensions/HeaderDictionaryTypeExtensions.cs rename src/{Microsoft.AspNet.WebUtilities => Microsoft.AspNet.Http.Extensions}/QueryBuilder.cs (97%) create mode 100644 src/Microsoft.AspNet.Http.Extensions/RequestHeaders.cs create mode 100644 src/Microsoft.AspNet.Http.Extensions/ResponseHeaders.cs rename src/{Microsoft.AspNet.WebUtilities => Microsoft.AspNet.Http.Extensions}/UriHelper.cs (85%) create mode 100644 src/Microsoft.Net.Http.Headers/BaseHeaderParser.cs create mode 100644 src/Microsoft.Net.Http.Headers/CacheControlHeaderValue.cs create mode 100644 src/Microsoft.Net.Http.Headers/ContentDispositionHeaderValue.cs create mode 100644 src/Microsoft.Net.Http.Headers/ContentRangeHeaderValue.cs create mode 100644 src/Microsoft.Net.Http.Headers/CookieHeaderParser.cs create mode 100644 src/Microsoft.Net.Http.Headers/CookieHeaderValue.cs create mode 100644 src/Microsoft.Net.Http.Headers/EntityTagHeaderValue.cs create mode 100644 src/Microsoft.Net.Http.Headers/GenericHeaderParser.cs create mode 100644 src/Microsoft.Net.Http.Headers/HeaderNames.cs create mode 100644 src/Microsoft.Net.Http.Headers/HeaderQuality.cs create mode 100644 src/Microsoft.Net.Http.Headers/HeaderUtilities.cs create mode 100644 src/Microsoft.Net.Http.Headers/HttpHeaderParser.cs create mode 100644 src/Microsoft.Net.Http.Headers/HttpParseResult.cs create mode 100644 src/Microsoft.Net.Http.Headers/HttpRuleParser.cs create mode 100644 src/Microsoft.Net.Http.Headers/MediaTypeHeaderValue.cs create mode 100644 src/Microsoft.Net.Http.Headers/MediaTypeHeaderValueComparer.cs create mode 100644 src/Microsoft.Net.Http.Headers/Microsoft.Net.Http.Headers.kproj create mode 100644 src/Microsoft.Net.Http.Headers/NameValueHeaderValue.cs create mode 100644 src/Microsoft.Net.Http.Headers/NotNullAttribute.cs create mode 100644 src/Microsoft.Net.Http.Headers/ObjectCollection.cs create mode 100644 src/Microsoft.Net.Http.Headers/RangeConditionHeaderValue.cs create mode 100644 src/Microsoft.Net.Http.Headers/RangeHeaderValue.cs create mode 100644 src/Microsoft.Net.Http.Headers/RangeItemHeaderValue.cs create mode 100644 src/Microsoft.Net.Http.Headers/SetCookieHeaderValue.cs create mode 100644 src/Microsoft.Net.Http.Headers/StringWithQualityHeaderValue.cs create mode 100644 src/Microsoft.Net.Http.Headers/StringWithQualityHeaderValueComparer.cs create mode 100644 src/Microsoft.Net.Http.Headers/project.json create mode 100644 test/Microsoft.AspNet.Http.Extensions.Tests/HeaderDictionaryTypeExtensionsTest.cs rename test/{Microsoft.AspNet.WebUtilities.Tests => Microsoft.AspNet.Http.Extensions.Tests}/QueryBuilderTests.cs (98%) create mode 100644 test/Microsoft.Net.Http.Headers.Tests/CacheControlHeaderValueTest.cs create mode 100644 test/Microsoft.Net.Http.Headers.Tests/ContentDispositionHeaderValueTest.cs create mode 100644 test/Microsoft.Net.Http.Headers.Tests/ContentRangeHeaderValueTest.cs create mode 100644 test/Microsoft.Net.Http.Headers.Tests/CookieHeaderValueTest.cs create mode 100644 test/Microsoft.Net.Http.Headers.Tests/DateParserTest.cs create mode 100644 test/Microsoft.Net.Http.Headers.Tests/EntityTagHeaderValueTest.cs create mode 100644 test/Microsoft.Net.Http.Headers.Tests/MediaTypeHeaderValueComparerTests.cs create mode 100644 test/Microsoft.Net.Http.Headers.Tests/MediaTypeHeaderValueTest.cs create mode 100644 test/Microsoft.Net.Http.Headers.Tests/Microsoft.Net.Http.Headers.Tests.kproj create mode 100644 test/Microsoft.Net.Http.Headers.Tests/NameValueHeaderValueTest.cs create mode 100644 test/Microsoft.Net.Http.Headers.Tests/RangeConditionHeaderValueTest.cs create mode 100644 test/Microsoft.Net.Http.Headers.Tests/RangeHeaderValueTest.cs create mode 100644 test/Microsoft.Net.Http.Headers.Tests/RangeItemHeaderValueTest.cs create mode 100644 test/Microsoft.Net.Http.Headers.Tests/SetCookieHeaderValueTest.cs create mode 100644 test/Microsoft.Net.Http.Headers.Tests/StringWithQualityHeaderValueComparerTest.cs create mode 100644 test/Microsoft.Net.Http.Headers.Tests/StringWithQualityHeaderValueTest.cs create mode 100644 test/Microsoft.Net.Http.Headers.Tests/project.json diff --git a/HttpAbstractions.sln b/HttpAbstractions.sln index cdea46535d..8ae65bd225 100644 --- a/HttpAbstractions.sln +++ b/HttpAbstractions.sln @@ -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 diff --git a/src/Microsoft.AspNet.FeatureModel/Microsoft.AspNet.FeatureModel.kproj b/src/Microsoft.AspNet.FeatureModel/Microsoft.AspNet.FeatureModel.kproj index 561cd372d7..5484c20589 100644 --- a/src/Microsoft.AspNet.FeatureModel/Microsoft.AspNet.FeatureModel.kproj +++ b/src/Microsoft.AspNet.FeatureModel/Microsoft.AspNet.FeatureModel.kproj @@ -1,4 +1,4 @@ - + 14.0 @@ -14,4 +14,9 @@ 2.0 - + + + + + + \ No newline at end of file diff --git a/src/Microsoft.AspNet.Http.Extensions/HeaderDictionaryTypeExtensions.cs b/src/Microsoft.AspNet.Http.Extensions/HeaderDictionaryTypeExtensions.cs new file mode 100644 index 0000000000..fc4438c018 --- /dev/null +++ b/src/Microsoft.AspNet.Http.Extensions/HeaderDictionaryTypeExtensions.cs @@ -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(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([NotNull] this IHeaderDictionary headers, [NotNull] string name, IList 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([NotNull] this IHeaderDictionary headers, [NotNull] string name, [NotNull] IList values) + { + headers.AppendValues(name, values.Select(value => value.ToString()).ToArray()); + } + + private static IDictionary KnownParsers = new Dictionary() + { + { typeof(CacheControlHeaderValue), new Func(value => { CacheControlHeaderValue result; return CacheControlHeaderValue.TryParse(value, out result) ? result : null; }) }, + { typeof(ContentDispositionHeaderValue), new Func(value => { ContentDispositionHeaderValue result; return ContentDispositionHeaderValue.TryParse(value, out result) ? result : null; }) }, + { typeof(ContentRangeHeaderValue), new Func(value => { ContentRangeHeaderValue result; return ContentRangeHeaderValue.TryParse(value, out result) ? result : null; }) }, + { typeof(MediaTypeHeaderValue), new Func(value => { MediaTypeHeaderValue result; return MediaTypeHeaderValue.TryParse(value, out result) ? result : null; }) }, + { typeof(RangeConditionHeaderValue), new Func(value => { RangeConditionHeaderValue result; return RangeConditionHeaderValue.TryParse(value, out result) ? result : null; }) }, + { typeof(RangeHeaderValue), new Func(value => { RangeHeaderValue result; return RangeHeaderValue.TryParse(value, out result) ? result : null; }) }, + { typeof(EntityTagHeaderValue), new Func(value => { EntityTagHeaderValue result; return EntityTagHeaderValue.TryParse(value, out result) ? result : null; }) }, + { typeof(DateTimeOffset?), new Func(value => { DateTimeOffset result; return HeaderUtilities.TryParseDate(value, out result) ? result : (DateTimeOffset?)null; }) }, + { typeof(long?), new Func(value => { long result; return HeaderUtilities.TryParseInt64(value, out result) ? result : (long?)null; }) }, + }; + + private static IDictionary KnownListParsers = new Dictionary() + { + { typeof(MediaTypeHeaderValue), new Func, IList>(value => { IList result; return MediaTypeHeaderValue.TryParseList(value, out result) ? result : null; }) }, + { typeof(StringWithQualityHeaderValue), new Func, IList>(value => { IList result; return StringWithQualityHeaderValue.TryParseList(value, out result) ? result : null; }) }, + { typeof(CookieHeaderValue), new Func, IList>(value => { IList result; return CookieHeaderValue.TryParseList(value, out result) ? result : null; }) }, + { typeof(EntityTagHeaderValue), new Func, IList>(value => { IList result; return EntityTagHeaderValue.TryParseList(value, out result) ? result : null; }) }, + { typeof(SetCookieHeaderValue), new Func, IList>(value => { IList result; return SetCookieHeaderValue.TryParseList(value, out result) ? result : null; }) }, + }; + + public static T Get([NotNull] this IHeaderDictionary headers, string name) + { + object temp; + if (KnownParsers.TryGetValue(typeof(T), out temp)) + { + var func = (Func)temp; + return func(headers[name]); + } + + var value = headers[name]; + if (string.IsNullOrWhiteSpace(value)) + { + return default(T); + } + + return GetViaReflection(value); + } + + public static IList GetList([NotNull] this IHeaderDictionary headers, string name) + { + object temp; + if (KnownListParsers.TryGetValue(typeof(T), out temp)) + { + var func = (Func, IList>)temp; + return func(headers.GetValues(name)); + } + + var values = headers.GetValues(name); + if (values == null || !values.Any()) + { + return null; + } + + return GetListViaReflection(values); + } + + private static T GetViaReflection(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 GetListViaReflection(IList 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)) + && methodParams[1].IsOut + && methodParams[1].ParameterType.Equals(typeof(IList).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, out IList<{0}>).", nameof(T))); + } + + var parameters = new object[] { values, null }; + var success = (bool)method.Invoke(null, parameters); + if (success) + { + return (IList)parameters[1]; + } + return null; + } + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNet.Http.Extensions/Microsoft.AspNet.Http.Extensions.kproj b/src/Microsoft.AspNet.Http.Extensions/Microsoft.AspNet.Http.Extensions.kproj index a9b64e3c08..b431b00760 100644 --- a/src/Microsoft.AspNet.Http.Extensions/Microsoft.AspNet.Http.Extensions.kproj +++ b/src/Microsoft.AspNet.Http.Extensions/Microsoft.AspNet.Http.Extensions.kproj @@ -1,4 +1,4 @@ - + 14.0 @@ -14,4 +14,9 @@ 2.0 - + + + + + + \ No newline at end of file diff --git a/src/Microsoft.AspNet.WebUtilities/QueryBuilder.cs b/src/Microsoft.AspNet.Http.Extensions/QueryBuilder.cs similarity index 97% rename from src/Microsoft.AspNet.WebUtilities/QueryBuilder.cs rename to src/Microsoft.AspNet.Http.Extensions/QueryBuilder.cs index 28cdfa85fe..a4afbba272 100644 --- a/src/Microsoft.AspNet.WebUtilities/QueryBuilder.cs +++ b/src/Microsoft.AspNet.Http.Extensions/QueryBuilder.cs @@ -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> diff --git a/src/Microsoft.AspNet.Http.Extensions/RequestHeaders.cs b/src/Microsoft.AspNet.Http.Extensions/RequestHeaders.cs new file mode 100644 index 0000000000..eeeef90b24 --- /dev/null +++ b/src/Microsoft.AspNet.Http.Extensions/RequestHeaders.cs @@ -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 Accept + { + get + { + return Headers.GetList(HeaderNames.Accept); + } + set + { + Headers.SetList(HeaderNames.Accept, value); + } + } + + public IList AcceptCharset + { + get + { + return Headers.GetList(HeaderNames.AcceptCharset); + } + set + { + Headers.SetList(HeaderNames.AcceptCharset, value); + } + } + + public IList AcceptEncoding + { + get + { + return Headers.GetList(HeaderNames.AcceptEncoding); + } + set + { + Headers.SetList(HeaderNames.AcceptEncoding, value); + } + } + + public IList AcceptLanguage + { + get + { + return Headers.GetList(HeaderNames.AcceptLanguage); + } + set + { + Headers.SetList(HeaderNames.AcceptLanguage, value); + } + } + + public CacheControlHeaderValue CacheControl + { + get + { + return Headers.Get(HeaderNames.CacheControl); + } + set + { + Headers.Set(HeaderNames.CacheControl, value); + } + } + + public ContentDispositionHeaderValue ContentDisposition + { + get + { + return Headers.Get(HeaderNames.ContentDisposition); + } + set + { + Headers.Set(HeaderNames.ContentDisposition, value); + } + } + + public long? ContentLength + { + get + { + return Headers.Get(HeaderNames.ContentLength); + } + set + { + Headers.Set(HeaderNames.ContentLength, value.HasValue ? HeaderUtilities.FormatInt64(value.Value) : null); + } + } + + public ContentRangeHeaderValue ContentRange + { + get + { + return Headers.Get(HeaderNames.ContentRange); + } + set + { + Headers.Set(HeaderNames.ContentRange, value); + } + } + + public MediaTypeHeaderValue ContentType + { + get + { + return Headers.Get(HeaderNames.ContentType); + } + set + { + Headers.Set(HeaderNames.ContentType, value); + } + } + + public IList Cookie + { + get + { + return Headers.GetList(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 IfMatch + { + get + { + return Headers.GetList(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 IfNoneMatch + { + get + { + return Headers.GetList(HeaderNames.IfNoneMatch); + } + set + { + Headers.SetList(HeaderNames.IfNoneMatch, value); + } + } + + public RangeConditionHeaderValue IfRange + { + get + { + return Headers.Get(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(HeaderNames.Range); + } + set + { + Headers.Set(HeaderNames.Range, value); + } + } + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNet.Http.Extensions/ResponseHeaders.cs b/src/Microsoft.AspNet.Http.Extensions/ResponseHeaders.cs new file mode 100644 index 0000000000..4169f237a7 --- /dev/null +++ b/src/Microsoft.AspNet.Http.Extensions/ResponseHeaders.cs @@ -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(HeaderNames.CacheControl); + } + set + { + Headers.Set(HeaderNames.CacheControl, value); + } + } + + public ContentDispositionHeaderValue ContentDisposition + { + get + { + return Headers.Get(HeaderNames.ContentDisposition); + } + set + { + Headers.Set(HeaderNames.ContentDisposition, value); + } + } + + public long? ContentLength + { + get + { + return Headers.Get(HeaderNames.ContentLength); + } + set + { + Headers.Set(HeaderNames.ContentLength, value.HasValue ? HeaderUtilities.FormatInt64(value.Value) : null); + } + } + + public ContentRangeHeaderValue ContentRange + { + get + { + return Headers.Get(HeaderNames.ContentRange); + } + set + { + Headers.Set(HeaderNames.ContentRange, value); + } + } + + public MediaTypeHeaderValue ContentType + { + get + { + return Headers.Get(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(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 SetCookie + { + get + { + return Headers.GetList(HeaderNames.SetCookie); + } + set + { + Headers.SetList(HeaderNames.SetCookie, value); + } + } + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNet.WebUtilities/UriHelper.cs b/src/Microsoft.AspNet.Http.Extensions/UriHelper.cs similarity index 85% rename from src/Microsoft.AspNet.WebUtilities/UriHelper.cs rename to src/Microsoft.AspNet.Http.Extensions/UriHelper.cs index 491c599d20..e271d41380 100644 --- a/src/Microsoft.AspNet.WebUtilities/UriHelper.cs +++ b/src/Microsoft.AspNet.Http.Extensions/UriHelper.cs @@ -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 { /// /// 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(), diff --git a/src/Microsoft.AspNet.Http.Extensions/project.json b/src/Microsoft.AspNet.Http.Extensions/project.json index cbc45a6ddc..be234399ec 100644 --- a/src/Microsoft.AspNet.Http.Extensions/project.json +++ b/src/Microsoft.AspNet.Http.Extensions/project.json @@ -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" : { diff --git a/src/Microsoft.AspNet.Http/HttpRequest.cs b/src/Microsoft.AspNet.Http/HttpRequest.cs index 8048152a08..5ab2010a59 100644 --- a/src/Microsoft.AspNet.Http/HttpRequest.cs +++ b/src/Microsoft.AspNet.Http/HttpRequest.cs @@ -93,30 +93,6 @@ namespace Microsoft.AspNet.Http /// The Content-Type header. public abstract string ContentType { get; set; } - /// - /// Gets or sets the Cache-Control header. - /// - /// The Cache-Control header. - // (TODO header conventions?) public abstract string CacheControl { get; set; } - - /// - /// Gets or sets the Media-Type header. - /// - /// The Media-Type header. - // (TODO header conventions?) public abstract string MediaType { get; set; } - - /// - /// Gets or set the Accept header. - /// - /// The Accept header. - public abstract string Accept { get; set; } - - /// - /// Gets or set the Accept-Charset header. - /// - /// The Accept-Charset header. - public abstract string AcceptCharset { get; set; } - /// /// Gets or set the owin.RequestBody Stream. /// diff --git a/src/Microsoft.AspNet.Http/Microsoft.AspNet.Http.kproj b/src/Microsoft.AspNet.Http/Microsoft.AspNet.Http.kproj index 1ccadbc30a..3a68cce3bc 100644 --- a/src/Microsoft.AspNet.Http/Microsoft.AspNet.Http.kproj +++ b/src/Microsoft.AspNet.Http/Microsoft.AspNet.Http.kproj @@ -1,4 +1,4 @@ - + 14.0 @@ -14,4 +14,9 @@ 2.0 - + + + + + + \ No newline at end of file diff --git a/src/Microsoft.AspNet.HttpFeature/Microsoft.AspNet.HttpFeature.kproj b/src/Microsoft.AspNet.HttpFeature/Microsoft.AspNet.HttpFeature.kproj index 25a0e7caf2..90f93a0f7b 100644 --- a/src/Microsoft.AspNet.HttpFeature/Microsoft.AspNet.HttpFeature.kproj +++ b/src/Microsoft.AspNet.HttpFeature/Microsoft.AspNet.HttpFeature.kproj @@ -1,4 +1,4 @@ - + 14.0 @@ -14,4 +14,9 @@ 2.0 - + + + + + + \ No newline at end of file diff --git a/src/Microsoft.AspNet.Owin/Microsoft.AspNet.Owin.kproj b/src/Microsoft.AspNet.Owin/Microsoft.AspNet.Owin.kproj index b0acecf075..4c7801eada 100644 --- a/src/Microsoft.AspNet.Owin/Microsoft.AspNet.Owin.kproj +++ b/src/Microsoft.AspNet.Owin/Microsoft.AspNet.Owin.kproj @@ -1,4 +1,4 @@ - + 14.0 @@ -14,4 +14,9 @@ 2.0 - + + + + + + \ No newline at end of file diff --git a/src/Microsoft.AspNet.Owin/OwinFeatureCollection.cs b/src/Microsoft.AspNet.Owin/OwinFeatureCollection.cs index 434817129d..3332221be8 100644 --- a/src/Microsoft.AspNet.Owin/OwinFeatureCollection.cs +++ b/src/Microsoft.AspNet.Owin/OwinFeatureCollection.cs @@ -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)) diff --git a/src/Microsoft.AspNet.PipelineCore/Collections/FormFileCollection.cs b/src/Microsoft.AspNet.PipelineCore/Collections/FormFileCollection.cs index 10aad2bfa2..2ef4d29476 100644 --- a/src/Microsoft.AspNet.PipelineCore/Collections/FormFileCollection.cs +++ b/src/Microsoft.AspNet.PipelineCore/Collections/FormFileCollection.cs @@ -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); } } } \ No newline at end of file diff --git a/src/Microsoft.AspNet.PipelineCore/Collections/HeaderDictionary.cs b/src/Microsoft.AspNet.PipelineCore/Collections/HeaderDictionary.cs index dfd54393cc..bf362c94ed 100644 --- a/src/Microsoft.AspNet.PipelineCore/Collections/HeaderDictionary.cs +++ b/src/Microsoft.AspNet.PipelineCore/Collections/HeaderDictionary.cs @@ -16,6 +16,10 @@ namespace Microsoft.AspNet.PipelineCore.Collections /// public class HeaderDictionary : IHeaderDictionary { + public HeaderDictionary() : this(new Dictionary(StringComparer.OrdinalIgnoreCase)) + { + } + /// /// Initializes a new instance of the class. /// diff --git a/src/Microsoft.AspNet.PipelineCore/DefaultHttpRequest.cs b/src/Microsoft.AspNet.PipelineCore/DefaultHttpRequest.cs index da839c1f16..d95c37b9c3 100644 --- a/src/Microsoft.AspNet.PipelineCore/DefaultHttpRequest.cs +++ b/src/Microsoft.AspNet.PipelineCore/DefaultHttpRequest.cs @@ -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; } diff --git a/src/Microsoft.AspNet.PipelineCore/FormFeature.cs b/src/Microsoft.AspNet.PipelineCore/FormFeature.cs index 8d20871688..0180728be2 100644 --- a/src/Microsoft.AspNet.PipelineCore/FormFeature.cs +++ b/src/Microsoft.AspNet.PipelineCore/FormFeature.cs @@ -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(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; } diff --git a/src/Microsoft.AspNet.PipelineCore/Infrastructure/ParsingHelpers.cs b/src/Microsoft.AspNet.PipelineCore/Infrastructure/ParsingHelpers.cs index e0ed2c7333..48dab300ac 100644 --- a/src/Microsoft.AspNet.PipelineCore/Infrastructure/ParsingHelpers.cs +++ b/src/Microsoft.AspNet.PipelineCore/Infrastructure/ParsingHelpers.cs @@ -518,25 +518,6 @@ namespace Microsoft.AspNet.PipelineCore.Infrastructure request.HttpContext.Items[key] = value; } - internal static IDictionary GetCookies(HttpRequest request) - { - var cookies = GetItem>(request, "Microsoft.Owin.Cookies#dictionary"); - if (cookies == null) - { - cookies = new Dictionary(StringComparer.Ordinal); - SetItem(request, "Microsoft.Owin.Cookies#dictionary", cookies); - } - - string text = GetHeader(request.Headers, "Cookie"); - if (GetItem(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 cookiesCollection) { ParseDelimited(cookiesHeader, SemicolonAndComma, AddCookieCallback, cookiesCollection); diff --git a/src/Microsoft.AspNet.PipelineCore/Microsoft.AspNet.PipelineCore.kproj b/src/Microsoft.AspNet.PipelineCore/Microsoft.AspNet.PipelineCore.kproj index 3a0556def8..96b305e9b6 100644 --- a/src/Microsoft.AspNet.PipelineCore/Microsoft.AspNet.PipelineCore.kproj +++ b/src/Microsoft.AspNet.PipelineCore/Microsoft.AspNet.PipelineCore.kproj @@ -1,4 +1,4 @@ - + 14.0 @@ -14,4 +14,9 @@ 2.0 - + + + + + + \ No newline at end of file diff --git a/src/Microsoft.AspNet.PipelineCore/project.json b/src/Microsoft.AspNet.PipelineCore/project.json index 6b1a0de735..8eaabc9a33 100644 --- a/src/Microsoft.AspNet.PipelineCore/project.json +++ b/src/Microsoft.AspNet.PipelineCore/project.json @@ -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": {}, diff --git a/src/Microsoft.AspNet.WebUtilities/FormReader.cs b/src/Microsoft.AspNet.WebUtilities/FormReader.cs index 7c6a034f78..1682e7c5b2 100644 --- a/src/Microsoft.AspNet.WebUtilities/FormReader.cs +++ b/src/Microsoft.AspNet.WebUtilities/FormReader.cs @@ -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 /// /// Parses an HTTP form body. /// - /// The HTTP form body to parse. + /// The HTTP form body to parse. /// The collection containing the parsed HTTP form body. - public static async Task> ReadFormAsync(Stream stream, CancellationToken cancellationToken = new CancellationToken()) + public static Task> ReadFormAsync(Stream stream, CancellationToken cancellationToken = new CancellationToken()) { - var reader = new FormReader(stream); + return ReadFormAsync(stream, Encoding.UTF8, cancellationToken); + } + + /// + /// Parses an HTTP form body. + /// + /// The HTTP form body to parse. + /// The collection containing the parsed HTTP form body. + public static async Task> ReadFormAsync(Stream stream, Encoding encoding, CancellationToken cancellationToken = new CancellationToken()) + { + var reader = new FormReader(stream, encoding); var accumulator = new KeyValueAccumulator(StringComparer.OrdinalIgnoreCase); var pair = await reader.ReadNextPairAsync(cancellationToken); diff --git a/src/Microsoft.AspNet.WebUtilities/Microsoft.AspNet.WebUtilities.kproj b/src/Microsoft.AspNet.WebUtilities/Microsoft.AspNet.WebUtilities.kproj index 75dd8975e7..efc8a50504 100644 --- a/src/Microsoft.AspNet.WebUtilities/Microsoft.AspNet.WebUtilities.kproj +++ b/src/Microsoft.AspNet.WebUtilities/Microsoft.AspNet.WebUtilities.kproj @@ -1,4 +1,4 @@ - + 14.0 @@ -14,4 +14,9 @@ 2.0 - + + + + + + \ No newline at end of file diff --git a/src/Microsoft.AspNet.WebUtilities/project.json b/src/Microsoft.AspNet.WebUtilities/project.json index 963fcc3178..e4e0e0cb08 100644 --- a/src/Microsoft.AspNet.WebUtilities/project.json +++ b/src/Microsoft.AspNet.WebUtilities/project.json @@ -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-*" } } } diff --git a/src/Microsoft.Net.Http.Headers/BaseHeaderParser.cs b/src/Microsoft.Net.Http.Headers/BaseHeaderParser.cs new file mode 100644 index 0000000000..679394e42d --- /dev/null +++ b/src/Microsoft.Net.Http.Headers/BaseHeaderParser.cs @@ -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 : HttpHeaderParser + { + 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; + } + } +} diff --git a/src/Microsoft.Net.Http.Headers/CacheControlHeaderValue.cs b/src/Microsoft.Net.Http.Headers/CacheControlHeaderValue.cs new file mode 100644 index 0000000000..1de23f206f --- /dev/null +++ b/src/Microsoft.Net.Http.Headers/CacheControlHeaderValue.cs @@ -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 Parser + = new GenericHeaderParser(true, GetCacheControlLength); + + private static readonly Action CheckIsValidTokenAction = CheckIsValidToken; + + private bool _noCache; + private ICollection _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 _privateHeaders; + private bool _mustRevalidate; + private bool _proxyRevalidate; + private ICollection _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 NoCacheHeaders + { + get + { + if (_noCacheHeaders == null) + { + _noCacheHeaders = new ObjectCollection(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 PrivateHeaders + { + get + { + if (_privateHeaders == null) + { + _privateHeaders = new ObjectCollection(CheckIsValidTokenAction); + } + return _privateHeaders; + } + } + + public bool MustRevalidate + { + get { return _mustRevalidate; } + set { _mustRevalidate = value; } + } + + public bool ProxyRevalidate + { + get { return _proxyRevalidate; } + set { _proxyRevalidate = value; } + } + + public ICollection Extensions + { + get + { + if (_extensions == null) + { + _extensions = new ObjectCollection(); + } + 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(); + 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 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 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(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 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"); + } + } +} diff --git a/src/Microsoft.Net.Http.Headers/ContentDispositionHeaderValue.cs b/src/Microsoft.Net.Http.Headers/ContentDispositionHeaderValue.cs new file mode 100644 index 0000000000..8a96da2efd --- /dev/null +++ b/src/Microsoft.Net.Http.Headers/ContentDispositionHeaderValue.cs @@ -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 Parser + = new GenericHeaderParser(false, GetDispositionTypeLength); + + // Use list instead of dictionary since we may have multiple parameters with the same name. + private ICollection _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 Parameters + { + get + { + if (_parameters == null) + { + _parameters = new ObjectCollection(); + } + 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)); + } + } + } + + /// + /// Sets both FileName and FileNameStar using encodings appropriate for HTTP headers. + /// + /// + public void SetHttpFileName(string fileName) + { + if (!string.IsNullOrEmpty(fileName)) + { + FileName = Sanatize(fileName); + } + else + { + FileName = fileName; + } + FileNameStar = fileName; + } + + /// + /// Sets the FileName parameter using encodings appropriate for MIME headers. + /// The FileNameStar paraemter is removed. + /// + /// + 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. in content-disposition string + // "; 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))); + } + } +} \ No newline at end of file diff --git a/src/Microsoft.Net.Http.Headers/ContentRangeHeaderValue.cs b/src/Microsoft.Net.Http.Headers/ContentRangeHeaderValue.cs new file mode 100644 index 0000000000..53fd996b9f --- /dev/null +++ b/src/Microsoft.Net.Http.Headers/ContentRangeHeaderValue.cs @@ -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 Parser + = new GenericHeaderParser(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: in ' -/' + 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 and in ' -/' + 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: in ' -/' + 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: in ' -/' + 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: in ' -/' + 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; + } + } +} diff --git a/src/Microsoft.Net.Http.Headers/CookieHeaderParser.cs b/src/Microsoft.Net.Http.Headers/CookieHeaderParser.cs new file mode 100644 index 0000000000..7c5be8753c --- /dev/null +++ b/src/Microsoft.Net.Http.Headers/CookieHeaderParser.cs @@ -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 + { + // 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; + } + } +} diff --git a/src/Microsoft.Net.Http.Headers/CookieHeaderValue.cs b/src/Microsoft.Net.Http.Headers/CookieHeaderValue.cs new file mode 100644 index 0000000000..c4c1207380 --- /dev/null +++ b/src/Microsoft.Net.Http.Headers/CookieHeaderValue.cs @@ -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 ParseList(IList inputs) + { + return MultipleValueParser.ParseValues(inputs); + } + + public static bool TryParseList(IList inputs, out IList 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() >= 0) && (Contract.Result() <= (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(); + } + } +} \ No newline at end of file diff --git a/src/Microsoft.Net.Http.Headers/EntityTagHeaderValue.cs b/src/Microsoft.Net.Http.Headers/EntityTagHeaderValue.cs new file mode 100644 index 0000000000..af10ff345e --- /dev/null +++ b/src/Microsoft.Net.Http.Headers/EntityTagHeaderValue.cs @@ -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 SingleValueParser + = new GenericHeaderParser(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 MultipleValueParser + = new GenericHeaderParser(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 ParseList(IList inputs) + { + return MultipleValueParser.ParseValues(inputs); + } + + public static bool TryParseList(IList inputs, out IList 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; + } + } +} diff --git a/src/Microsoft.Net.Http.Headers/GenericHeaderParser.cs b/src/Microsoft.Net.Http.Headers/GenericHeaderParser.cs new file mode 100644 index 0000000000..c8e5f933e2 --- /dev/null +++ b/src/Microsoft.Net.Http.Headers/GenericHeaderParser.cs @@ -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 : BaseHeaderParser + { + 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); + } + } +} diff --git a/src/Microsoft.Net.Http.Headers/HeaderNames.cs b/src/Microsoft.Net.Http.Headers/HeaderNames.cs new file mode 100644 index 0000000000..53f1d53dc9 --- /dev/null +++ b/src/Microsoft.Net.Http.Headers/HeaderNames.cs @@ -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"; + } +} \ No newline at end of file diff --git a/src/Microsoft.Net.Http.Headers/HeaderQuality.cs b/src/Microsoft.Net.Http.Headers/HeaderQuality.cs new file mode 100644 index 0000000000..c5247e1aca --- /dev/null +++ b/src/Microsoft.Net.Http.Headers/HeaderQuality.cs @@ -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 + { + /// + /// Quality factor to indicate a perfect match. + /// + public const double Match = 1.0; + + /// + /// Quality factor to indicate no match. + /// + public const double NoMatch = 0.0; + } +} \ No newline at end of file diff --git a/src/Microsoft.Net.Http.Headers/HeaderUtilities.cs b/src/Microsoft.Net.Http.Headers/HeaderUtilities.cs new file mode 100644 index 0000000000..aaf2d57470 --- /dev/null +++ b/src/Microsoft.Net.Http.Headers/HeaderUtilities.cs @@ -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 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 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(ICollection x, ICollection y) + { + return AreEqualCollections(x, y, null); + } + + internal static bool AreEqualCollections(ICollection x, ICollection y, IEqualityComparer 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; + } + } +} diff --git a/src/Microsoft.Net.Http.Headers/HttpHeaderParser.cs b/src/Microsoft.Net.Http.Headers/HttpHeaderParser.cs new file mode 100644 index 0000000000..af19f3421c --- /dev/null +++ b/src/Microsoft.Net.Http.Headers/HttpHeaderParser.cs @@ -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 + { + 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 ? "" : value.Substring(index))); + } + return result; + } + + public virtual bool TryParseValues(IList values, out IList 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(); + 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 ParseValues(IList 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(); + 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 ? "" : 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(); + } + } +} diff --git a/src/Microsoft.Net.Http.Headers/HttpParseResult.cs b/src/Microsoft.Net.Http.Headers/HttpParseResult.cs new file mode 100644 index 0000000000..76713cf1cc --- /dev/null +++ b/src/Microsoft.Net.Http.Headers/HttpParseResult.cs @@ -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, + } +} diff --git a/src/Microsoft.Net.Http.Headers/HttpRuleParser.cs b/src/Microsoft.Net.Http.Headers/HttpRuleParser.cs new file mode 100644 index 0000000000..66f6361da7 --- /dev/null +++ b/src/Microsoft.Net.Http.Headers/HttpRuleParser.cs @@ -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* + // CTL = + + 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() >= 0) && (Contract.Result() <= (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() >= 0) && (Contract.Result() <= (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() >= 0) && (Contract.Result() <= (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 = + 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 = + // LWS = [CRLF] 1*( SP | HT ) + // CTL = + // + // 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.Parsed) || + (Contract.ValueAtReturn(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; + } + } +} diff --git a/src/Microsoft.Net.Http.Headers/MediaTypeHeaderValue.cs b/src/Microsoft.Net.Http.Headers/MediaTypeHeaderValue.cs new file mode 100644 index 0000000000..1fa7535452 --- /dev/null +++ b/src/Microsoft.Net.Http.Headers/MediaTypeHeaderValue.cs @@ -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 SingleValueParser + = new GenericHeaderParser(false, GetMediaTypeLength); + private static readonly HttpHeaderParser MultipleValueParser + = new GenericHeaderParser(true, GetMediaTypeLength); + + // Use list instead of dictionary since we may have multiple parameters with the same name. + private ICollection _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 Parameters + { + get + { + if (_parameters == null) + { + _parameters = new ObjectCollection(); + } + 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); + } + } + + /// + /// MediaType = "*/*" + /// + public bool MatchesAllTypes + { + get + { + return MediaType.Equals("*/*", StringComparison.Ordinal); + } + } + + /// + /// SubType = "*" + /// + 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 ParseList(IList inputs) + { + return MultipleValueParser.ParseValues(inputs); + } + + public static bool TryParseList(IList inputs, out IList 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. in media type string "/; 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. in media type string "/; param1=value1; param2=value2" + var subtypeLength = HttpRuleParser.GetTokenLength(input, current); + + if (subtypeLength == 0) + { + return 0; + } + + // If there are no whitespaces between and in / get the media type using + // one Substring call. Otherwise get substrings for and 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)); + } + } + } +} diff --git a/src/Microsoft.Net.Http.Headers/MediaTypeHeaderValueComparer.cs b/src/Microsoft.Net.Http.Headers/MediaTypeHeaderValueComparer.cs new file mode 100644 index 0000000000..0125b2dbc4 --- /dev/null +++ b/src/Microsoft.Net.Http.Headers/MediaTypeHeaderValueComparer.cs @@ -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 +{ + /// + /// Implementation of that can compare accept media type header fields + /// based on their quality values (a.k.a q-values). + /// + public class MediaTypeHeaderValueComparer : IComparer + { + private static readonly MediaTypeHeaderValueComparer _mediaTypeComparer = + new MediaTypeHeaderValueComparer(); + + private MediaTypeHeaderValueComparer() + { + } + + public static MediaTypeHeaderValueComparer QualityComparer + { + get { return _mediaTypeComparer; } + } + + /// + /// + /// 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 + /// following their q-values in the order of specific + /// media types, subtype wildcards, and last any full wildcards. + /// + 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; + } + } +} diff --git a/src/Microsoft.Net.Http.Headers/Microsoft.Net.Http.Headers.kproj b/src/Microsoft.Net.Http.Headers/Microsoft.Net.Http.Headers.kproj new file mode 100644 index 0000000000..f6e3ec7676 --- /dev/null +++ b/src/Microsoft.Net.Http.Headers/Microsoft.Net.Http.Headers.kproj @@ -0,0 +1,23 @@ + + + + 14.0 + $(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion) + + + + 60aa2fdb-8121-4826-8d00-9a143fefaf66 + Microsoft.Net.Http.Headers + ..\..\artifacts\obj\$(MSBuildProjectName) + ..\..\artifacts\bin\$(MSBuildProjectName)\ + + + 2.0 + + + + + + + + \ No newline at end of file diff --git a/src/Microsoft.Net.Http.Headers/NameValueHeaderValue.cs b/src/Microsoft.Net.Http.Headers/NameValueHeaderValue.cs new file mode 100644 index 0000000000..fc943f7822 --- /dev/null +++ b/src/Microsoft.Net.Http.Headers/NameValueHeaderValue.cs @@ -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 SingleValueParser + = new GenericHeaderParser(false, GetNameValueLength); + internal static readonly HttpHeaderParser MultipleValueParser + = new GenericHeaderParser(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 ParseList(IList input) + { + return MultipleValueParser.ParseValues(input); + } + + public static bool TryParseList(IList input, out IList 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 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 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 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. in name/value string "=". 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. in name/value string "=" + 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 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 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(); + } + } +} diff --git a/src/Microsoft.Net.Http.Headers/NotNullAttribute.cs b/src/Microsoft.Net.Http.Headers/NotNullAttribute.cs new file mode 100644 index 0000000000..a725ce11fe --- /dev/null +++ b/src/Microsoft.Net.Http.Headers/NotNullAttribute.cs @@ -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 + { + } +} \ No newline at end of file diff --git a/src/Microsoft.Net.Http.Headers/ObjectCollection.cs b/src/Microsoft.Net.Http.Headers/ObjectCollection.cs new file mode 100644 index 0000000000..f6746df140 --- /dev/null +++ b/src/Microsoft.Net.Http.Headers/ObjectCollection.cs @@ -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 allows 'null' values to be added. This is not what we want so we use a custom Collection derived + // type to throw if 'null' gets added. Collection internally uses List which comes at some cost. In addition + // Collection.Add() calls List.InsertItem() which is an O(n) operation (compared to O(1) for List.Add()). + // This type is only used for very small collections (1-2 items) to keep the impact of using Collection small. + internal class ObjectCollection : Collection where T : class + { + private static readonly Action DefaultValidator = CheckNotNull; + + private Action _validator; + + public ObjectCollection() + : this(DefaultValidator) + { + } + + public ObjectCollection(Action 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"); + } + } + } +} \ No newline at end of file diff --git a/src/Microsoft.Net.Http.Headers/RangeConditionHeaderValue.cs b/src/Microsoft.Net.Http.Headers/RangeConditionHeaderValue.cs new file mode 100644 index 0000000000..65f976a21c --- /dev/null +++ b/src/Microsoft.Net.Http.Headers/RangeConditionHeaderValue.cs @@ -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 Parser + = new GenericHeaderParser(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; + } + } +} \ No newline at end of file diff --git a/src/Microsoft.Net.Http.Headers/RangeHeaderValue.cs b/src/Microsoft.Net.Http.Headers/RangeHeaderValue.cs new file mode 100644 index 0000000000..3045a8b387 --- /dev/null +++ b/src/Microsoft.Net.Http.Headers/RangeHeaderValue.cs @@ -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 Parser + = new GenericHeaderParser(false, GetRangeLength); + + private string _unit; + private ICollection _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 Ranges + { + get + { + if (_ranges == null) + { + _ranges = new ObjectCollection(); + } + 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: in '=-, -' + 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; + } + } +} diff --git a/src/Microsoft.Net.Http.Headers/RangeItemHeaderValue.cs b/src/Microsoft.Net.Http.Headers/RangeItemHeaderValue.cs new file mode 100644 index 0000000000..21c46f004c --- /dev/null +++ b/src/Microsoft.Net.Http.Headers/RangeItemHeaderValue.cs @@ -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 rangeCollection) + { + Contract.Requires(rangeCollection != null); + Contract.Requires(startIndex >= 0); + Contract.Ensures((Contract.Result() == 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; + } + } +} diff --git a/src/Microsoft.Net.Http.Headers/SetCookieHeaderValue.cs b/src/Microsoft.Net.Http.Headers/SetCookieHeaderValue.cs new file mode 100644 index 0000000000..3556454156 --- /dev/null +++ b/src/Microsoft.Net.Http.Headers/SetCookieHeaderValue.cs @@ -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 SingleValueParser + = new GenericHeaderParser(false, GetSetCookieLength); + private static readonly HttpHeaderParser MultipleValueParser + = new GenericHeaderParser(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 ParseList(IList inputs) + { + return MultipleValueParser.ParseValues(inputs); + } + + public static bool TryParseList(IList inputs, out IList 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 = ; 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 = + 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 = + 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(); + } + } +} \ No newline at end of file diff --git a/src/Microsoft.Net.Http.Headers/StringWithQualityHeaderValue.cs b/src/Microsoft.Net.Http.Headers/StringWithQualityHeaderValue.cs new file mode 100644 index 0000000000..41cacc9380 --- /dev/null +++ b/src/Microsoft.Net.Http.Headers/StringWithQualityHeaderValue.cs @@ -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 SingleValueParser + = new GenericHeaderParser(false, GetStringWithQualityLength); + private static readonly HttpHeaderParser MultipleValueParser + = new GenericHeaderParser(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 ParseList(IList input) + { + return MultipleValueParser.ParseValues(input); + } + + public static bool TryParseList(IList input, out IList 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: in '; q=' + 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; + } + } +} diff --git a/src/Microsoft.Net.Http.Headers/StringWithQualityHeaderValueComparer.cs b/src/Microsoft.Net.Http.Headers/StringWithQualityHeaderValueComparer.cs new file mode 100644 index 0000000000..d7a3def9e1 --- /dev/null +++ b/src/Microsoft.Net.Http.Headers/StringWithQualityHeaderValueComparer.cs @@ -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 +{ + /// + /// Implementation of 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 + /// for a comparer for media type + /// q-values. + /// + public class StringWithQualityHeaderValueComparer : IComparer + { + private static readonly StringWithQualityHeaderValueComparer _qualityComparer = + new StringWithQualityHeaderValueComparer(); + + private StringWithQualityHeaderValueComparer() + { + } + + public static StringWithQualityHeaderValueComparer QualityComparer + { + get { return _qualityComparer; } + } + + /// + /// Compares two 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 following their q-values ending up with any + /// wild-cards at the end. + /// + /// The first value to compare. + /// The second value to compare + /// The result of the comparison. + 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; + } + } +} diff --git a/src/Microsoft.Net.Http.Headers/project.json b/src/Microsoft.Net.Http.Headers/project.json new file mode 100644 index 0000000000..c0c551004b --- /dev/null +++ b/src/Microsoft.Net.Http.Headers/project.json @@ -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-*" + } + } + } +} diff --git a/test/Microsoft.AspNet.Http.Extensions.Tests/HeaderDictionaryTypeExtensionsTest.cs b/test/Microsoft.AspNet.Http.Extensions.Tests/HeaderDictionaryTypeExtensionsTest.cs new file mode 100644 index 0000000000..6b437d8925 --- /dev/null +++ b/test/Microsoft.AspNet.Http.Extensions.Tests/HeaderDictionaryTypeExtensionsTest.cs @@ -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(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(HeaderNames.ContentType); + + Assert.Null(result); + } + + [Fact] + public void GetT_KnownTypeWithInvalidValue_Null() + { + var headers = new HeaderDictionary(); + headers[HeaderNames.ContentType] = "invalid"; + + var result = headers.Get(HeaderNames.ContentType); + + Assert.Null(result); + } + + [Fact] + public void GetT_UnknownTypeWithTryParseAndValidValue_Success() + { + var headers = new HeaderDictionary(); + headers["custom"] = "valid"; + + var result = headers.Get("custom"); + Assert.NotNull(result); + } + + [Fact] + public void GetT_UnknownTypeWithTryParseAndInvalidValue_Null() + { + var headers = new HeaderDictionary(); + headers["custom"] = "invalid"; + + var result = headers.Get("custom"); + Assert.Null(result); + } + + [Fact] + public void GetT_UnknownTypeWithTryParseAndMissingValue_Null() + { + var headers = new HeaderDictionary(); + + var result = headers.Get("custom"); + Assert.Null(result); + } + + [Fact] + public void GetT_UnknownTypeWithoutTryParse_Throws() + { + var headers = new HeaderDictionary(); + headers["custom"] = "valid"; + + Assert.Throws(() => headers.Get("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(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(HeaderNames.Accept); + + Assert.Null(result); + } + + [Fact] + public void GetListT_KnownTypeWithInvalidValue_Null() + { + var headers = new HeaderDictionary(); + headers[HeaderNames.Accept] = "invalid"; + + var result = headers.GetList(HeaderNames.Accept); + + Assert.Null(result); + } + + [Fact] + public void GetListT_UnknownTypeWithTryParseListAndValidValue_Success() + { + var headers = new HeaderDictionary(); + headers["custom"] = "valid"; + + var results = headers.GetList("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("custom"); + Assert.Null(results); + } + + [Fact] + public void GetListT_UnknownTypeWithTryParseListAndMissingValue_Null() + { + var headers = new HeaderDictionary(); + + var results = headers.GetList("custom"); + Assert.Null(results); + } + + [Fact] + public void GetListT_UnknownTypeWithoutTryParseList_Throws() + { + var headers = new HeaderDictionary(); + headers["custom"] = "valid"; + + Assert.Throws(() => headers.GetList("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 values, out IList result) + { + var results = new List(); + 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; + } + } + } +} \ No newline at end of file diff --git a/test/Microsoft.AspNet.Http.Extensions.Tests/Microsoft.AspNet.Http.Extensions.Tests.kproj b/test/Microsoft.AspNet.Http.Extensions.Tests/Microsoft.AspNet.Http.Extensions.Tests.kproj index cf6c82e1bb..b3dbf90a69 100644 --- a/test/Microsoft.AspNet.Http.Extensions.Tests/Microsoft.AspNet.Http.Extensions.Tests.kproj +++ b/test/Microsoft.AspNet.Http.Extensions.Tests/Microsoft.AspNet.Http.Extensions.Tests.kproj @@ -1,4 +1,4 @@ - + 14.0 @@ -14,4 +14,9 @@ 2.0 - + + + + + + \ No newline at end of file diff --git a/test/Microsoft.AspNet.WebUtilities.Tests/QueryBuilderTests.cs b/test/Microsoft.AspNet.Http.Extensions.Tests/QueryBuilderTests.cs similarity index 98% rename from test/Microsoft.AspNet.WebUtilities.Tests/QueryBuilderTests.cs rename to test/Microsoft.AspNet.Http.Extensions.Tests/QueryBuilderTests.cs index 76d3ac977d..2f52599c83 100644 --- a/test/Microsoft.AspNet.WebUtilities.Tests/QueryBuilderTests.cs +++ b/test/Microsoft.AspNet.Http.Extensions.Tests/QueryBuilderTests.cs @@ -5,7 +5,7 @@ using System; using System.Collections.Generic; using Xunit; -namespace Microsoft.AspNet.WebUtilities +namespace Microsoft.AspNet.Http.Extensions { public class QueryBuilderTests { diff --git a/test/Microsoft.AspNet.Http.Extensions.Tests/UseWithServicesTests.cs b/test/Microsoft.AspNet.Http.Extensions.Tests/UseWithServicesTests.cs index 9dc54ab09e..1ed29d112e 100644 --- a/test/Microsoft.AspNet.Http.Extensions.Tests/UseWithServicesTests.cs +++ b/test/Microsoft.AspNet.Http.Extensions.Tests/UseWithServicesTests.cs @@ -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); } } } \ No newline at end of file diff --git a/test/Microsoft.AspNet.PipelineCore.Tests/DefaultHttpRequestTests.cs b/test/Microsoft.AspNet.PipelineCore.Tests/DefaultHttpRequestTests.cs index c82bfd67b7..397d480580 100644 --- a/test/Microsoft.AspNet.PipelineCore.Tests/DefaultHttpRequestTests.cs +++ b/test/Microsoft.AspNet.PipelineCore.Tests/DefaultHttpRequestTests.cs @@ -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() { diff --git a/test/Microsoft.AspNet.WebUtilities.Tests/Microsoft.AspNet.WebUtilities.Tests.kproj b/test/Microsoft.AspNet.WebUtilities.Tests/Microsoft.AspNet.WebUtilities.Tests.kproj index 1a6f3883fd..098750fe14 100644 --- a/test/Microsoft.AspNet.WebUtilities.Tests/Microsoft.AspNet.WebUtilities.Tests.kproj +++ b/test/Microsoft.AspNet.WebUtilities.Tests/Microsoft.AspNet.WebUtilities.Tests.kproj @@ -1,4 +1,4 @@ - + 14.0 @@ -14,4 +14,9 @@ 2.0 - + + + + + + \ No newline at end of file diff --git a/test/Microsoft.AspNet.WebUtilities.Tests/project.json b/test/Microsoft.AspNet.WebUtilities.Tests/project.json index c3fcb3e97c..a445ee721d 100644 --- a/test/Microsoft.AspNet.WebUtilities.Tests/project.json +++ b/test/Microsoft.AspNet.WebUtilities.Tests/project.json @@ -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": { } } } diff --git a/test/Microsoft.Net.Http.Headers.Tests/CacheControlHeaderValueTest.cs b/test/Microsoft.Net.Http.Headers.Tests/CacheControlHeaderValueTest.cs new file mode 100644 index 0000000000..0ac0fe3d2c --- /dev/null +++ b/test/Microsoft.Net.Http.Headers.Tests/CacheControlHeaderValueTest.cs @@ -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(() => cacheControl.NoCacheHeaders.Add(null)); + Assert.Throws(() => 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(() => cacheControl.PrivateHeaders.Add(null)); + Assert.Throws(() => 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(() => 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(() => 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 + } +} diff --git a/test/Microsoft.Net.Http.Headers.Tests/ContentDispositionHeaderValueTest.cs b/test/Microsoft.Net.Http.Headers.Tests/ContentDispositionHeaderValueTest.cs new file mode 100644 index 0000000000..41bb2bdaae --- /dev/null +++ b/test/Microsoft.Net.Http.Headers.Tests/ContentDispositionHeaderValueTest.cs @@ -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(() => 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(() => 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(() => 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(() => 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. ."); + 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 ValidContentDispositionTestCases = new TheoryData() + { + { "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 Section 2.8 of RFC 2183.). + { @"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 Section 2.8 of RFC 2183.). The extension parameter actually uses backslash-escapes. This tests whether the UA properly skips the parameter. + { @"attachment; FILENAME=""foo.html""", new ContentDispositionHeaderValue("attachment") { FileName = @"""foo.html""" } }, + { @"attachment; filename=foo.html", new ContentDispositionHeaderValue("attachment") { FileName = "foo.html" } }, // 'attachment', specifying a filename of foo.html using a token instead of a quoted-string. + { @"attachment; filename='foo.bar'", new ContentDispositionHeaderValue("attachment") { FileName = "'foo.bar'" } }, // 'attachment', specifying a filename of 'foo.bar' using single quotes. + { @"attachment; filename=""foo-ä.html""", new ContentDispositionHeaderValue("attachment" ) { Parameters = { new NameValueHeaderValue("filename", @"""foo-ä.html""") } } }, // 'attachment', specifying a filename of foo-ä.html, using plain ISO-8859-1 + { @"attachment; filename=""foo-ä.html""", new ContentDispositionHeaderValue("attachment") { FileName = @"""foo-ä.html""" } }, // 'attachment', specifying a filename of foo-ä.html, which happens to be foo-ä.html using UTF-8 encoding. + { @"attachment; filename=""foo-%41.html""", new ContentDispositionHeaderValue("attachment") { Parameters = { new NameValueHeaderValue("filename", @"""foo-%41.html""") } } }, + { @"attachment; filename=""50%.html""", new ContentDispositionHeaderValue("attachment") { Parameters = { new NameValueHeaderValue("filename", @"""50%.html""") } } }, + { @"attachment; filename=""foo-%\41.html""", new ContentDispositionHeaderValue("attachment") { Parameters = { new NameValueHeaderValue("filename", @"""foo-%\41.html""") } } }, // 'attachment', specifying a filename of foo-%41.html, using an escape character (this tests whether adding an escape character inside a %xx sequence can be used to disable the non-conformant %xx-unescaping). + { @"attachment; name=""foo-%41.html""", new ContentDispositionHeaderValue("attachment") { Name = @"""foo-%41.html""" } }, // 'attachment', specifying a name parameter of foo-%41.html. (this test was added to observe the behavior of the (unspecified) treatment of ""name"" as synonym for ""filename""; see Ned Freed's summary where this comes from in MIME messages) + { @"attachment; filename=""ä-%41.html""", new ContentDispositionHeaderValue("attachment") { Parameters = { new NameValueHeaderValue("filename", @"""ä-%41.html""") } } }, // 'attachment', specifying a filename parameter of ä-%41.html. (this test was added to observe the behavior when non-ASCII characters and percent-hexdig sequences are combined) + { @"attachment; filename=""foo-%c3%a4-%e2%82%ac.html""", new ContentDispositionHeaderValue("attachment") { FileName = @"""foo-%c3%a4-%e2%82%ac.html""" } }, // 'attachment', specifying a filename of foo-%c3%a4-%e2%82%ac.html, using raw percent encoded UTF-8 to represent foo-ä-€.html + { @"attachment; filename =""foo.html""", new ContentDispositionHeaderValue("attachment") { FileName = @"""foo.html""" } }, + { @"attachment; xfilename=foo.html", new ContentDispositionHeaderValue("attachment" ) { Parameters = { new NameValueHeaderValue("xfilename", "foo.html") } } }, + { @"attachment; filename=""/foo.html""", new ContentDispositionHeaderValue("attachment") { FileName = @"""/foo.html""" } }, + { @"attachment; creation-date=""Wed, 12 Feb 1997 16:29:51 -0500""", new ContentDispositionHeaderValue("attachment") { Parameters = { new NameValueHeaderValue("creation-date", @"""Wed, 12 Feb 1997 16:29:51 -0500""") } } }, + { @"attachment; modification-date=""Wed, 12 Feb 1997 16:29:51 -0500""", new ContentDispositionHeaderValue("attachment") { Parameters = { new NameValueHeaderValue("modification-date", @"""Wed, 12 Feb 1997 16:29:51 -0500""") } } }, + { @"foobar", new ContentDispositionHeaderValue("foobar") }, // @"This should be equivalent to using ""attachment""." + { @"attachment; example=""filename=example.txt""", new ContentDispositionHeaderValue("attachment") { Parameters = { new NameValueHeaderValue("example", @"""filename=example.txt""") } } }, + { @"attachment; filename*=iso-8859-1''foo-%E4.html", new ContentDispositionHeaderValue("attachment") { Parameters = { new NameValueHeaderValue("filename*", "iso-8859-1''foo-%E4.html") } } }, // 'attachment', specifying a filename of foo-ä.html, using RFC2231 encoded ISO-8859-1 + { @"attachment; filename*=UTF-8''foo-%c3%a4-%e2%82%ac.html", new ContentDispositionHeaderValue("attachment") { Parameters = { new NameValueHeaderValue("filename*", "UTF-8''foo-%c3%a4-%e2%82%ac.html") } } }, // 'attachment', specifying a filename of foo-ä-€.html, using RFC2231 encoded UTF-8 + { @"attachment; filename*=''foo-%c3%a4-%e2%82%ac.html", new ContentDispositionHeaderValue("attachment") { Parameters = { new NameValueHeaderValue("filename*", "''foo-%c3%a4-%e2%82%ac.html") } } }, // Behavior is undefined in RFC 2231, the charset part is missing, although UTF-8 was used. + { @"attachment; filename*=UTF-8''foo-a%22.html", new ContentDispositionHeaderValue("attachment") { FileNameStar = @"foo-a"".html" } }, + { @"attachment; filename*= UTF-8''foo-%c3%a4.html", new ContentDispositionHeaderValue("attachment") { FileNameStar = "foo-ä.html" } }, + { @"attachment; filename* =UTF-8''foo-%c3%a4.html", new ContentDispositionHeaderValue("attachment") { FileNameStar = "foo-ä.html" } }, + { @"attachment; filename*=UTF-8''A-%2541.html", new ContentDispositionHeaderValue("attachment") { FileNameStar = "A-%41.html" } }, + { @"attachment; filename*=UTF-8''%5cfoo.html", new ContentDispositionHeaderValue("attachment") { FileNameStar = @"\foo.html" } }, + { @"attachment; filename=""foo-ae.html""; filename*=UTF-8''foo-%c3%a4.html", new ContentDispositionHeaderValue("attachment") { FileName = @"""foo-ae.html""", FileNameStar = "foo-ä.html" } }, + { @"attachment; filename*=UTF-8''foo-%c3%a4.html; filename=""foo-ae.html""", new ContentDispositionHeaderValue("attachment") { FileNameStar = "foo-ä.html", FileName = @"""foo-ae.html""" } }, + { @"attachment; foobar=x; filename=""foo.html""", new ContentDispositionHeaderValue("attachment") { FileName = @"""foo.html""", Parameters = { new NameValueHeaderValue("foobar", "x") } } }, + { @"attachment; filename=""=?ISO-8859-1?Q?foo-=E4.html?=""", new ContentDispositionHeaderValue("attachment") { FileName = @"""=?ISO-8859-1?Q?foo-=E4.html?=""" } }, // attachment; filename="=?ISO-8859-1?Q?foo-=E4.html?=" + { @"attachment; filename=""=?utf-8?B?Zm9vLeQuaHRtbA==?=""", new ContentDispositionHeaderValue("attachment") { FileName = @"""=?utf-8?B?Zm9vLeQuaHRtbA==?=""" } }, // attachment; filename="=?utf-8?B?Zm9vLeQuaHRtbA==?=" + { @"attachment; filename=foo.html ;", new ContentDispositionHeaderValue("attachment") { FileName="foo.html" } }, // 'attachment', specifying a filename of foo.html using a token instead of a quoted-string, and adding a trailing semicolon., + }; + + [Theory] + [MemberData(nameof(ValidContentDispositionTestCases))] + public void ContentDispositionHeaderValue_ParseValid_Success(string input, ContentDispositionHeaderValue expected) + { + // System.Diagnostics.Debugger.Launch(); + var result = ContentDispositionHeaderValue.Parse(input); + Assert.Equal(expected, result); + } + + [Theory] + // Invalid values + [InlineData(@"""inline""")] // @"'inline' only, using double quotes", false) }, + [InlineData(@"""attachment""")] // @"'attachment' only, using double quotes", false) }, + [InlineData(@"attachment; filename=foo bar.html")] // @"'attachment', specifying a filename of foo bar.html without using quoting.", false) }, + // Duplicate file name parameter + // @"attachment; filename=""foo.html""; // filename=""bar.html""", @"'attachment', specifying two filename parameters. This is invalid syntax.", false) }, + [InlineData(@"attachment; filename=foo[1](2).html")] // @"'attachment', specifying a filename of foo[1](2).html, but missing the quotes. Also, ""["", ""]"", ""("" and "")"" are not allowed in the HTTP token production.", false) }, + [InlineData(@"attachment; filename=foo-ä.html")] // @"'attachment', specifying a filename of foo-ä.html, but missing the quotes.", false) }, + // HTML escaping, not supported + // @"attachment; filename=foo-ä.html", // "'attachment', specifying a filename of foo-ä.html (which happens to be foo-ä.html using UTF-8 encoding) but missing the quotes.", false) }, + [InlineData(@"filename=foo.html")] // @"Disposition type missing, filename specified.", false) }, + [InlineData(@"x=y; filename=foo.html")] // @"Disposition type missing, filename specified after extension parameter.", false) }, + [InlineData(@"""foo; filename=bar;baz""; filename=qux")] // @"Disposition type missing, filename ""qux"". Can it be more broken? (Probably)", false) }, + [InlineData(@"filename=foo.html, filename=bar.html")] // @"Disposition type missing, two filenames specified separated by a comma (this is syntactically equivalent to have two instances of the header with one filename parameter each).", false) }, + [InlineData(@"; filename=foo.html")] // @"Disposition type missing (but delimiter present), filename specified.", false) }, + // This is permitted as a parameter without a value + // @"inline; attachment; filename=foo.html", // @"Both disposition types specified.", false) }, + // This is permitted as a parameter without a value + // @"inline; attachment; filename=foo.html", // @"Both disposition types specified.", false) }, + [InlineData(@"attachment; filename=""foo.html"".txt")] // @"'attachment', specifying a filename parameter that is broken (quoted-string followed by more characters). This is invalid syntax. ", false) }, + [InlineData(@"attachment; filename=""bar")] // @"'attachment', specifying a filename parameter that is broken (missing ending double quote). This is invalid syntax.", false) }, + [InlineData(@"attachment; filename=foo""bar;baz""qux")] // @"'attachment', specifying a filename parameter that is broken (disallowed characters in token syntax). This is invalid syntax.", false) }, + [InlineData(@"attachment; filename=foo.html, attachment; filename=bar.html")] // @"'attachment', two comma-separated instances of the header field. As Content-Disposition doesn't use a list-style syntax, this is invalid syntax and, according to RFC 2616, Section 4.2, roughly equivalent to having two separate header field instances.", false) }, + [InlineData(@"filename=foo.html; attachment")] // @"filename parameter and disposition type reversed.", false) }, + // Escaping is not verified + // @"attachment; filename*=iso-8859-1''foo-%c3%a4-%e2%82%ac.html", // @"'attachment', specifying a filename of foo-ä-€.html, using RFC2231 encoded UTF-8, but declaring ISO-8859-1", false) }, + // Escaping is not verified + // @"attachment; filename *=UTF-8''foo-%c3%a4.html", // @"'attachment', specifying a filename of foo-ä.html, using RFC2231 encoded UTF-8, with whitespace before ""*=""", false) }, + // Escaping is not verified + // @"attachment; filename*=""UTF-8''foo-%c3%a4.html""", // @"'attachment', specifying a filename of foo-ä.html, using RFC2231 encoded UTF-8, with double quotes around the parameter value.", false) }, + [InlineData(@"attachment; filename==?ISO-8859-1?Q?foo-=E4.html?=")] // @"Uses RFC 2047 style encoded word. ""="" is invalid inside the token production, so this is invalid.", false) }, + [InlineData(@"attachment; filename==?utf-8?B?Zm9vLeQuaHRtbA==?=")] // @"Uses RFC 2047 style encoded word. ""="" is invalid inside the token production, so this is invalid.", false) }, + public void ContentDispositionHeaderValue_ParseInvalid_Throws(string input) + { + Assert.Throws(() => 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(() => 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(() => new ContentDispositionHeaderValue(contentDisposition)); + } + } +} diff --git a/test/Microsoft.Net.Http.Headers.Tests/ContentRangeHeaderValueTest.cs b/test/Microsoft.Net.Http.Headers.Tests/ContentRangeHeaderValueTest.cs new file mode 100644 index 0000000000..d479f4d46f --- /dev/null +++ b/test/Microsoft.Net.Http.Headers.Tests/ContentRangeHeaderValueTest.cs @@ -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(() => 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(() => new ContentRangeHeaderValue(-1, 1)); + Assert.Throws(() => new ContentRangeHeaderValue(0, -1)); + Assert.Throws(() => 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(() => new ContentRangeHeaderValue(-1, 1, 2)); + Assert.Throws(() => new ContentRangeHeaderValue(0, -1, 2)); + Assert.Throws(() => new ContentRangeHeaderValue(0, 1, -1)); + Assert.Throws(() => new ContentRangeHeaderValue(2, 1, 3)); + Assert.Throws(() => 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(() => range.Unit = null); + Assert.Throws(() => range.Unit = ""); + Assert.Throws(() => range.Unit = " x"); + Assert.Throws(() => range.Unit = "x "); + Assert.Throws(() => 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. "); + 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(() => 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); + } + } +} diff --git a/test/Microsoft.Net.Http.Headers.Tests/CookieHeaderValueTest.cs b/test/Microsoft.Net.Http.Headers.Tests/CookieHeaderValueTest.cs new file mode 100644 index 0000000000..9a25389a50 --- /dev/null +++ b/test/Microsoft.Net.Http.Headers.Tests/CookieHeaderValueTest.cs @@ -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 CookieHeaderDataSet + { + get + { + var dataset = new TheoryData(); + 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 InvalidCookieHeaderDataSet + { + get + { + return new TheoryData + { + "=value", + "name=value;", + "name=value,", + }; + } + } + + public static TheoryData InvalidCookieNames + { + get + { + return new TheoryData + { + "", + "{acb}", + "[acb]", + "\"acb\"", + "a,b", + "a;b", + "a\\b", + "a b", + }; + } + } + + public static TheoryData InvalidCookieValues + { + get + { + return new TheoryData + { + { "\"" }, + { "a,b" }, + { "a;b" }, + { "a\\b" }, + { "\"abc" }, + { "a\"bc" }, + { "abc\"" }, + { "a b" }, + }; + } + } + public static TheoryData, string[]> ListOfCookieHeaderDataSet + { + get + { + var dataset = new TheoryData, 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(() => new CookieHeaderValue(null, "value")); + } + + [Theory] + [MemberData(nameof(InvalidCookieNames))] + public void CookieHeaderValue_CtorThrowsOnInvalidName(string name) + { + Assert.Throws(() => new CookieHeaderValue(name, "value")); + } + + [Theory] + [MemberData(nameof(InvalidCookieValues))] + public void CookieHeaderValue_CtorThrowsOnInvalidValue(string value) + { + Assert.Throws(() => 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(() => 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 cookies, string[] input) + { + var results = CookieHeaderValue.ParseList(input); + + Assert.Equal(cookies, results); + } + + [Theory] + [MemberData(nameof(ListOfCookieHeaderDataSet))] + public void CookieHeaderValue_TryParseList_AcceptsValidValues(IList cookies, string[] input) + { + IList results; + bool result = CookieHeaderValue.TryParseList(input, out results); + Assert.True(result); + + Assert.Equal(cookies, results); + } + } +} diff --git a/test/Microsoft.Net.Http.Headers.Tests/DateParserTest.cs b/test/Microsoft.Net.Http.Headers.Tests/DateParserTest.cs new file mode 100644 index 0000000000..61479e9adf --- /dev/null +++ b/test/Microsoft.Net.Http.Headers.Tests/DateParserTest.cs @@ -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 + } +} diff --git a/test/Microsoft.Net.Http.Headers.Tests/EntityTagHeaderValueTest.cs b/test/Microsoft.Net.Http.Headers.Tests/EntityTagHeaderValueTest.cs new file mode 100644 index 0000000000..d94ce6409f --- /dev/null +++ b/test/Microsoft.Net.Http.Headers.Tests/EntityTagHeaderValueTest.cs @@ -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(() => new EntityTagHeaderValue(null)); + // null and empty should be treated the same. So we also throw for empty strings. + Assert.Throws(() => 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. ."); + 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 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 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 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(() => 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 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(() => 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(() => new EntityTagHeaderValue(tag)); + } + } +} diff --git a/test/Microsoft.Net.Http.Headers.Tests/MediaTypeHeaderValueComparerTests.cs b/test/Microsoft.Net.Http.Headers.Tests/MediaTypeHeaderValueComparerTests.cs new file mode 100644 index 0000000000..8ef17a55aa --- /dev/null +++ b/test/Microsoft.Net.Http.Headers.Tests/MediaTypeHeaderValueComparerTests.cs @@ -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 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 unsorted, IEnumerable 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); + } + } +} diff --git a/test/Microsoft.Net.Http.Headers.Tests/MediaTypeHeaderValueTest.cs b/test/Microsoft.Net.Http.Headers.Tests/MediaTypeHeaderValueTest.cs new file mode 100644 index 0000000000..140d629497 --- /dev/null +++ b/test/Microsoft.Net.Http.Headers.Tests/MediaTypeHeaderValueTest.cs @@ -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(() => new MediaTypeHeaderValue(null)); + // null and empty should be treated the same. So we also throw for empty strings. + Assert.Throws(() => 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(() => 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(() => new MediaTypeHeaderValue("application/xml", -0.01)); + } + + [Fact] + public void Quality_GreaterThanOne_Throw() + { + var mediaType = new MediaTypeHeaderValue("application/xml"); + Assert.Throws(() => 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. ."); + 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 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 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(() => 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 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(() => 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(() => new MediaTypeHeaderValue(mediaType)); + } + } +} diff --git a/test/Microsoft.Net.Http.Headers.Tests/Microsoft.Net.Http.Headers.Tests.kproj b/test/Microsoft.Net.Http.Headers.Tests/Microsoft.Net.Http.Headers.Tests.kproj new file mode 100644 index 0000000000..cbf789a8a0 --- /dev/null +++ b/test/Microsoft.Net.Http.Headers.Tests/Microsoft.Net.Http.Headers.Tests.kproj @@ -0,0 +1,23 @@ + + + + 14.0 + $(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion) + + + + e6bb7ad1-bd10-4a23-b780-f4a86adf00d1 + Microsoft.Net.Http.Headers.Tests + ..\..\artifacts\obj\$(MSBuildProjectName) + ..\..\artifacts\bin\$(MSBuildProjectName)\ + + + 2.0 + + + + + + + + \ No newline at end of file diff --git a/test/Microsoft.Net.Http.Headers.Tests/NameValueHeaderValueTest.cs b/test/Microsoft.Net.Http.Headers.Tests/NameValueHeaderValueTest.cs new file mode 100644 index 0000000000..3ae04a9212 --- /dev/null +++ b/test/Microsoft.Net.Http.Headers.Tests/NameValueHeaderValueTest.cs @@ -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(() => new NameValueHeaderValue(null)); + // null and empty should be treated the same. So we also throw for empty strings. + Assert.Throws(() => 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(() => { var x = new NameValueHeaderValue("name"); x.Value = " x "; }); + Assert.Throws(() => { 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), " vs. ."); + + nameValue1.Value = "token"; + nameValue2.Value = null; + Assert.False(nameValue1.Equals(nameValue2), "token vs. ."); + + nameValue1.Value = null; + nameValue2.Value = "token"; + Assert.False(nameValue1.Equals(nameValue2), " 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), " 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. ."); + } + + [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 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(() => 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 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(() => 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(() => new NameValueHeaderValue(name, value)); + } + + #endregion + } +} diff --git a/test/Microsoft.Net.Http.Headers.Tests/RangeConditionHeaderValueTest.cs b/test/Microsoft.Net.Http.Headers.Tests/RangeConditionHeaderValueTest.cs new file mode 100644 index 0000000000..b47b03b026 --- /dev/null +++ b/test/Microsoft.Net.Http.Headers.Tests/RangeConditionHeaderValueTest.cs @@ -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(() => 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(() => 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. "); + 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(() => 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 + } +} diff --git a/test/Microsoft.Net.Http.Headers.Tests/RangeHeaderValueTest.cs b/test/Microsoft.Net.Http.Headers.Tests/RangeHeaderValueTest.cs new file mode 100644 index 0000000000..22dc5dc749 --- /dev/null +++ b/test/Microsoft.Net.Http.Headers.Tests/RangeHeaderValueTest.cs @@ -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(() => new RangeHeaderValue(5, 2)); + } + + [Fact] + public void Unit_GetAndSetValidAndInvalidValues_MatchExpectation() + { + var range = new RangeHeaderValue(); + range.Unit = "myunit"; + Assert.Equal("myunit", range.Unit); + + Assert.Throws(() => range.Unit = null); + Assert.Throws(() => range.Unit = ""); + Assert.Throws(() => range.Unit = " x"); + Assert.Throws(() => range.Unit = "x "); + Assert.Throws(() => 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. "); + 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(() => 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 + } +} diff --git a/test/Microsoft.Net.Http.Headers.Tests/RangeItemHeaderValueTest.cs b/test/Microsoft.Net.Http.Headers.Tests/RangeItemHeaderValueTest.cs new file mode 100644 index 0000000000..573ae8964e --- /dev/null +++ b/test/Microsoft.Net.Http.Headers.Tests/RangeItemHeaderValueTest.cs @@ -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(() => new RangeItemHeaderValue(null, null)); + } + + [Fact] + public void Ctor_FromValueNegative_Throw() + { + Assert.Throws(() => new RangeItemHeaderValue(-1, null)); + } + + [Fact] + public void Ctor_FromGreaterThanToValue_Throw() + { + Assert.Throws(() => new RangeItemHeaderValue(2, 1)); + } + + [Fact] + public void Ctor_ToValueNegative_Throw() + { + Assert.Throws(() => 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(1, 2), new Tuple(3, null), + new Tuple(null, 6)); + CheckValidTryParse("1-2,", new Tuple(1, 2)); + CheckValidTryParse("1-", new Tuple(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[] 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); + } + } + } +} diff --git a/test/Microsoft.Net.Http.Headers.Tests/SetCookieHeaderValueTest.cs b/test/Microsoft.Net.Http.Headers.Tests/SetCookieHeaderValueTest.cs new file mode 100644 index 0000000000..ee3e12292b --- /dev/null +++ b/test/Microsoft.Net.Http.Headers.Tests/SetCookieHeaderValueTest.cs @@ -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 SetCookieHeaderDataSet + { + get + { + var dataset = new TheoryData(); + 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 InvalidSetCookieHeaderDataSet + { + get + { + return new TheoryData + { + "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 InvalidCookieNames + { + get + { + return new TheoryData + { + "", + "{acb}", + "[acb]", + "\"acb\"", + "a,b", + "a;b", + "a\\b", + }; + } + } + + public static TheoryData InvalidCookieValues + { + get + { + return new TheoryData + { + { "\"" }, + { "a,b" }, + { "a;b" }, + { "a\\b" }, + { "\"abc" }, + { "a\"bc" }, + { "abc\"" }, + }; + } + } + public static TheoryData, string[]> ListOfSetCookieHeaderDataSet + { + get + { + var dataset = new TheoryData, 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(() => new SetCookieHeaderValue(null, "value")); + } + + [Theory] + [MemberData(nameof(InvalidCookieNames))] + public void SetCookieHeaderValue_CtorThrowsOnInvalidName(string name) + { + Assert.Throws(() => new SetCookieHeaderValue(name, "value")); + } + + [Theory] + [MemberData(nameof(InvalidCookieValues))] + public void SetCookieHeaderValue_CtorThrowsOnInvalidValue(string value) + { + Assert.Throws(() => 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(() => 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 cookies, string[] input) + { + var results = SetCookieHeaderValue.ParseList(input); + + Assert.Equal(cookies, results); + } + + [Theory] + [MemberData(nameof(ListOfSetCookieHeaderDataSet))] + public void SetCookieHeaderValue_TryParseList_AcceptsValidValues(IList cookies, string[] input) + { + IList results; + bool result = SetCookieHeaderValue.TryParseList(input, out results); + Assert.True(result); + + Assert.Equal(cookies, results); + } + } +} diff --git a/test/Microsoft.Net.Http.Headers.Tests/StringWithQualityHeaderValueComparerTest.cs b/test/Microsoft.Net.Http.Headers.Tests/StringWithQualityHeaderValueComparerTest.cs new file mode 100644 index 0000000000..df4e729493 --- /dev/null +++ b/test/Microsoft.Net.Http.Headers.Tests/StringWithQualityHeaderValueComparerTest.cs @@ -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 StringWithQualityHeaderValueComparerTestsBeforeAfterSortedValues + { + get + { + return new TheoryData + { + { + 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 unsorted, IEnumerable 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)); + } + } +} diff --git a/test/Microsoft.Net.Http.Headers.Tests/StringWithQualityHeaderValueTest.cs b/test/Microsoft.Net.Http.Headers.Tests/StringWithQualityHeaderValueTest.cs new file mode 100644 index 0000000000..0a57c61958 --- /dev/null +++ b/test/Microsoft.Net.Http.Headers.Tests/StringWithQualityHeaderValueTest.cs @@ -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(() => new StringWithQualityHeaderValue(null)); + Assert.Throws(() => new StringWithQualityHeaderValue("")); + Assert.Throws(() => 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(() => new StringWithQualityHeaderValue(null, 0.1)); + Assert.Throws(() => new StringWithQualityHeaderValue("", 0.1)); + Assert.Throws(() => new StringWithQualityHeaderValue("in valid", 0.1)); + + Assert.Throws(() => new StringWithQualityHeaderValue("t", 1.1)); + Assert.Throws(() => 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. "); + 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(() => 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 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 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(() => 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 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 + } +} diff --git a/test/Microsoft.Net.Http.Headers.Tests/project.json b/test/Microsoft.Net.Http.Headers.Tests/project.json new file mode 100644 index 0000000000..4ffa75e704 --- /dev/null +++ b/test/Microsoft.Net.Http.Headers.Tests/project.json @@ -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-*" + } + } + } +} From 68be1d1b19108e00ea49db4e5de5352c06699efd Mon Sep 17 00:00:00 2001 From: Chris Ross Date: Thu, 15 Jan 2015 11:52:34 -0800 Subject: [PATCH 07/16] #162 - Change PipelineCore namespace to Http.Core. Part-1. --- src/Microsoft.AspNet.Owin/OwinEnvironment.cs | 2 +- src/Microsoft.AspNet.Owin/OwinExtensions.cs | 2 +- .../BufferingHelper.cs | 2 +- .../Collections/FormCollection.cs | 2 +- .../Collections/FormFileCollection.cs | 2 +- .../Collections/HeaderDictionary.cs | 4 +- .../Collections/ItemsDictionary.cs | 2 +- .../Collections/ReadableStringCollection.cs | 2 +- .../Collections/RequestCookiesCollection.cs | 4 +- .../Collections/ResponseCookies.cs | 2 +- .../Collections/SessionCollection.cs | 2 +- .../DefaultHttpContext.cs | 12 +++--- .../DefaultHttpRequest.cs | 6 +-- .../DefaultHttpResponse.cs | 8 ++-- .../FormFeature.cs | 4 +- src/Microsoft.AspNet.PipelineCore/FormFile.cs | 2 +- ...equestFeature.cs => HttpRequestFeature.cs} | 6 +-- ...ponseFeature.cs => HttpResponseFeature.cs} | 6 +-- .../IFormFeature.cs | 2 +- .../IItemsFeature.cs | 2 +- .../IQueryFeature.cs | 2 +- .../IRequestCookiesFeature.cs | 2 +- .../IResponseCookiesFeature.cs | 4 +- .../IServiceProvidersFeature.cs | 2 +- .../Infrastructure/FeatureReference.cs | 2 +- .../Infrastructure/ParsingHelpers.cs | 2 +- .../ItemsFeature.cs | 2 +- .../NotNullAttribute.cs | 2 +- .../QueryFeature.cs | 6 +-- .../ReferenceReadStream.cs | 2 +- .../RequestCookiesFeature.cs | 6 +-- .../ResponseCookiesFeature.cs | 6 +-- .../Security/AuthTypeContext.cs | 2 +- .../Security/AuthenticateContext.cs | 2 +- .../Security/ChallengeContext.cs | 2 +- .../Security/HttpAuthenticationFeature.cs | 2 +- .../Security/SignInContext.cs | 2 +- .../Security/SignOutContext.cs | 2 +- .../ServiceProvidersFeature.cs | 2 +- .../WebSocketAcceptContext.cs | 2 +- .../HeaderDictionaryTypeExtensionsTest.cs | 2 +- .../HttpResponseSendingExtensionsTests.cs | 2 +- .../UseWithServicesTests.cs | 2 +- .../HttpResponseWritingExtensionsTests.cs | 2 +- .../MapPathMiddlewareTests.cs | 2 +- .../MapPredicateMiddlewareTests.cs | 2 +- .../OwinEnvironmentTests.cs | 3 +- .../DefaultHttpContextTests.cs | 2 +- .../DefaultHttpRequestTests.cs | 2 +- .../FormFeatureTests.cs | 2 +- .../HeaderDictionaryTests.cs | 4 +- .../Properties/AssemblyInfo.cs | 39 ------------------- .../QueryFeatureTests.cs | 2 +- 53 files changed, 77 insertions(+), 117 deletions(-) rename src/Microsoft.AspNet.PipelineCore/{DefaultHttpRequestFeature.cs => HttpRequestFeature.cs} (87%) rename src/Microsoft.AspNet.PipelineCore/{DefaultHttpResponseFeature.cs => HttpResponseFeature.cs} (85%) delete mode 100644 test/Microsoft.AspNet.PipelineCore.Tests/Properties/AssemblyInfo.cs diff --git a/src/Microsoft.AspNet.Owin/OwinEnvironment.cs b/src/Microsoft.AspNet.Owin/OwinEnvironment.cs index 8a0e7cbcfd..9ff85950f2 100644 --- a/src/Microsoft.AspNet.Owin/OwinEnvironment.cs +++ b/src/Microsoft.AspNet.Owin/OwinEnvironment.cs @@ -16,7 +16,7 @@ using System.Threading.Tasks; using Microsoft.AspNet.Http; using Microsoft.AspNet.HttpFeature; using Microsoft.AspNet.HttpFeature.Security; -using Microsoft.AspNet.PipelineCore.Security; +using Microsoft.AspNet.Http.Core.Security; namespace Microsoft.AspNet.Owin { diff --git a/src/Microsoft.AspNet.Owin/OwinExtensions.cs b/src/Microsoft.AspNet.Owin/OwinExtensions.cs index 5bd2e0a9fa..fe4b17d1ac 100644 --- a/src/Microsoft.AspNet.Owin/OwinExtensions.cs +++ b/src/Microsoft.AspNet.Owin/OwinExtensions.cs @@ -7,7 +7,7 @@ using System.Threading.Tasks; using Microsoft.AspNet.Http; using Microsoft.AspNet.FeatureModel; using Microsoft.AspNet.Owin; -using Microsoft.AspNet.PipelineCore; +using Microsoft.AspNet.Http.Core; namespace Microsoft.AspNet.Builder { diff --git a/src/Microsoft.AspNet.PipelineCore/BufferingHelper.cs b/src/Microsoft.AspNet.PipelineCore/BufferingHelper.cs index b10c017c36..58ef9c509e 100644 --- a/src/Microsoft.AspNet.PipelineCore/BufferingHelper.cs +++ b/src/Microsoft.AspNet.PipelineCore/BufferingHelper.cs @@ -6,7 +6,7 @@ using System.IO; using Microsoft.AspNet.Http; using Microsoft.AspNet.WebUtilities; -namespace Microsoft.AspNet.PipelineCore +namespace Microsoft.AspNet.Http.Core { public static class BufferingHelper { diff --git a/src/Microsoft.AspNet.PipelineCore/Collections/FormCollection.cs b/src/Microsoft.AspNet.PipelineCore/Collections/FormCollection.cs index a80fea12d1..55d7b321a0 100644 --- a/src/Microsoft.AspNet.PipelineCore/Collections/FormCollection.cs +++ b/src/Microsoft.AspNet.PipelineCore/Collections/FormCollection.cs @@ -4,7 +4,7 @@ using System.Collections.Generic; using Microsoft.AspNet.Http; -namespace Microsoft.AspNet.PipelineCore.Collections +namespace Microsoft.AspNet.Http.Core.Collections { /// /// Contains the parsed form values. diff --git a/src/Microsoft.AspNet.PipelineCore/Collections/FormFileCollection.cs b/src/Microsoft.AspNet.PipelineCore/Collections/FormFileCollection.cs index 2ef4d29476..d33992bff8 100644 --- a/src/Microsoft.AspNet.PipelineCore/Collections/FormFileCollection.cs +++ b/src/Microsoft.AspNet.PipelineCore/Collections/FormFileCollection.cs @@ -5,7 +5,7 @@ using System.Collections.Generic; using Microsoft.AspNet.Http; using Microsoft.Net.Http.Headers; -namespace Microsoft.AspNet.PipelineCore.Collections +namespace Microsoft.AspNet.Http.Core.Collections { public class FormFileCollection : List, IFormFileCollection { diff --git a/src/Microsoft.AspNet.PipelineCore/Collections/HeaderDictionary.cs b/src/Microsoft.AspNet.PipelineCore/Collections/HeaderDictionary.cs index bf362c94ed..72dc55ec36 100644 --- a/src/Microsoft.AspNet.PipelineCore/Collections/HeaderDictionary.cs +++ b/src/Microsoft.AspNet.PipelineCore/Collections/HeaderDictionary.cs @@ -7,9 +7,9 @@ using System.Collections.Generic; using System.Linq; using Microsoft.AspNet.Http.Infrastructure; using Microsoft.AspNet.Http; -using Microsoft.AspNet.PipelineCore.Infrastructure; +using Microsoft.AspNet.Http.Core.Infrastructure; -namespace Microsoft.AspNet.PipelineCore.Collections +namespace Microsoft.AspNet.Http.Core.Collections { /// /// Represents a wrapper for owin.RequestHeaders and owin.ResponseHeaders. diff --git a/src/Microsoft.AspNet.PipelineCore/Collections/ItemsDictionary.cs b/src/Microsoft.AspNet.PipelineCore/Collections/ItemsDictionary.cs index dc5216d117..06c27fbc1e 100644 --- a/src/Microsoft.AspNet.PipelineCore/Collections/ItemsDictionary.cs +++ b/src/Microsoft.AspNet.PipelineCore/Collections/ItemsDictionary.cs @@ -4,7 +4,7 @@ using System.Collections; using System.Collections.Generic; -namespace Microsoft.AspNet.PipelineCore +namespace Microsoft.AspNet.Http.Core { public class ItemsDictionary : IDictionary { diff --git a/src/Microsoft.AspNet.PipelineCore/Collections/ReadableStringCollection.cs b/src/Microsoft.AspNet.PipelineCore/Collections/ReadableStringCollection.cs index 1e12bb6dc1..91f3b8c8f3 100644 --- a/src/Microsoft.AspNet.PipelineCore/Collections/ReadableStringCollection.cs +++ b/src/Microsoft.AspNet.PipelineCore/Collections/ReadableStringCollection.cs @@ -6,7 +6,7 @@ using System.Collections; using System.Collections.Generic; using Microsoft.AspNet.Http; -namespace Microsoft.AspNet.PipelineCore.Collections +namespace Microsoft.AspNet.Http.Core.Collections { /// /// Accessors for query, forms, etc. diff --git a/src/Microsoft.AspNet.PipelineCore/Collections/RequestCookiesCollection.cs b/src/Microsoft.AspNet.PipelineCore/Collections/RequestCookiesCollection.cs index 5fd131d990..36a91735a3 100644 --- a/src/Microsoft.AspNet.PipelineCore/Collections/RequestCookiesCollection.cs +++ b/src/Microsoft.AspNet.PipelineCore/Collections/RequestCookiesCollection.cs @@ -5,9 +5,9 @@ using System; using System.Collections; using System.Collections.Generic; using Microsoft.AspNet.Http; -using Microsoft.AspNet.PipelineCore.Infrastructure; +using Microsoft.AspNet.Http.Core.Infrastructure; -namespace Microsoft.AspNet.PipelineCore.Collections +namespace Microsoft.AspNet.Http.Core.Collections { public class RequestCookiesCollection : IReadableStringCollection { diff --git a/src/Microsoft.AspNet.PipelineCore/Collections/ResponseCookies.cs b/src/Microsoft.AspNet.PipelineCore/Collections/ResponseCookies.cs index 2412b54a83..35aa02b17c 100644 --- a/src/Microsoft.AspNet.PipelineCore/Collections/ResponseCookies.cs +++ b/src/Microsoft.AspNet.PipelineCore/Collections/ResponseCookies.cs @@ -8,7 +8,7 @@ using System.Linq; using Microsoft.AspNet.Http; using Microsoft.AspNet.Http.Infrastructure; -namespace Microsoft.AspNet.PipelineCore.Collections +namespace Microsoft.AspNet.Http.Core.Collections { /// /// A wrapper for the response Set-Cookie header diff --git a/src/Microsoft.AspNet.PipelineCore/Collections/SessionCollection.cs b/src/Microsoft.AspNet.PipelineCore/Collections/SessionCollection.cs index 261b401cdb..8d2c3b7565 100644 --- a/src/Microsoft.AspNet.PipelineCore/Collections/SessionCollection.cs +++ b/src/Microsoft.AspNet.PipelineCore/Collections/SessionCollection.cs @@ -7,7 +7,7 @@ using System.Collections.Generic; using Microsoft.AspNet.Http; using Microsoft.AspNet.HttpFeature; -namespace Microsoft.AspNet.PipelineCore.Collections +namespace Microsoft.AspNet.Http.Core.Collections { public class SessionCollection : ISessionCollection { diff --git a/src/Microsoft.AspNet.PipelineCore/DefaultHttpContext.cs b/src/Microsoft.AspNet.PipelineCore/DefaultHttpContext.cs index 3b445fefbe..09915207ee 100644 --- a/src/Microsoft.AspNet.PipelineCore/DefaultHttpContext.cs +++ b/src/Microsoft.AspNet.PipelineCore/DefaultHttpContext.cs @@ -14,11 +14,11 @@ using Microsoft.AspNet.Http.Infrastructure; using Microsoft.AspNet.Http.Security; using Microsoft.AspNet.HttpFeature; using Microsoft.AspNet.HttpFeature.Security; -using Microsoft.AspNet.PipelineCore.Collections; -using Microsoft.AspNet.PipelineCore.Infrastructure; -using Microsoft.AspNet.PipelineCore.Security; +using Microsoft.AspNet.Http.Core.Collections; +using Microsoft.AspNet.Http.Core.Infrastructure; +using Microsoft.AspNet.Http.Core.Security; -namespace Microsoft.AspNet.PipelineCore +namespace Microsoft.AspNet.Http.Core { public class DefaultHttpContext : HttpContext { @@ -38,8 +38,8 @@ namespace Microsoft.AspNet.PipelineCore public DefaultHttpContext() : this(new FeatureCollection()) { - SetFeature(new DefaultHttpRequestFeature()); - SetFeature(new DefaultHttpResponseFeature()); + SetFeature(new HttpRequestFeature()); + SetFeature(new HttpResponseFeature()); } public DefaultHttpContext(IFeatureCollection features) diff --git a/src/Microsoft.AspNet.PipelineCore/DefaultHttpRequest.cs b/src/Microsoft.AspNet.PipelineCore/DefaultHttpRequest.cs index d95c37b9c3..28e8f2807c 100644 --- a/src/Microsoft.AspNet.PipelineCore/DefaultHttpRequest.cs +++ b/src/Microsoft.AspNet.PipelineCore/DefaultHttpRequest.cs @@ -9,10 +9,10 @@ using Microsoft.AspNet.FeatureModel; using Microsoft.AspNet.Http; using Microsoft.AspNet.Http.Infrastructure; using Microsoft.AspNet.HttpFeature; -using Microsoft.AspNet.PipelineCore.Collections; -using Microsoft.AspNet.PipelineCore.Infrastructure; +using Microsoft.AspNet.Http.Core.Collections; +using Microsoft.AspNet.Http.Core.Infrastructure; -namespace Microsoft.AspNet.PipelineCore +namespace Microsoft.AspNet.Http.Core { public class DefaultHttpRequest : HttpRequest { diff --git a/src/Microsoft.AspNet.PipelineCore/DefaultHttpResponse.cs b/src/Microsoft.AspNet.PipelineCore/DefaultHttpResponse.cs index 16d187450d..46358ad69f 100644 --- a/src/Microsoft.AspNet.PipelineCore/DefaultHttpResponse.cs +++ b/src/Microsoft.AspNet.PipelineCore/DefaultHttpResponse.cs @@ -14,11 +14,11 @@ using Microsoft.AspNet.Http.Security; using Microsoft.AspNet.FeatureModel; using Microsoft.AspNet.HttpFeature; using Microsoft.AspNet.HttpFeature.Security; -using Microsoft.AspNet.PipelineCore.Collections; -using Microsoft.AspNet.PipelineCore.Infrastructure; -using Microsoft.AspNet.PipelineCore.Security; +using Microsoft.AspNet.Http.Core.Collections; +using Microsoft.AspNet.Http.Core.Infrastructure; +using Microsoft.AspNet.Http.Core.Security; -namespace Microsoft.AspNet.PipelineCore +namespace Microsoft.AspNet.Http.Core { public class DefaultHttpResponse : HttpResponse { diff --git a/src/Microsoft.AspNet.PipelineCore/FormFeature.cs b/src/Microsoft.AspNet.PipelineCore/FormFeature.cs index 0180728be2..a0f1d5db07 100644 --- a/src/Microsoft.AspNet.PipelineCore/FormFeature.cs +++ b/src/Microsoft.AspNet.PipelineCore/FormFeature.cs @@ -9,11 +9,11 @@ using System.Text; using System.Threading; using System.Threading.Tasks; using Microsoft.AspNet.Http; -using Microsoft.AspNet.PipelineCore.Collections; +using Microsoft.AspNet.Http.Core.Collections; using Microsoft.AspNet.WebUtilities; using Microsoft.Net.Http.Headers; -namespace Microsoft.AspNet.PipelineCore +namespace Microsoft.AspNet.Http.Core { public class FormFeature : IFormFeature { diff --git a/src/Microsoft.AspNet.PipelineCore/FormFile.cs b/src/Microsoft.AspNet.PipelineCore/FormFile.cs index 19ea07466f..bbf0a52699 100644 --- a/src/Microsoft.AspNet.PipelineCore/FormFile.cs +++ b/src/Microsoft.AspNet.PipelineCore/FormFile.cs @@ -4,7 +4,7 @@ using System.IO; using Microsoft.AspNet.Http; -namespace Microsoft.AspNet.PipelineCore +namespace Microsoft.AspNet.Http.Core { public class FormFile : IFormFile { diff --git a/src/Microsoft.AspNet.PipelineCore/DefaultHttpRequestFeature.cs b/src/Microsoft.AspNet.PipelineCore/HttpRequestFeature.cs similarity index 87% rename from src/Microsoft.AspNet.PipelineCore/DefaultHttpRequestFeature.cs rename to src/Microsoft.AspNet.PipelineCore/HttpRequestFeature.cs index 156e362c9b..751b366ebe 100644 --- a/src/Microsoft.AspNet.PipelineCore/DefaultHttpRequestFeature.cs +++ b/src/Microsoft.AspNet.PipelineCore/HttpRequestFeature.cs @@ -6,11 +6,11 @@ using System.Collections.Generic; using System.IO; using Microsoft.AspNet.HttpFeature; -namespace Microsoft.AspNet.PipelineCore +namespace Microsoft.AspNet.Http.Core { - public class DefaultHttpRequestFeature : IHttpRequestFeature + public class HttpRequestFeature : IHttpRequestFeature { - public DefaultHttpRequestFeature() + public HttpRequestFeature() { Headers = new Dictionary(StringComparer.OrdinalIgnoreCase); Body = Stream.Null; diff --git a/src/Microsoft.AspNet.PipelineCore/DefaultHttpResponseFeature.cs b/src/Microsoft.AspNet.PipelineCore/HttpResponseFeature.cs similarity index 85% rename from src/Microsoft.AspNet.PipelineCore/DefaultHttpResponseFeature.cs rename to src/Microsoft.AspNet.PipelineCore/HttpResponseFeature.cs index c3b933bb8a..d4b5287885 100644 --- a/src/Microsoft.AspNet.PipelineCore/DefaultHttpResponseFeature.cs +++ b/src/Microsoft.AspNet.PipelineCore/HttpResponseFeature.cs @@ -6,11 +6,11 @@ using System.Collections.Generic; using System.IO; using Microsoft.AspNet.HttpFeature; -namespace Microsoft.AspNet.PipelineCore +namespace Microsoft.AspNet.Http.Core { - public class DefaultHttpResponseFeature : IHttpResponseFeature + public class HttpResponseFeature : IHttpResponseFeature { - public DefaultHttpResponseFeature() + public HttpResponseFeature() { StatusCode = 200; Headers = new Dictionary(StringComparer.OrdinalIgnoreCase); diff --git a/src/Microsoft.AspNet.PipelineCore/IFormFeature.cs b/src/Microsoft.AspNet.PipelineCore/IFormFeature.cs index cc84d32a4b..00f2645118 100644 --- a/src/Microsoft.AspNet.PipelineCore/IFormFeature.cs +++ b/src/Microsoft.AspNet.PipelineCore/IFormFeature.cs @@ -6,7 +6,7 @@ using System.Threading.Tasks; using Microsoft.AspNet.Http; using Microsoft.Framework.Runtime; -namespace Microsoft.AspNet.PipelineCore +namespace Microsoft.AspNet.Http.Core { public interface IFormFeature { diff --git a/src/Microsoft.AspNet.PipelineCore/IItemsFeature.cs b/src/Microsoft.AspNet.PipelineCore/IItemsFeature.cs index 9afe1623de..a38a9cb720 100644 --- a/src/Microsoft.AspNet.PipelineCore/IItemsFeature.cs +++ b/src/Microsoft.AspNet.PipelineCore/IItemsFeature.cs @@ -3,7 +3,7 @@ using System.Collections.Generic; -namespace Microsoft.AspNet.PipelineCore +namespace Microsoft.AspNet.Http.Core { public interface IItemsFeature { diff --git a/src/Microsoft.AspNet.PipelineCore/IQueryFeature.cs b/src/Microsoft.AspNet.PipelineCore/IQueryFeature.cs index e1ad3146c0..545200b3c5 100644 --- a/src/Microsoft.AspNet.PipelineCore/IQueryFeature.cs +++ b/src/Microsoft.AspNet.PipelineCore/IQueryFeature.cs @@ -3,7 +3,7 @@ using Microsoft.AspNet.Http; -namespace Microsoft.AspNet.PipelineCore +namespace Microsoft.AspNet.Http.Core { public interface IQueryFeature { diff --git a/src/Microsoft.AspNet.PipelineCore/IRequestCookiesFeature.cs b/src/Microsoft.AspNet.PipelineCore/IRequestCookiesFeature.cs index ba1a0e3616..d52e714c71 100644 --- a/src/Microsoft.AspNet.PipelineCore/IRequestCookiesFeature.cs +++ b/src/Microsoft.AspNet.PipelineCore/IRequestCookiesFeature.cs @@ -3,7 +3,7 @@ using Microsoft.AspNet.Http; -namespace Microsoft.AspNet.PipelineCore +namespace Microsoft.AspNet.Http.Core { public interface IRequestCookiesFeature { diff --git a/src/Microsoft.AspNet.PipelineCore/IResponseCookiesFeature.cs b/src/Microsoft.AspNet.PipelineCore/IResponseCookiesFeature.cs index 6d51b7a015..f33a15361b 100644 --- a/src/Microsoft.AspNet.PipelineCore/IResponseCookiesFeature.cs +++ b/src/Microsoft.AspNet.PipelineCore/IResponseCookiesFeature.cs @@ -2,9 +2,9 @@ // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using Microsoft.AspNet.Http; -using Microsoft.AspNet.PipelineCore.Collections; +using Microsoft.AspNet.Http.Core.Collections; -namespace Microsoft.AspNet.PipelineCore +namespace Microsoft.AspNet.Http.Core { public interface IResponseCookiesFeature { diff --git a/src/Microsoft.AspNet.PipelineCore/IServiceProvidersFeature.cs b/src/Microsoft.AspNet.PipelineCore/IServiceProvidersFeature.cs index 03dffc578d..3435e53972 100644 --- a/src/Microsoft.AspNet.PipelineCore/IServiceProvidersFeature.cs +++ b/src/Microsoft.AspNet.PipelineCore/IServiceProvidersFeature.cs @@ -3,7 +3,7 @@ using System; -namespace Microsoft.AspNet.PipelineCore +namespace Microsoft.AspNet.Http.Core { public interface IServiceProvidersFeature { diff --git a/src/Microsoft.AspNet.PipelineCore/Infrastructure/FeatureReference.cs b/src/Microsoft.AspNet.PipelineCore/Infrastructure/FeatureReference.cs index 0726b73b95..f6db6e4fef 100644 --- a/src/Microsoft.AspNet.PipelineCore/Infrastructure/FeatureReference.cs +++ b/src/Microsoft.AspNet.PipelineCore/Infrastructure/FeatureReference.cs @@ -3,7 +3,7 @@ using Microsoft.AspNet.FeatureModel; -namespace Microsoft.AspNet.PipelineCore.Infrastructure +namespace Microsoft.AspNet.Http.Core.Infrastructure { internal struct FeatureReference { diff --git a/src/Microsoft.AspNet.PipelineCore/Infrastructure/ParsingHelpers.cs b/src/Microsoft.AspNet.PipelineCore/Infrastructure/ParsingHelpers.cs index 48dab300ac..85208daa0d 100644 --- a/src/Microsoft.AspNet.PipelineCore/Infrastructure/ParsingHelpers.cs +++ b/src/Microsoft.AspNet.PipelineCore/Infrastructure/ParsingHelpers.cs @@ -9,7 +9,7 @@ using System.Linq; using Microsoft.AspNet.Http; using Microsoft.AspNet.Http.Infrastructure; -namespace Microsoft.AspNet.PipelineCore.Infrastructure +namespace Microsoft.AspNet.Http.Core.Infrastructure { internal struct HeaderSegment : IEquatable { diff --git a/src/Microsoft.AspNet.PipelineCore/ItemsFeature.cs b/src/Microsoft.AspNet.PipelineCore/ItemsFeature.cs index 54d7d123f4..50b32711a3 100644 --- a/src/Microsoft.AspNet.PipelineCore/ItemsFeature.cs +++ b/src/Microsoft.AspNet.PipelineCore/ItemsFeature.cs @@ -3,7 +3,7 @@ using System.Collections.Generic; -namespace Microsoft.AspNet.PipelineCore +namespace Microsoft.AspNet.Http.Core { public class ItemsFeature : IItemsFeature { diff --git a/src/Microsoft.AspNet.PipelineCore/NotNullAttribute.cs b/src/Microsoft.AspNet.PipelineCore/NotNullAttribute.cs index 33fc2e3070..2328b5d5d8 100644 --- a/src/Microsoft.AspNet.PipelineCore/NotNullAttribute.cs +++ b/src/Microsoft.AspNet.PipelineCore/NotNullAttribute.cs @@ -3,7 +3,7 @@ using System; -namespace Microsoft.AspNet.PipelineCore +namespace Microsoft.AspNet.Http.Core { [AttributeUsage(AttributeTargets.Parameter, AllowMultiple = false)] internal sealed class NotNullAttribute : Attribute diff --git a/src/Microsoft.AspNet.PipelineCore/QueryFeature.cs b/src/Microsoft.AspNet.PipelineCore/QueryFeature.cs index e57b406153..e1b05f6b84 100644 --- a/src/Microsoft.AspNet.PipelineCore/QueryFeature.cs +++ b/src/Microsoft.AspNet.PipelineCore/QueryFeature.cs @@ -5,11 +5,11 @@ using System.Collections.Generic; using Microsoft.AspNet.FeatureModel; using Microsoft.AspNet.Http; using Microsoft.AspNet.HttpFeature; -using Microsoft.AspNet.PipelineCore.Collections; -using Microsoft.AspNet.PipelineCore.Infrastructure; +using Microsoft.AspNet.Http.Core.Collections; +using Microsoft.AspNet.Http.Core.Infrastructure; using Microsoft.AspNet.WebUtilities; -namespace Microsoft.AspNet.PipelineCore +namespace Microsoft.AspNet.Http.Core { public class QueryFeature : IQueryFeature { diff --git a/src/Microsoft.AspNet.PipelineCore/ReferenceReadStream.cs b/src/Microsoft.AspNet.PipelineCore/ReferenceReadStream.cs index aaad97ae92..06755f543d 100644 --- a/src/Microsoft.AspNet.PipelineCore/ReferenceReadStream.cs +++ b/src/Microsoft.AspNet.PipelineCore/ReferenceReadStream.cs @@ -6,7 +6,7 @@ using System.IO; using System.Threading; using System.Threading.Tasks; -namespace Microsoft.AspNet.PipelineCore +namespace Microsoft.AspNet.Http.Core { /// /// A Stream that wraps another stream starting at a certain offset and reading for the given length. diff --git a/src/Microsoft.AspNet.PipelineCore/RequestCookiesFeature.cs b/src/Microsoft.AspNet.PipelineCore/RequestCookiesFeature.cs index 952bfed0f6..e57ff741ce 100644 --- a/src/Microsoft.AspNet.PipelineCore/RequestCookiesFeature.cs +++ b/src/Microsoft.AspNet.PipelineCore/RequestCookiesFeature.cs @@ -7,10 +7,10 @@ using Microsoft.AspNet.FeatureModel; using Microsoft.AspNet.Http; using Microsoft.AspNet.Http.Infrastructure; using Microsoft.AspNet.HttpFeature; -using Microsoft.AspNet.PipelineCore.Collections; -using Microsoft.AspNet.PipelineCore.Infrastructure; +using Microsoft.AspNet.Http.Core.Collections; +using Microsoft.AspNet.Http.Core.Infrastructure; -namespace Microsoft.AspNet.PipelineCore +namespace Microsoft.AspNet.Http.Core { public class RequestCookiesFeature : IRequestCookiesFeature { diff --git a/src/Microsoft.AspNet.PipelineCore/ResponseCookiesFeature.cs b/src/Microsoft.AspNet.PipelineCore/ResponseCookiesFeature.cs index f8ea2bf1b7..801d6d4b05 100644 --- a/src/Microsoft.AspNet.PipelineCore/ResponseCookiesFeature.cs +++ b/src/Microsoft.AspNet.PipelineCore/ResponseCookiesFeature.cs @@ -4,10 +4,10 @@ using Microsoft.AspNet.Http; using Microsoft.AspNet.FeatureModel; using Microsoft.AspNet.HttpFeature; -using Microsoft.AspNet.PipelineCore.Collections; -using Microsoft.AspNet.PipelineCore.Infrastructure; +using Microsoft.AspNet.Http.Core.Collections; +using Microsoft.AspNet.Http.Core.Infrastructure; -namespace Microsoft.AspNet.PipelineCore +namespace Microsoft.AspNet.Http.Core { public class ResponseCookiesFeature : IResponseCookiesFeature { diff --git a/src/Microsoft.AspNet.PipelineCore/Security/AuthTypeContext.cs b/src/Microsoft.AspNet.PipelineCore/Security/AuthTypeContext.cs index ffd779039e..d9e516e756 100644 --- a/src/Microsoft.AspNet.PipelineCore/Security/AuthTypeContext.cs +++ b/src/Microsoft.AspNet.PipelineCore/Security/AuthTypeContext.cs @@ -6,7 +6,7 @@ using System.Collections.Generic; using Microsoft.AspNet.Http.Security; using Microsoft.AspNet.HttpFeature.Security; -namespace Microsoft.AspNet.PipelineCore.Security +namespace Microsoft.AspNet.Http.Core.Security { public class AuthTypeContext : IAuthTypeContext { diff --git a/src/Microsoft.AspNet.PipelineCore/Security/AuthenticateContext.cs b/src/Microsoft.AspNet.PipelineCore/Security/AuthenticateContext.cs index 1e27a006c4..e04326b257 100644 --- a/src/Microsoft.AspNet.PipelineCore/Security/AuthenticateContext.cs +++ b/src/Microsoft.AspNet.PipelineCore/Security/AuthenticateContext.cs @@ -10,7 +10,7 @@ using System.Threading.Tasks; using Microsoft.AspNet.Http.Security; using Microsoft.AspNet.HttpFeature.Security; -namespace Microsoft.AspNet.PipelineCore.Security +namespace Microsoft.AspNet.Http.Core.Security { public class AuthenticateContext : IAuthenticateContext { diff --git a/src/Microsoft.AspNet.PipelineCore/Security/ChallengeContext.cs b/src/Microsoft.AspNet.PipelineCore/Security/ChallengeContext.cs index ea87d06599..5c8b8ef74b 100644 --- a/src/Microsoft.AspNet.PipelineCore/Security/ChallengeContext.cs +++ b/src/Microsoft.AspNet.PipelineCore/Security/ChallengeContext.cs @@ -8,7 +8,7 @@ using System.Text; using System.Threading.Tasks; using Microsoft.AspNet.HttpFeature.Security; -namespace Microsoft.AspNet.PipelineCore.Security +namespace Microsoft.AspNet.Http.Core.Security { public class ChallengeContext : IChallengeContext { diff --git a/src/Microsoft.AspNet.PipelineCore/Security/HttpAuthenticationFeature.cs b/src/Microsoft.AspNet.PipelineCore/Security/HttpAuthenticationFeature.cs index 368b888573..8e40287b22 100644 --- a/src/Microsoft.AspNet.PipelineCore/Security/HttpAuthenticationFeature.cs +++ b/src/Microsoft.AspNet.PipelineCore/Security/HttpAuthenticationFeature.cs @@ -4,7 +4,7 @@ using System.Security.Claims; using Microsoft.AspNet.HttpFeature.Security; -namespace Microsoft.AspNet.PipelineCore.Security +namespace Microsoft.AspNet.Http.Core.Security { public class HttpAuthenticationFeature : IHttpAuthenticationFeature { diff --git a/src/Microsoft.AspNet.PipelineCore/Security/SignInContext.cs b/src/Microsoft.AspNet.PipelineCore/Security/SignInContext.cs index 43a88a82de..9856e7ee1a 100644 --- a/src/Microsoft.AspNet.PipelineCore/Security/SignInContext.cs +++ b/src/Microsoft.AspNet.PipelineCore/Security/SignInContext.cs @@ -6,7 +6,7 @@ using System.Collections.Generic; using System.Security.Claims; using Microsoft.AspNet.HttpFeature.Security; -namespace Microsoft.AspNet.PipelineCore.Security +namespace Microsoft.AspNet.Http.Core.Security { public class SignInContext : ISignInContext { diff --git a/src/Microsoft.AspNet.PipelineCore/Security/SignOutContext.cs b/src/Microsoft.AspNet.PipelineCore/Security/SignOutContext.cs index 546c5bca83..4ebde735d2 100644 --- a/src/Microsoft.AspNet.PipelineCore/Security/SignOutContext.cs +++ b/src/Microsoft.AspNet.PipelineCore/Security/SignOutContext.cs @@ -5,7 +5,7 @@ using System; using System.Collections.Generic; using Microsoft.AspNet.HttpFeature.Security; -namespace Microsoft.AspNet.PipelineCore.Security +namespace Microsoft.AspNet.Http.Core.Security { public class SignOutContext : ISignOutContext { diff --git a/src/Microsoft.AspNet.PipelineCore/ServiceProvidersFeature.cs b/src/Microsoft.AspNet.PipelineCore/ServiceProvidersFeature.cs index 50bfab0065..9f218d9bbb 100644 --- a/src/Microsoft.AspNet.PipelineCore/ServiceProvidersFeature.cs +++ b/src/Microsoft.AspNet.PipelineCore/ServiceProvidersFeature.cs @@ -3,7 +3,7 @@ using System; -namespace Microsoft.AspNet.PipelineCore +namespace Microsoft.AspNet.Http.Core { public class ServiceProvidersFeature : IServiceProvidersFeature { diff --git a/src/Microsoft.AspNet.PipelineCore/WebSocketAcceptContext.cs b/src/Microsoft.AspNet.PipelineCore/WebSocketAcceptContext.cs index 6e9a7ba0dd..3c935b52f8 100644 --- a/src/Microsoft.AspNet.PipelineCore/WebSocketAcceptContext.cs +++ b/src/Microsoft.AspNet.PipelineCore/WebSocketAcceptContext.cs @@ -3,7 +3,7 @@ using Microsoft.AspNet.HttpFeature; -namespace Microsoft.AspNet.PipelineCore +namespace Microsoft.AspNet.Http.Core { public class WebSocketAcceptContext : IWebSocketAcceptContext { diff --git a/test/Microsoft.AspNet.Http.Extensions.Tests/HeaderDictionaryTypeExtensionsTest.cs b/test/Microsoft.AspNet.Http.Extensions.Tests/HeaderDictionaryTypeExtensionsTest.cs index 6b437d8925..a7e15c1844 100644 --- a/test/Microsoft.AspNet.Http.Extensions.Tests/HeaderDictionaryTypeExtensionsTest.cs +++ b/test/Microsoft.AspNet.Http.Extensions.Tests/HeaderDictionaryTypeExtensionsTest.cs @@ -4,7 +4,7 @@ using System; using System.Collections.Generic; using System.Linq; -using Microsoft.AspNet.PipelineCore.Collections; +using Microsoft.AspNet.Http.Core.Collections; using Microsoft.Net.Http.Headers; using Xunit; diff --git a/test/Microsoft.AspNet.Http.Extensions.Tests/HttpResponseSendingExtensionsTests.cs b/test/Microsoft.AspNet.Http.Extensions.Tests/HttpResponseSendingExtensionsTests.cs index 05d662a955..ce046ba3e7 100644 --- a/test/Microsoft.AspNet.Http.Extensions.Tests/HttpResponseSendingExtensionsTests.cs +++ b/test/Microsoft.AspNet.Http.Extensions.Tests/HttpResponseSendingExtensionsTests.cs @@ -4,7 +4,7 @@ using System.IO; using System.Text; using System.Threading.Tasks; -using Microsoft.AspNet.PipelineCore; +using Microsoft.AspNet.Http.Core; using Xunit; namespace Microsoft.AspNet.Http.Extensions diff --git a/test/Microsoft.AspNet.Http.Extensions.Tests/UseWithServicesTests.cs b/test/Microsoft.AspNet.Http.Extensions.Tests/UseWithServicesTests.cs index 1ed29d112e..d2393592ad 100644 --- a/test/Microsoft.AspNet.Http.Extensions.Tests/UseWithServicesTests.cs +++ b/test/Microsoft.AspNet.Http.Extensions.Tests/UseWithServicesTests.cs @@ -6,7 +6,7 @@ using Xunit; using Microsoft.AspNet.Builder; using Microsoft.Framework.DependencyInjection; using Microsoft.Framework.DependencyInjection.Fallback; -using Microsoft.AspNet.PipelineCore; +using Microsoft.AspNet.Http.Core; using System.Collections.Generic; using System.Threading.Tasks; diff --git a/test/Microsoft.AspNet.Http.Tests/HttpResponseWritingExtensionsTests.cs b/test/Microsoft.AspNet.Http.Tests/HttpResponseWritingExtensionsTests.cs index aa6582e753..c9692254f1 100644 --- a/test/Microsoft.AspNet.Http.Tests/HttpResponseWritingExtensionsTests.cs +++ b/test/Microsoft.AspNet.Http.Tests/HttpResponseWritingExtensionsTests.cs @@ -3,7 +3,7 @@ using System.IO; using System.Threading.Tasks; -using Microsoft.AspNet.PipelineCore; +using Microsoft.AspNet.Http.Core; using Xunit; namespace Microsoft.AspNet.Http diff --git a/test/Microsoft.AspNet.Http.Tests/MapPathMiddlewareTests.cs b/test/Microsoft.AspNet.Http.Tests/MapPathMiddlewareTests.cs index bdaa1eeedb..e49637ed56 100644 --- a/test/Microsoft.AspNet.Http.Tests/MapPathMiddlewareTests.cs +++ b/test/Microsoft.AspNet.Http.Tests/MapPathMiddlewareTests.cs @@ -5,7 +5,7 @@ using System; using System.Threading.Tasks; using Microsoft.AspNet.Http; using Microsoft.AspNet.HttpFeature; -using Microsoft.AspNet.PipelineCore; +using Microsoft.AspNet.Http.Core; using Shouldly; using Xunit; diff --git a/test/Microsoft.AspNet.Http.Tests/MapPredicateMiddlewareTests.cs b/test/Microsoft.AspNet.Http.Tests/MapPredicateMiddlewareTests.cs index c5016f3064..b0ef791a3e 100644 --- a/test/Microsoft.AspNet.Http.Tests/MapPredicateMiddlewareTests.cs +++ b/test/Microsoft.AspNet.Http.Tests/MapPredicateMiddlewareTests.cs @@ -6,7 +6,7 @@ using System.Collections.Generic; using System.Threading.Tasks; using Microsoft.AspNet.Http; using Microsoft.AspNet.HttpFeature; -using Microsoft.AspNet.PipelineCore; +using Microsoft.AspNet.Http.Core; using Xunit; namespace Microsoft.AspNet.Builder.Extensions diff --git a/test/Microsoft.AspNet.Owin.Tests/OwinEnvironmentTests.cs b/test/Microsoft.AspNet.Owin.Tests/OwinEnvironmentTests.cs index 88fd2f7174..b98e99cfc1 100644 --- a/test/Microsoft.AspNet.Owin.Tests/OwinEnvironmentTests.cs +++ b/test/Microsoft.AspNet.Owin.Tests/OwinEnvironmentTests.cs @@ -7,8 +7,7 @@ using System.Linq; using System.Security.Claims; using System.Threading; using Microsoft.AspNet.Http; -using Microsoft.AspNet.HttpFeature; -using Microsoft.AspNet.PipelineCore; +using Microsoft.AspNet.Http.Core; using Xunit; namespace Microsoft.AspNet.Owin diff --git a/test/Microsoft.AspNet.PipelineCore.Tests/DefaultHttpContextTests.cs b/test/Microsoft.AspNet.PipelineCore.Tests/DefaultHttpContextTests.cs index 723811a947..2b04823c53 100644 --- a/test/Microsoft.AspNet.PipelineCore.Tests/DefaultHttpContextTests.cs +++ b/test/Microsoft.AspNet.PipelineCore.Tests/DefaultHttpContextTests.cs @@ -12,7 +12,7 @@ using Microsoft.AspNet.FeatureModel; using Microsoft.AspNet.HttpFeature; using Xunit; -namespace Microsoft.AspNet.PipelineCore.Tests +namespace Microsoft.AspNet.Http.Core.Tests { public class DefaultHttpContextTests { diff --git a/test/Microsoft.AspNet.PipelineCore.Tests/DefaultHttpRequestTests.cs b/test/Microsoft.AspNet.PipelineCore.Tests/DefaultHttpRequestTests.cs index 397d480580..e2518fd10b 100644 --- a/test/Microsoft.AspNet.PipelineCore.Tests/DefaultHttpRequestTests.cs +++ b/test/Microsoft.AspNet.PipelineCore.Tests/DefaultHttpRequestTests.cs @@ -8,7 +8,7 @@ using Microsoft.AspNet.Http; using Microsoft.AspNet.HttpFeature; using Xunit; -namespace Microsoft.AspNet.PipelineCore.Tests +namespace Microsoft.AspNet.Http.Core.Tests { public class DefaultHttpRequestTests { diff --git a/test/Microsoft.AspNet.PipelineCore.Tests/FormFeatureTests.cs b/test/Microsoft.AspNet.PipelineCore.Tests/FormFeatureTests.cs index b129adc0e1..7f1224c1b7 100644 --- a/test/Microsoft.AspNet.PipelineCore.Tests/FormFeatureTests.cs +++ b/test/Microsoft.AspNet.PipelineCore.Tests/FormFeatureTests.cs @@ -9,7 +9,7 @@ using System.Threading.Tasks; using Microsoft.AspNet.WebUtilities; using Xunit; -namespace Microsoft.AspNet.PipelineCore +namespace Microsoft.AspNet.Http.Core { public class FormFeatureTests { diff --git a/test/Microsoft.AspNet.PipelineCore.Tests/HeaderDictionaryTests.cs b/test/Microsoft.AspNet.PipelineCore.Tests/HeaderDictionaryTests.cs index 74667bbeb0..4bee6e446d 100644 --- a/test/Microsoft.AspNet.PipelineCore.Tests/HeaderDictionaryTests.cs +++ b/test/Microsoft.AspNet.PipelineCore.Tests/HeaderDictionaryTests.cs @@ -3,10 +3,10 @@ using System; using System.Collections.Generic; -using Microsoft.AspNet.PipelineCore.Collections; +using Microsoft.AspNet.Http.Core.Collections; using Xunit; -namespace Microsoft.AspNet.PipelineCore.Tests +namespace Microsoft.AspNet.Http.Core.Tests { public class HeaderDictionaryTests { diff --git a/test/Microsoft.AspNet.PipelineCore.Tests/Properties/AssemblyInfo.cs b/test/Microsoft.AspNet.PipelineCore.Tests/Properties/AssemblyInfo.cs deleted file mode 100644 index 2a4dd7d85d..0000000000 --- a/test/Microsoft.AspNet.PipelineCore.Tests/Properties/AssemblyInfo.cs +++ /dev/null @@ -1,39 +0,0 @@ -// 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.Reflection; -using System.Runtime.CompilerServices; -using System.Runtime.InteropServices; - -// General Information about an assembly is controlled through the following -// set of attributes. Change these attribute values to modify the information -// associated with an assembly. -[assembly: AssemblyTitle("Microsoft.AspNet.PipelineCore.Tests")] -[assembly: AssemblyDescription("")] -[assembly: AssemblyConfiguration("")] -[assembly: AssemblyCompany("")] -[assembly: AssemblyProduct("Microsoft.AspNet.PipelineCore.Tests")] -[assembly: AssemblyCopyright("Copyright © 2013")] -[assembly: AssemblyTrademark("")] -[assembly: AssemblyCulture("")] - -// Setting ComVisible to false makes the types in this assembly not visible -// to COM components. If you need to access a type in this assembly from -// COM, set the ComVisible attribute to true on that type. -[assembly: ComVisible(false)] - -// The following GUID is for the ID of the typelib if this project is exposed to COM -[assembly: Guid("7c564547-b037-4054-a1ec-18e62717be47")] - -// Version information for an assembly consists of the following four values: -// -// Major Version -// Minor Version -// Build Number -// Revision -// -// You can specify all the values or you can default the Build and Revision Numbers -// by using the '*' as shown below: -// [assembly: AssemblyVersion("0.1.0")] -[assembly: AssemblyVersion("0.1.0")] -[assembly: AssemblyFileVersion("0.1.0")] diff --git a/test/Microsoft.AspNet.PipelineCore.Tests/QueryFeatureTests.cs b/test/Microsoft.AspNet.PipelineCore.Tests/QueryFeatureTests.cs index 975edda5b1..f76fdb25fe 100644 --- a/test/Microsoft.AspNet.PipelineCore.Tests/QueryFeatureTests.cs +++ b/test/Microsoft.AspNet.PipelineCore.Tests/QueryFeatureTests.cs @@ -6,7 +6,7 @@ using Microsoft.AspNet.HttpFeature; using Moq; using Xunit; -namespace Microsoft.AspNet.PipelineCore.Tests +namespace Microsoft.AspNet.Http.Core.Tests { public class QueryFeatureTests { From d43cf30eff2cbbcc33352c6f49bf1f7ffb13dfc0 Mon Sep 17 00:00:00 2001 From: Chris Ross Date: Thu, 15 Jan 2015 12:37:34 -0800 Subject: [PATCH 08/16] #162 - Rename PipelineCore project to Http.Core. Part 2. --- HttpAbstractions.sln | 6 +++--- .../ApplicationBuilder.cs | 0 .../BufferingHelper.cs | 0 .../Collections/FormCollection.cs | 0 .../Collections/FormFileCollection.cs | 0 .../Collections/HeaderDictionary.cs | 0 .../Collections/ItemsDictionary.cs | 0 .../Collections/ReadableStringCollection.cs | 0 .../Collections/RequestCookiesCollection.cs | 0 .../Collections/ResponseCookies.cs | 0 .../Collections/SessionCollection.cs | 0 .../DefaultHttpContext.cs | 0 .../DefaultHttpRequest.cs | 0 .../DefaultHttpResponse.cs | 0 .../FormFeature.cs | 0 .../FormFile.cs | 0 .../HttpRequestFeature.cs | 0 .../HttpResponseFeature.cs | 0 .../IFormFeature.cs | 0 .../IItemsFeature.cs | 0 .../IQueryFeature.cs | 0 .../IRequestCookiesFeature.cs | 0 .../IResponseCookiesFeature.cs | 0 .../IServiceProvidersFeature.cs | 0 .../Infrastructure/Constants.cs | 0 .../Infrastructure/FeatureReference.cs | 0 .../Infrastructure/ParsingHelpers.cs | 0 .../ItemsFeature.cs | 0 .../Microsoft.AspNet.Http.Core.kproj} | 0 .../NotNullAttribute.cs | 0 .../QueryFeature.cs | 0 .../ReferenceReadStream.cs | 0 .../RequestCookiesFeature.cs | 0 .../ResponseCookiesFeature.cs | 0 .../Security/AuthTypeContext.cs | 0 .../Security/AuthenticateContext.cs | 0 .../Security/ChallengeContext.cs | 0 .../Security/HttpAuthenticationFeature.cs | 0 .../Security/SignInContext.cs | 0 .../Security/SignOutContext.cs | 0 .../ServiceProvidersFeature.cs | 0 .../WebSocketAcceptContext.cs | 0 .../project.json | 0 src/Microsoft.AspNet.Owin/project.json | 2 +- .../ApplicationBuilderTests.cs | 0 .../DefaultHttpContextTests.cs | 0 .../DefaultHttpRequestTests.cs | 0 .../FormFeatureTests.cs | 0 .../HeaderDictionaryTests.cs | 0 .../Microsoft.AspNet.Http.Core.Tests.kproj} | 0 .../QueryFeatureTests.cs | 0 .../project.json | 2 +- test/Microsoft.AspNet.Http.Extensions.Tests/project.json | 2 +- test/Microsoft.AspNet.Http.Tests/project.json | 2 +- test/Microsoft.AspNet.Owin.Tests/project.json | 2 +- test/Microsoft.AspNet.WebUtilities.Tests/project.json | 2 +- 56 files changed, 9 insertions(+), 9 deletions(-) rename src/{Microsoft.AspNet.PipelineCore => Microsoft.AspNet.Http.Core}/ApplicationBuilder.cs (100%) rename src/{Microsoft.AspNet.PipelineCore => Microsoft.AspNet.Http.Core}/BufferingHelper.cs (100%) rename src/{Microsoft.AspNet.PipelineCore => Microsoft.AspNet.Http.Core}/Collections/FormCollection.cs (100%) rename src/{Microsoft.AspNet.PipelineCore => Microsoft.AspNet.Http.Core}/Collections/FormFileCollection.cs (100%) rename src/{Microsoft.AspNet.PipelineCore => Microsoft.AspNet.Http.Core}/Collections/HeaderDictionary.cs (100%) rename src/{Microsoft.AspNet.PipelineCore => Microsoft.AspNet.Http.Core}/Collections/ItemsDictionary.cs (100%) rename src/{Microsoft.AspNet.PipelineCore => Microsoft.AspNet.Http.Core}/Collections/ReadableStringCollection.cs (100%) rename src/{Microsoft.AspNet.PipelineCore => Microsoft.AspNet.Http.Core}/Collections/RequestCookiesCollection.cs (100%) rename src/{Microsoft.AspNet.PipelineCore => Microsoft.AspNet.Http.Core}/Collections/ResponseCookies.cs (100%) rename src/{Microsoft.AspNet.PipelineCore => Microsoft.AspNet.Http.Core}/Collections/SessionCollection.cs (100%) rename src/{Microsoft.AspNet.PipelineCore => Microsoft.AspNet.Http.Core}/DefaultHttpContext.cs (100%) rename src/{Microsoft.AspNet.PipelineCore => Microsoft.AspNet.Http.Core}/DefaultHttpRequest.cs (100%) rename src/{Microsoft.AspNet.PipelineCore => Microsoft.AspNet.Http.Core}/DefaultHttpResponse.cs (100%) rename src/{Microsoft.AspNet.PipelineCore => Microsoft.AspNet.Http.Core}/FormFeature.cs (100%) rename src/{Microsoft.AspNet.PipelineCore => Microsoft.AspNet.Http.Core}/FormFile.cs (100%) rename src/{Microsoft.AspNet.PipelineCore => Microsoft.AspNet.Http.Core}/HttpRequestFeature.cs (100%) rename src/{Microsoft.AspNet.PipelineCore => Microsoft.AspNet.Http.Core}/HttpResponseFeature.cs (100%) rename src/{Microsoft.AspNet.PipelineCore => Microsoft.AspNet.Http.Core}/IFormFeature.cs (100%) rename src/{Microsoft.AspNet.PipelineCore => Microsoft.AspNet.Http.Core}/IItemsFeature.cs (100%) rename src/{Microsoft.AspNet.PipelineCore => Microsoft.AspNet.Http.Core}/IQueryFeature.cs (100%) rename src/{Microsoft.AspNet.PipelineCore => Microsoft.AspNet.Http.Core}/IRequestCookiesFeature.cs (100%) rename src/{Microsoft.AspNet.PipelineCore => Microsoft.AspNet.Http.Core}/IResponseCookiesFeature.cs (100%) rename src/{Microsoft.AspNet.PipelineCore => Microsoft.AspNet.Http.Core}/IServiceProvidersFeature.cs (100%) rename src/{Microsoft.AspNet.PipelineCore => Microsoft.AspNet.Http.Core}/Infrastructure/Constants.cs (100%) rename src/{Microsoft.AspNet.PipelineCore => Microsoft.AspNet.Http.Core}/Infrastructure/FeatureReference.cs (100%) rename src/{Microsoft.AspNet.PipelineCore => Microsoft.AspNet.Http.Core}/Infrastructure/ParsingHelpers.cs (100%) rename src/{Microsoft.AspNet.PipelineCore => Microsoft.AspNet.Http.Core}/ItemsFeature.cs (100%) rename src/{Microsoft.AspNet.PipelineCore/Microsoft.AspNet.PipelineCore.kproj => Microsoft.AspNet.Http.Core/Microsoft.AspNet.Http.Core.kproj} (100%) rename src/{Microsoft.AspNet.PipelineCore => Microsoft.AspNet.Http.Core}/NotNullAttribute.cs (100%) rename src/{Microsoft.AspNet.PipelineCore => Microsoft.AspNet.Http.Core}/QueryFeature.cs (100%) rename src/{Microsoft.AspNet.PipelineCore => Microsoft.AspNet.Http.Core}/ReferenceReadStream.cs (100%) rename src/{Microsoft.AspNet.PipelineCore => Microsoft.AspNet.Http.Core}/RequestCookiesFeature.cs (100%) rename src/{Microsoft.AspNet.PipelineCore => Microsoft.AspNet.Http.Core}/ResponseCookiesFeature.cs (100%) rename src/{Microsoft.AspNet.PipelineCore => Microsoft.AspNet.Http.Core}/Security/AuthTypeContext.cs (100%) rename src/{Microsoft.AspNet.PipelineCore => Microsoft.AspNet.Http.Core}/Security/AuthenticateContext.cs (100%) rename src/{Microsoft.AspNet.PipelineCore => Microsoft.AspNet.Http.Core}/Security/ChallengeContext.cs (100%) rename src/{Microsoft.AspNet.PipelineCore => Microsoft.AspNet.Http.Core}/Security/HttpAuthenticationFeature.cs (100%) rename src/{Microsoft.AspNet.PipelineCore => Microsoft.AspNet.Http.Core}/Security/SignInContext.cs (100%) rename src/{Microsoft.AspNet.PipelineCore => Microsoft.AspNet.Http.Core}/Security/SignOutContext.cs (100%) rename src/{Microsoft.AspNet.PipelineCore => Microsoft.AspNet.Http.Core}/ServiceProvidersFeature.cs (100%) rename src/{Microsoft.AspNet.PipelineCore => Microsoft.AspNet.Http.Core}/WebSocketAcceptContext.cs (100%) rename src/{Microsoft.AspNet.PipelineCore => Microsoft.AspNet.Http.Core}/project.json (100%) rename test/{Microsoft.AspNet.PipelineCore.Tests => Microsoft.AspNet.Http.Core.Tests}/ApplicationBuilderTests.cs (100%) rename test/{Microsoft.AspNet.PipelineCore.Tests => Microsoft.AspNet.Http.Core.Tests}/DefaultHttpContextTests.cs (100%) rename test/{Microsoft.AspNet.PipelineCore.Tests => Microsoft.AspNet.Http.Core.Tests}/DefaultHttpRequestTests.cs (100%) rename test/{Microsoft.AspNet.PipelineCore.Tests => Microsoft.AspNet.Http.Core.Tests}/FormFeatureTests.cs (100%) rename test/{Microsoft.AspNet.PipelineCore.Tests => Microsoft.AspNet.Http.Core.Tests}/HeaderDictionaryTests.cs (100%) rename test/{Microsoft.AspNet.PipelineCore.Tests/Microsoft.AspNet.PipelineCore.Tests.kproj => Microsoft.AspNet.Http.Core.Tests/Microsoft.AspNet.Http.Core.Tests.kproj} (100%) rename test/{Microsoft.AspNet.PipelineCore.Tests => Microsoft.AspNet.Http.Core.Tests}/QueryFeatureTests.cs (100%) rename test/{Microsoft.AspNet.PipelineCore.Tests => Microsoft.AspNet.Http.Core.Tests}/project.json (90%) diff --git a/HttpAbstractions.sln b/HttpAbstractions.sln index 8ae65bd225..e4bd71292f 100644 --- a/HttpAbstractions.sln +++ b/HttpAbstractions.sln @@ -1,13 +1,13 @@  Microsoft Visual Studio Solution File, Format Version 12.00 # Visual Studio 14 -VisualStudioVersion = 14.0.22410.0 +VisualStudioVersion = 14.0.22513.0 MinimumVisualStudioVersion = 10.0.40219.1 Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{A5A15F1C-885A-452A-A731-B0173DDBD913}" EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "test", "test", "{F31FF137-390C-49BF-A3BD-7C6ED3597C21}" EndProject -Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "Microsoft.AspNet.PipelineCore", "src\Microsoft.AspNet.PipelineCore\Microsoft.AspNet.PipelineCore.kproj", "{BCF0F967-8753-4438-BD07-AADCA9CE509A}" +Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "Microsoft.AspNet.Http.Core", "src\Microsoft.AspNet.Http.Core\Microsoft.AspNet.Http.Core.kproj", "{BCF0F967-8753-4438-BD07-AADCA9CE509A}" EndProject Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "Microsoft.AspNet.Http", "src\Microsoft.AspNet.Http\Microsoft.AspNet.Http.kproj", "{22071333-15BA-4D16-A1D5-4D5B1A83FBDD}" EndProject @@ -15,7 +15,7 @@ Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "Microsoft.AspNet.HttpFeatur EndProject Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "Microsoft.AspNet.FeatureModel", "src\Microsoft.AspNet.FeatureModel\Microsoft.AspNet.FeatureModel.kproj", "{32A4C918-30EE-41DB-8E26-8A3BB88ED231}" EndProject -Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "Microsoft.AspNet.PipelineCore.Tests", "test\Microsoft.AspNet.PipelineCore.Tests\Microsoft.AspNet.PipelineCore.Tests.kproj", "{AA99AF26-F7B1-4A6B-A922-5C25539F6391}" +Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "Microsoft.AspNet.Http.Core.Tests", "test\Microsoft.AspNet.Http.Core.Tests\Microsoft.AspNet.Http.Core.Tests.kproj", "{AA99AF26-F7B1-4A6B-A922-5C25539F6391}" EndProject Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "Microsoft.AspNet.FeatureModel.Tests", "test\Microsoft.AspNet.FeatureModel.Tests\Microsoft.AspNet.FeatureModel.Tests.kproj", "{C5D2BAE1-E182-48A0-AA74-1AF14B782BF7}" EndProject diff --git a/src/Microsoft.AspNet.PipelineCore/ApplicationBuilder.cs b/src/Microsoft.AspNet.Http.Core/ApplicationBuilder.cs similarity index 100% rename from src/Microsoft.AspNet.PipelineCore/ApplicationBuilder.cs rename to src/Microsoft.AspNet.Http.Core/ApplicationBuilder.cs diff --git a/src/Microsoft.AspNet.PipelineCore/BufferingHelper.cs b/src/Microsoft.AspNet.Http.Core/BufferingHelper.cs similarity index 100% rename from src/Microsoft.AspNet.PipelineCore/BufferingHelper.cs rename to src/Microsoft.AspNet.Http.Core/BufferingHelper.cs diff --git a/src/Microsoft.AspNet.PipelineCore/Collections/FormCollection.cs b/src/Microsoft.AspNet.Http.Core/Collections/FormCollection.cs similarity index 100% rename from src/Microsoft.AspNet.PipelineCore/Collections/FormCollection.cs rename to src/Microsoft.AspNet.Http.Core/Collections/FormCollection.cs diff --git a/src/Microsoft.AspNet.PipelineCore/Collections/FormFileCollection.cs b/src/Microsoft.AspNet.Http.Core/Collections/FormFileCollection.cs similarity index 100% rename from src/Microsoft.AspNet.PipelineCore/Collections/FormFileCollection.cs rename to src/Microsoft.AspNet.Http.Core/Collections/FormFileCollection.cs diff --git a/src/Microsoft.AspNet.PipelineCore/Collections/HeaderDictionary.cs b/src/Microsoft.AspNet.Http.Core/Collections/HeaderDictionary.cs similarity index 100% rename from src/Microsoft.AspNet.PipelineCore/Collections/HeaderDictionary.cs rename to src/Microsoft.AspNet.Http.Core/Collections/HeaderDictionary.cs diff --git a/src/Microsoft.AspNet.PipelineCore/Collections/ItemsDictionary.cs b/src/Microsoft.AspNet.Http.Core/Collections/ItemsDictionary.cs similarity index 100% rename from src/Microsoft.AspNet.PipelineCore/Collections/ItemsDictionary.cs rename to src/Microsoft.AspNet.Http.Core/Collections/ItemsDictionary.cs diff --git a/src/Microsoft.AspNet.PipelineCore/Collections/ReadableStringCollection.cs b/src/Microsoft.AspNet.Http.Core/Collections/ReadableStringCollection.cs similarity index 100% rename from src/Microsoft.AspNet.PipelineCore/Collections/ReadableStringCollection.cs rename to src/Microsoft.AspNet.Http.Core/Collections/ReadableStringCollection.cs diff --git a/src/Microsoft.AspNet.PipelineCore/Collections/RequestCookiesCollection.cs b/src/Microsoft.AspNet.Http.Core/Collections/RequestCookiesCollection.cs similarity index 100% rename from src/Microsoft.AspNet.PipelineCore/Collections/RequestCookiesCollection.cs rename to src/Microsoft.AspNet.Http.Core/Collections/RequestCookiesCollection.cs diff --git a/src/Microsoft.AspNet.PipelineCore/Collections/ResponseCookies.cs b/src/Microsoft.AspNet.Http.Core/Collections/ResponseCookies.cs similarity index 100% rename from src/Microsoft.AspNet.PipelineCore/Collections/ResponseCookies.cs rename to src/Microsoft.AspNet.Http.Core/Collections/ResponseCookies.cs diff --git a/src/Microsoft.AspNet.PipelineCore/Collections/SessionCollection.cs b/src/Microsoft.AspNet.Http.Core/Collections/SessionCollection.cs similarity index 100% rename from src/Microsoft.AspNet.PipelineCore/Collections/SessionCollection.cs rename to src/Microsoft.AspNet.Http.Core/Collections/SessionCollection.cs diff --git a/src/Microsoft.AspNet.PipelineCore/DefaultHttpContext.cs b/src/Microsoft.AspNet.Http.Core/DefaultHttpContext.cs similarity index 100% rename from src/Microsoft.AspNet.PipelineCore/DefaultHttpContext.cs rename to src/Microsoft.AspNet.Http.Core/DefaultHttpContext.cs diff --git a/src/Microsoft.AspNet.PipelineCore/DefaultHttpRequest.cs b/src/Microsoft.AspNet.Http.Core/DefaultHttpRequest.cs similarity index 100% rename from src/Microsoft.AspNet.PipelineCore/DefaultHttpRequest.cs rename to src/Microsoft.AspNet.Http.Core/DefaultHttpRequest.cs diff --git a/src/Microsoft.AspNet.PipelineCore/DefaultHttpResponse.cs b/src/Microsoft.AspNet.Http.Core/DefaultHttpResponse.cs similarity index 100% rename from src/Microsoft.AspNet.PipelineCore/DefaultHttpResponse.cs rename to src/Microsoft.AspNet.Http.Core/DefaultHttpResponse.cs diff --git a/src/Microsoft.AspNet.PipelineCore/FormFeature.cs b/src/Microsoft.AspNet.Http.Core/FormFeature.cs similarity index 100% rename from src/Microsoft.AspNet.PipelineCore/FormFeature.cs rename to src/Microsoft.AspNet.Http.Core/FormFeature.cs diff --git a/src/Microsoft.AspNet.PipelineCore/FormFile.cs b/src/Microsoft.AspNet.Http.Core/FormFile.cs similarity index 100% rename from src/Microsoft.AspNet.PipelineCore/FormFile.cs rename to src/Microsoft.AspNet.Http.Core/FormFile.cs diff --git a/src/Microsoft.AspNet.PipelineCore/HttpRequestFeature.cs b/src/Microsoft.AspNet.Http.Core/HttpRequestFeature.cs similarity index 100% rename from src/Microsoft.AspNet.PipelineCore/HttpRequestFeature.cs rename to src/Microsoft.AspNet.Http.Core/HttpRequestFeature.cs diff --git a/src/Microsoft.AspNet.PipelineCore/HttpResponseFeature.cs b/src/Microsoft.AspNet.Http.Core/HttpResponseFeature.cs similarity index 100% rename from src/Microsoft.AspNet.PipelineCore/HttpResponseFeature.cs rename to src/Microsoft.AspNet.Http.Core/HttpResponseFeature.cs diff --git a/src/Microsoft.AspNet.PipelineCore/IFormFeature.cs b/src/Microsoft.AspNet.Http.Core/IFormFeature.cs similarity index 100% rename from src/Microsoft.AspNet.PipelineCore/IFormFeature.cs rename to src/Microsoft.AspNet.Http.Core/IFormFeature.cs diff --git a/src/Microsoft.AspNet.PipelineCore/IItemsFeature.cs b/src/Microsoft.AspNet.Http.Core/IItemsFeature.cs similarity index 100% rename from src/Microsoft.AspNet.PipelineCore/IItemsFeature.cs rename to src/Microsoft.AspNet.Http.Core/IItemsFeature.cs diff --git a/src/Microsoft.AspNet.PipelineCore/IQueryFeature.cs b/src/Microsoft.AspNet.Http.Core/IQueryFeature.cs similarity index 100% rename from src/Microsoft.AspNet.PipelineCore/IQueryFeature.cs rename to src/Microsoft.AspNet.Http.Core/IQueryFeature.cs diff --git a/src/Microsoft.AspNet.PipelineCore/IRequestCookiesFeature.cs b/src/Microsoft.AspNet.Http.Core/IRequestCookiesFeature.cs similarity index 100% rename from src/Microsoft.AspNet.PipelineCore/IRequestCookiesFeature.cs rename to src/Microsoft.AspNet.Http.Core/IRequestCookiesFeature.cs diff --git a/src/Microsoft.AspNet.PipelineCore/IResponseCookiesFeature.cs b/src/Microsoft.AspNet.Http.Core/IResponseCookiesFeature.cs similarity index 100% rename from src/Microsoft.AspNet.PipelineCore/IResponseCookiesFeature.cs rename to src/Microsoft.AspNet.Http.Core/IResponseCookiesFeature.cs diff --git a/src/Microsoft.AspNet.PipelineCore/IServiceProvidersFeature.cs b/src/Microsoft.AspNet.Http.Core/IServiceProvidersFeature.cs similarity index 100% rename from src/Microsoft.AspNet.PipelineCore/IServiceProvidersFeature.cs rename to src/Microsoft.AspNet.Http.Core/IServiceProvidersFeature.cs diff --git a/src/Microsoft.AspNet.PipelineCore/Infrastructure/Constants.cs b/src/Microsoft.AspNet.Http.Core/Infrastructure/Constants.cs similarity index 100% rename from src/Microsoft.AspNet.PipelineCore/Infrastructure/Constants.cs rename to src/Microsoft.AspNet.Http.Core/Infrastructure/Constants.cs diff --git a/src/Microsoft.AspNet.PipelineCore/Infrastructure/FeatureReference.cs b/src/Microsoft.AspNet.Http.Core/Infrastructure/FeatureReference.cs similarity index 100% rename from src/Microsoft.AspNet.PipelineCore/Infrastructure/FeatureReference.cs rename to src/Microsoft.AspNet.Http.Core/Infrastructure/FeatureReference.cs diff --git a/src/Microsoft.AspNet.PipelineCore/Infrastructure/ParsingHelpers.cs b/src/Microsoft.AspNet.Http.Core/Infrastructure/ParsingHelpers.cs similarity index 100% rename from src/Microsoft.AspNet.PipelineCore/Infrastructure/ParsingHelpers.cs rename to src/Microsoft.AspNet.Http.Core/Infrastructure/ParsingHelpers.cs diff --git a/src/Microsoft.AspNet.PipelineCore/ItemsFeature.cs b/src/Microsoft.AspNet.Http.Core/ItemsFeature.cs similarity index 100% rename from src/Microsoft.AspNet.PipelineCore/ItemsFeature.cs rename to src/Microsoft.AspNet.Http.Core/ItemsFeature.cs diff --git a/src/Microsoft.AspNet.PipelineCore/Microsoft.AspNet.PipelineCore.kproj b/src/Microsoft.AspNet.Http.Core/Microsoft.AspNet.Http.Core.kproj similarity index 100% rename from src/Microsoft.AspNet.PipelineCore/Microsoft.AspNet.PipelineCore.kproj rename to src/Microsoft.AspNet.Http.Core/Microsoft.AspNet.Http.Core.kproj diff --git a/src/Microsoft.AspNet.PipelineCore/NotNullAttribute.cs b/src/Microsoft.AspNet.Http.Core/NotNullAttribute.cs similarity index 100% rename from src/Microsoft.AspNet.PipelineCore/NotNullAttribute.cs rename to src/Microsoft.AspNet.Http.Core/NotNullAttribute.cs diff --git a/src/Microsoft.AspNet.PipelineCore/QueryFeature.cs b/src/Microsoft.AspNet.Http.Core/QueryFeature.cs similarity index 100% rename from src/Microsoft.AspNet.PipelineCore/QueryFeature.cs rename to src/Microsoft.AspNet.Http.Core/QueryFeature.cs diff --git a/src/Microsoft.AspNet.PipelineCore/ReferenceReadStream.cs b/src/Microsoft.AspNet.Http.Core/ReferenceReadStream.cs similarity index 100% rename from src/Microsoft.AspNet.PipelineCore/ReferenceReadStream.cs rename to src/Microsoft.AspNet.Http.Core/ReferenceReadStream.cs diff --git a/src/Microsoft.AspNet.PipelineCore/RequestCookiesFeature.cs b/src/Microsoft.AspNet.Http.Core/RequestCookiesFeature.cs similarity index 100% rename from src/Microsoft.AspNet.PipelineCore/RequestCookiesFeature.cs rename to src/Microsoft.AspNet.Http.Core/RequestCookiesFeature.cs diff --git a/src/Microsoft.AspNet.PipelineCore/ResponseCookiesFeature.cs b/src/Microsoft.AspNet.Http.Core/ResponseCookiesFeature.cs similarity index 100% rename from src/Microsoft.AspNet.PipelineCore/ResponseCookiesFeature.cs rename to src/Microsoft.AspNet.Http.Core/ResponseCookiesFeature.cs diff --git a/src/Microsoft.AspNet.PipelineCore/Security/AuthTypeContext.cs b/src/Microsoft.AspNet.Http.Core/Security/AuthTypeContext.cs similarity index 100% rename from src/Microsoft.AspNet.PipelineCore/Security/AuthTypeContext.cs rename to src/Microsoft.AspNet.Http.Core/Security/AuthTypeContext.cs diff --git a/src/Microsoft.AspNet.PipelineCore/Security/AuthenticateContext.cs b/src/Microsoft.AspNet.Http.Core/Security/AuthenticateContext.cs similarity index 100% rename from src/Microsoft.AspNet.PipelineCore/Security/AuthenticateContext.cs rename to src/Microsoft.AspNet.Http.Core/Security/AuthenticateContext.cs diff --git a/src/Microsoft.AspNet.PipelineCore/Security/ChallengeContext.cs b/src/Microsoft.AspNet.Http.Core/Security/ChallengeContext.cs similarity index 100% rename from src/Microsoft.AspNet.PipelineCore/Security/ChallengeContext.cs rename to src/Microsoft.AspNet.Http.Core/Security/ChallengeContext.cs diff --git a/src/Microsoft.AspNet.PipelineCore/Security/HttpAuthenticationFeature.cs b/src/Microsoft.AspNet.Http.Core/Security/HttpAuthenticationFeature.cs similarity index 100% rename from src/Microsoft.AspNet.PipelineCore/Security/HttpAuthenticationFeature.cs rename to src/Microsoft.AspNet.Http.Core/Security/HttpAuthenticationFeature.cs diff --git a/src/Microsoft.AspNet.PipelineCore/Security/SignInContext.cs b/src/Microsoft.AspNet.Http.Core/Security/SignInContext.cs similarity index 100% rename from src/Microsoft.AspNet.PipelineCore/Security/SignInContext.cs rename to src/Microsoft.AspNet.Http.Core/Security/SignInContext.cs diff --git a/src/Microsoft.AspNet.PipelineCore/Security/SignOutContext.cs b/src/Microsoft.AspNet.Http.Core/Security/SignOutContext.cs similarity index 100% rename from src/Microsoft.AspNet.PipelineCore/Security/SignOutContext.cs rename to src/Microsoft.AspNet.Http.Core/Security/SignOutContext.cs diff --git a/src/Microsoft.AspNet.PipelineCore/ServiceProvidersFeature.cs b/src/Microsoft.AspNet.Http.Core/ServiceProvidersFeature.cs similarity index 100% rename from src/Microsoft.AspNet.PipelineCore/ServiceProvidersFeature.cs rename to src/Microsoft.AspNet.Http.Core/ServiceProvidersFeature.cs diff --git a/src/Microsoft.AspNet.PipelineCore/WebSocketAcceptContext.cs b/src/Microsoft.AspNet.Http.Core/WebSocketAcceptContext.cs similarity index 100% rename from src/Microsoft.AspNet.PipelineCore/WebSocketAcceptContext.cs rename to src/Microsoft.AspNet.Http.Core/WebSocketAcceptContext.cs diff --git a/src/Microsoft.AspNet.PipelineCore/project.json b/src/Microsoft.AspNet.Http.Core/project.json similarity index 100% rename from src/Microsoft.AspNet.PipelineCore/project.json rename to src/Microsoft.AspNet.Http.Core/project.json diff --git a/src/Microsoft.AspNet.Owin/project.json b/src/Microsoft.AspNet.Owin/project.json index d8dac9991b..ce8c5b3058 100644 --- a/src/Microsoft.AspNet.Owin/project.json +++ b/src/Microsoft.AspNet.Owin/project.json @@ -4,7 +4,7 @@ "dependencies": { "Microsoft.AspNet.Http": "1.0.0-*", "Microsoft.AspNet.FeatureModel": "1.0.0-*", - "Microsoft.AspNet.PipelineCore": "1.0.0-*", + "Microsoft.AspNet.Http.Core": "1.0.0-*", "Microsoft.AspNet.HttpFeature": { "version": "1.0.0-*", "type": "build" } }, "frameworks": { diff --git a/test/Microsoft.AspNet.PipelineCore.Tests/ApplicationBuilderTests.cs b/test/Microsoft.AspNet.Http.Core.Tests/ApplicationBuilderTests.cs similarity index 100% rename from test/Microsoft.AspNet.PipelineCore.Tests/ApplicationBuilderTests.cs rename to test/Microsoft.AspNet.Http.Core.Tests/ApplicationBuilderTests.cs diff --git a/test/Microsoft.AspNet.PipelineCore.Tests/DefaultHttpContextTests.cs b/test/Microsoft.AspNet.Http.Core.Tests/DefaultHttpContextTests.cs similarity index 100% rename from test/Microsoft.AspNet.PipelineCore.Tests/DefaultHttpContextTests.cs rename to test/Microsoft.AspNet.Http.Core.Tests/DefaultHttpContextTests.cs diff --git a/test/Microsoft.AspNet.PipelineCore.Tests/DefaultHttpRequestTests.cs b/test/Microsoft.AspNet.Http.Core.Tests/DefaultHttpRequestTests.cs similarity index 100% rename from test/Microsoft.AspNet.PipelineCore.Tests/DefaultHttpRequestTests.cs rename to test/Microsoft.AspNet.Http.Core.Tests/DefaultHttpRequestTests.cs diff --git a/test/Microsoft.AspNet.PipelineCore.Tests/FormFeatureTests.cs b/test/Microsoft.AspNet.Http.Core.Tests/FormFeatureTests.cs similarity index 100% rename from test/Microsoft.AspNet.PipelineCore.Tests/FormFeatureTests.cs rename to test/Microsoft.AspNet.Http.Core.Tests/FormFeatureTests.cs diff --git a/test/Microsoft.AspNet.PipelineCore.Tests/HeaderDictionaryTests.cs b/test/Microsoft.AspNet.Http.Core.Tests/HeaderDictionaryTests.cs similarity index 100% rename from test/Microsoft.AspNet.PipelineCore.Tests/HeaderDictionaryTests.cs rename to test/Microsoft.AspNet.Http.Core.Tests/HeaderDictionaryTests.cs diff --git a/test/Microsoft.AspNet.PipelineCore.Tests/Microsoft.AspNet.PipelineCore.Tests.kproj b/test/Microsoft.AspNet.Http.Core.Tests/Microsoft.AspNet.Http.Core.Tests.kproj similarity index 100% rename from test/Microsoft.AspNet.PipelineCore.Tests/Microsoft.AspNet.PipelineCore.Tests.kproj rename to test/Microsoft.AspNet.Http.Core.Tests/Microsoft.AspNet.Http.Core.Tests.kproj diff --git a/test/Microsoft.AspNet.PipelineCore.Tests/QueryFeatureTests.cs b/test/Microsoft.AspNet.Http.Core.Tests/QueryFeatureTests.cs similarity index 100% rename from test/Microsoft.AspNet.PipelineCore.Tests/QueryFeatureTests.cs rename to test/Microsoft.AspNet.Http.Core.Tests/QueryFeatureTests.cs diff --git a/test/Microsoft.AspNet.PipelineCore.Tests/project.json b/test/Microsoft.AspNet.Http.Core.Tests/project.json similarity index 90% rename from test/Microsoft.AspNet.PipelineCore.Tests/project.json rename to test/Microsoft.AspNet.Http.Core.Tests/project.json index fda938bb96..3d6bee5edb 100644 --- a/test/Microsoft.AspNet.PipelineCore.Tests/project.json +++ b/test/Microsoft.AspNet.Http.Core.Tests/project.json @@ -3,7 +3,7 @@ "Microsoft.AspNet.FeatureModel": "1.0.0-*", "Microsoft.AspNet.Http": "1.0.0-*", "Microsoft.AspNet.HttpFeature": "1.0.0-*", - "Microsoft.AspNet.PipelineCore": "1.0.0-*", + "Microsoft.AspNet.Http.Core": "1.0.0-*", "xunit.runner.kre": "1.0.0-*" }, "commands": { diff --git a/test/Microsoft.AspNet.Http.Extensions.Tests/project.json b/test/Microsoft.AspNet.Http.Extensions.Tests/project.json index 85c20ce28b..55f53f5fe7 100644 --- a/test/Microsoft.AspNet.Http.Extensions.Tests/project.json +++ b/test/Microsoft.AspNet.Http.Extensions.Tests/project.json @@ -3,7 +3,7 @@ "Microsoft.AspNet.Http": "1.0.0-*", "Microsoft.AspNet.Http.Extensions": "1.0.0-*", "Microsoft.AspNet.HttpFeature": "1.0.0-*", - "Microsoft.AspNet.PipelineCore": "1.0.0-*", + "Microsoft.AspNet.Http.Core": "1.0.0-*", "xunit.runner.kre": "1.0.0-*" }, "commands": { diff --git a/test/Microsoft.AspNet.Http.Tests/project.json b/test/Microsoft.AspNet.Http.Tests/project.json index ff303ab74d..134db2aeae 100644 --- a/test/Microsoft.AspNet.Http.Tests/project.json +++ b/test/Microsoft.AspNet.Http.Tests/project.json @@ -2,7 +2,7 @@ "dependencies": { "Microsoft.AspNet.Http": "1.0.0-*", "Microsoft.AspNet.HttpFeature": "1.0.0-*", - "Microsoft.AspNet.PipelineCore": "1.0.0-*", + "Microsoft.AspNet.Http.Core": "1.0.0-*", "Microsoft.AspNet.Testing": "1.0.0-*", "xunit.runner.kre": "1.0.0-*" }, diff --git a/test/Microsoft.AspNet.Owin.Tests/project.json b/test/Microsoft.AspNet.Owin.Tests/project.json index 16f501140e..4725d69b82 100644 --- a/test/Microsoft.AspNet.Owin.Tests/project.json +++ b/test/Microsoft.AspNet.Owin.Tests/project.json @@ -4,7 +4,7 @@ "Microsoft.AspNet.Http": "1.0.0-*", "Microsoft.AspNet.HttpFeature": "1.0.0-*", "Microsoft.AspNet.Owin": "1.0.0-*", - "Microsoft.AspNet.PipelineCore": "1.0.0-*", + "Microsoft.AspNet.Http.Core": "1.0.0-*", "xunit.runner.kre": "1.0.0-*" }, "commands": { diff --git a/test/Microsoft.AspNet.WebUtilities.Tests/project.json b/test/Microsoft.AspNet.WebUtilities.Tests/project.json index a445ee721d..d449b763bd 100644 --- a/test/Microsoft.AspNet.WebUtilities.Tests/project.json +++ b/test/Microsoft.AspNet.WebUtilities.Tests/project.json @@ -2,7 +2,7 @@ "dependencies": { "Microsoft.AspNet.Http": "1.0.0-*", "Microsoft.AspNet.WebUtilities": "1.0.0-*", - "Microsoft.AspNet.PipelineCore": "1.0.0-*", + "Microsoft.AspNet.Http.Core": "1.0.0-*", "xunit.runner.kre": "1.0.0-*" }, "commands": { From 26860ad7de3e74a7c72bb5900e3928b27456a7d3 Mon Sep 17 00:00:00 2001 From: Ajay Bhargav Baaskaran Date: Thu, 15 Jan 2015 18:34:38 -0800 Subject: [PATCH 09/16] Code cleanup --- src/Microsoft.AspNet.Http.Extensions/FormFileExtensions.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/Microsoft.AspNet.Http.Extensions/FormFileExtensions.cs b/src/Microsoft.AspNet.Http.Extensions/FormFileExtensions.cs index a0c78ead9e..e97059c66b 100644 --- a/src/Microsoft.AspNet.Http.Extensions/FormFileExtensions.cs +++ b/src/Microsoft.AspNet.Http.Extensions/FormFileExtensions.cs @@ -12,7 +12,8 @@ namespace Microsoft.AspNet.Http /// public static class FormFileExtensions { - private static int DefaultBufferSize = 81920; + // Stream.CopyTo method uses 80KB as the default buffer size. + private static int DefaultBufferSize = 80 * 1024; /// /// Saves the contents of an uploaded file. From db484a7dcb32d4ff7e137c08480192126268ce88 Mon Sep 17 00:00:00 2001 From: Stephen Halter Date: Fri, 16 Jan 2015 17:08:30 -0800 Subject: [PATCH 10/16] Rename Microsoft.AspNet.HttpFeature to Microsoft.AspNet.Http.Interfaces --- HttpAbstractions.sln | 2 +- .../Collections/SessionCollection.cs | 2 +- src/Microsoft.AspNet.Http.Core/DefaultHttpContext.cs | 4 ++-- src/Microsoft.AspNet.Http.Core/DefaultHttpRequest.cs | 2 +- src/Microsoft.AspNet.Http.Core/DefaultHttpResponse.cs | 4 ++-- src/Microsoft.AspNet.Http.Core/HttpRequestFeature.cs | 2 +- src/Microsoft.AspNet.Http.Core/HttpResponseFeature.cs | 2 +- src/Microsoft.AspNet.Http.Core/QueryFeature.cs | 2 +- src/Microsoft.AspNet.Http.Core/RequestCookiesFeature.cs | 2 +- src/Microsoft.AspNet.Http.Core/ResponseCookiesFeature.cs | 2 +- src/Microsoft.AspNet.Http.Core/Security/AuthTypeContext.cs | 2 +- .../Security/AuthenticateContext.cs | 2 +- src/Microsoft.AspNet.Http.Core/Security/ChallengeContext.cs | 2 +- .../Security/HttpAuthenticationFeature.cs | 2 +- src/Microsoft.AspNet.Http.Core/Security/SignInContext.cs | 2 +- src/Microsoft.AspNet.Http.Core/Security/SignOutContext.cs | 2 +- src/Microsoft.AspNet.Http.Core/WebSocketAcceptContext.cs | 2 +- src/Microsoft.AspNet.Http.Core/project.json | 2 +- .../AssemblyNeutralAttribute.cs | 0 .../IHttpApplicationFeature.cs | 2 +- .../IHttpBufferingFeature.cs | 2 +- .../IHttpClientCertificateFeature.cs | 2 +- .../IHttpConnectionFeature.cs | 2 +- .../IHttpRequestFeature.cs | 2 +- .../IHttpRequestLifetimeFeature.cs | 2 +- .../IHttpResponseFeature.cs | 2 +- .../IHttpSendFileFeature.cs | 2 +- .../IHttpUpgradeFeature.cs | 2 +- .../IHttpWebSocketFeature.cs | 2 +- .../ISession.cs | 2 +- .../ISessionFactory.cs | 2 +- .../ISessionFeature.cs | 2 +- .../IWebSocketAcceptContext.cs | 2 +- .../Microsoft.AspNet.Http.Interfaces.kproj} | 0 .../Security/IAuthTypeContext.cs | 2 +- .../Security/IAuthenticateContext.cs | 2 +- .../Security/IAuthenticationHandler.cs | 2 +- .../Security/IChallengeContext.cs | 2 +- .../Security/IHttpAuthenticationFeature.cs | 2 +- .../Security/ISignInContext.cs | 2 +- .../Security/ISignOutContext .cs | 2 +- .../project.json | 0 src/Microsoft.AspNet.Owin/OwinEnvironment.cs | 4 ++-- src/Microsoft.AspNet.Owin/OwinFeatureCollection.cs | 4 ++-- .../WebSockets/OwinWebSocketAcceptAdapter.cs | 2 +- .../WebSockets/OwinWebSocketAcceptContext.cs | 2 +- .../WebSockets/WebSocketAcceptAdapter.cs | 2 +- src/Microsoft.AspNet.Owin/project.json | 2 +- test/Microsoft.AspNet.FeatureModel.Tests/project.json | 2 +- .../DefaultHttpContextTests.cs | 2 +- .../DefaultHttpRequestTests.cs | 2 +- test/Microsoft.AspNet.Http.Core.Tests/QueryFeatureTests.cs | 2 +- test/Microsoft.AspNet.Http.Core.Tests/project.json | 2 +- test/Microsoft.AspNet.Http.Extensions.Tests/project.json | 2 +- test/Microsoft.AspNet.Http.Tests/MapPathMiddlewareTests.cs | 2 +- .../MapPredicateMiddlewareTests.cs | 2 +- test/Microsoft.AspNet.Http.Tests/project.json | 2 +- .../Microsoft.AspNet.Owin.Tests/OwinFeatureCollectionTests.cs | 2 +- test/Microsoft.AspNet.Owin.Tests/project.json | 2 +- 59 files changed, 60 insertions(+), 60 deletions(-) rename src/{Microsoft.AspNet.HttpFeature => Microsoft.AspNet.Http.Interfaces}/AssemblyNeutralAttribute.cs (100%) rename src/{Microsoft.AspNet.HttpFeature => Microsoft.AspNet.Http.Interfaces}/IHttpApplicationFeature.cs (91%) rename src/{Microsoft.AspNet.HttpFeature => Microsoft.AspNet.Http.Interfaces}/IHttpBufferingFeature.cs (89%) rename src/{Microsoft.AspNet.HttpFeature => Microsoft.AspNet.Http.Interfaces}/IHttpClientCertificateFeature.cs (95%) rename src/{Microsoft.AspNet.HttpFeature => Microsoft.AspNet.Http.Interfaces}/IHttpConnectionFeature.cs (92%) rename src/{Microsoft.AspNet.HttpFeature => Microsoft.AspNet.Http.Interfaces}/IHttpRequestFeature.cs (94%) rename src/{Microsoft.AspNet.HttpFeature => Microsoft.AspNet.Http.Interfaces}/IHttpRequestLifetimeFeature.cs (90%) rename src/{Microsoft.AspNet.HttpFeature => Microsoft.AspNet.Http.Interfaces}/IHttpResponseFeature.cs (93%) rename src/{Microsoft.AspNet.HttpFeature => Microsoft.AspNet.Http.Interfaces}/IHttpSendFileFeature.cs (91%) rename src/{Microsoft.AspNet.HttpFeature => Microsoft.AspNet.Http.Interfaces}/IHttpUpgradeFeature.cs (90%) rename src/{Microsoft.AspNet.HttpFeature => Microsoft.AspNet.Http.Interfaces}/IHttpWebSocketFeature.cs (91%) rename src/{Microsoft.AspNet.HttpFeature => Microsoft.AspNet.Http.Interfaces}/ISession.cs (93%) rename src/{Microsoft.AspNet.HttpFeature => Microsoft.AspNet.Http.Interfaces}/ISessionFactory.cs (89%) rename src/{Microsoft.AspNet.HttpFeature => Microsoft.AspNet.Http.Interfaces}/ISessionFeature.cs (91%) rename src/{Microsoft.AspNet.HttpFeature => Microsoft.AspNet.Http.Interfaces}/IWebSocketAcceptContext.cs (88%) rename src/{Microsoft.AspNet.HttpFeature/Microsoft.AspNet.HttpFeature.kproj => Microsoft.AspNet.Http.Interfaces/Microsoft.AspNet.Http.Interfaces.kproj} (100%) rename src/{Microsoft.AspNet.HttpFeature => Microsoft.AspNet.Http.Interfaces}/Security/IAuthTypeContext.cs (88%) rename src/{Microsoft.AspNet.HttpFeature => Microsoft.AspNet.Http.Interfaces}/Security/IAuthenticateContext.cs (93%) rename src/{Microsoft.AspNet.HttpFeature => Microsoft.AspNet.Http.Interfaces}/Security/IAuthenticationHandler.cs (92%) rename src/{Microsoft.AspNet.HttpFeature => Microsoft.AspNet.Http.Interfaces}/Security/IChallengeContext.cs (90%) rename src/{Microsoft.AspNet.HttpFeature => Microsoft.AspNet.Http.Interfaces}/Security/IHttpAuthenticationFeature.cs (89%) rename src/{Microsoft.AspNet.HttpFeature => Microsoft.AspNet.Http.Interfaces}/Security/ISignInContext.cs (91%) rename src/{Microsoft.AspNet.HttpFeature => Microsoft.AspNet.Http.Interfaces}/Security/ISignOutContext .cs (89%) rename src/{Microsoft.AspNet.HttpFeature => Microsoft.AspNet.Http.Interfaces}/project.json (100%) diff --git a/HttpAbstractions.sln b/HttpAbstractions.sln index e4bd71292f..7a857f0800 100644 --- a/HttpAbstractions.sln +++ b/HttpAbstractions.sln @@ -11,7 +11,7 @@ Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "Microsoft.AspNet.Http.Core" EndProject Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "Microsoft.AspNet.Http", "src\Microsoft.AspNet.Http\Microsoft.AspNet.Http.kproj", "{22071333-15BA-4D16-A1D5-4D5B1A83FBDD}" EndProject -Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "Microsoft.AspNet.HttpFeature", "src\Microsoft.AspNet.HttpFeature\Microsoft.AspNet.HttpFeature.kproj", "{D9128247-8F97-48B8-A863-F1F21A029FCE}" +Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "Microsoft.AspNet.Http.Interfaces", "src\Microsoft.AspNet.Http.Interfaces\Microsoft.AspNet.Http.Interfaces.kproj", "{D9128247-8F97-48B8-A863-F1F21A029FCE}" EndProject Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "Microsoft.AspNet.FeatureModel", "src\Microsoft.AspNet.FeatureModel\Microsoft.AspNet.FeatureModel.kproj", "{32A4C918-30EE-41DB-8E26-8A3BB88ED231}" EndProject diff --git a/src/Microsoft.AspNet.Http.Core/Collections/SessionCollection.cs b/src/Microsoft.AspNet.Http.Core/Collections/SessionCollection.cs index 8d2c3b7565..52ad9af615 100644 --- a/src/Microsoft.AspNet.Http.Core/Collections/SessionCollection.cs +++ b/src/Microsoft.AspNet.Http.Core/Collections/SessionCollection.cs @@ -5,7 +5,7 @@ using System; using System.Collections; using System.Collections.Generic; using Microsoft.AspNet.Http; -using Microsoft.AspNet.HttpFeature; +using Microsoft.AspNet.Http.Interfaces; namespace Microsoft.AspNet.Http.Core.Collections { diff --git a/src/Microsoft.AspNet.Http.Core/DefaultHttpContext.cs b/src/Microsoft.AspNet.Http.Core/DefaultHttpContext.cs index 09915207ee..2de1680de3 100644 --- a/src/Microsoft.AspNet.Http.Core/DefaultHttpContext.cs +++ b/src/Microsoft.AspNet.Http.Core/DefaultHttpContext.cs @@ -12,8 +12,8 @@ using Microsoft.AspNet.FeatureModel; using Microsoft.AspNet.Http; using Microsoft.AspNet.Http.Infrastructure; using Microsoft.AspNet.Http.Security; -using Microsoft.AspNet.HttpFeature; -using Microsoft.AspNet.HttpFeature.Security; +using Microsoft.AspNet.Http.Interfaces; +using Microsoft.AspNet.Http.Interfaces.Security; using Microsoft.AspNet.Http.Core.Collections; using Microsoft.AspNet.Http.Core.Infrastructure; using Microsoft.AspNet.Http.Core.Security; diff --git a/src/Microsoft.AspNet.Http.Core/DefaultHttpRequest.cs b/src/Microsoft.AspNet.Http.Core/DefaultHttpRequest.cs index 28e8f2807c..46fa1907b7 100644 --- a/src/Microsoft.AspNet.Http.Core/DefaultHttpRequest.cs +++ b/src/Microsoft.AspNet.Http.Core/DefaultHttpRequest.cs @@ -8,7 +8,7 @@ using System.Threading.Tasks; using Microsoft.AspNet.FeatureModel; using Microsoft.AspNet.Http; using Microsoft.AspNet.Http.Infrastructure; -using Microsoft.AspNet.HttpFeature; +using Microsoft.AspNet.Http.Interfaces; using Microsoft.AspNet.Http.Core.Collections; using Microsoft.AspNet.Http.Core.Infrastructure; diff --git a/src/Microsoft.AspNet.Http.Core/DefaultHttpResponse.cs b/src/Microsoft.AspNet.Http.Core/DefaultHttpResponse.cs index 46358ad69f..7e44614704 100644 --- a/src/Microsoft.AspNet.Http.Core/DefaultHttpResponse.cs +++ b/src/Microsoft.AspNet.Http.Core/DefaultHttpResponse.cs @@ -12,8 +12,8 @@ using Microsoft.AspNet.Http; using Microsoft.AspNet.Http.Infrastructure; using Microsoft.AspNet.Http.Security; using Microsoft.AspNet.FeatureModel; -using Microsoft.AspNet.HttpFeature; -using Microsoft.AspNet.HttpFeature.Security; +using Microsoft.AspNet.Http.Interfaces; +using Microsoft.AspNet.Http.Interfaces.Security; using Microsoft.AspNet.Http.Core.Collections; using Microsoft.AspNet.Http.Core.Infrastructure; using Microsoft.AspNet.Http.Core.Security; diff --git a/src/Microsoft.AspNet.Http.Core/HttpRequestFeature.cs b/src/Microsoft.AspNet.Http.Core/HttpRequestFeature.cs index 751b366ebe..ae9fb236c9 100644 --- a/src/Microsoft.AspNet.Http.Core/HttpRequestFeature.cs +++ b/src/Microsoft.AspNet.Http.Core/HttpRequestFeature.cs @@ -4,7 +4,7 @@ using System; using System.Collections.Generic; using System.IO; -using Microsoft.AspNet.HttpFeature; +using Microsoft.AspNet.Http.Interfaces; namespace Microsoft.AspNet.Http.Core { diff --git a/src/Microsoft.AspNet.Http.Core/HttpResponseFeature.cs b/src/Microsoft.AspNet.Http.Core/HttpResponseFeature.cs index d4b5287885..4a5c6c7634 100644 --- a/src/Microsoft.AspNet.Http.Core/HttpResponseFeature.cs +++ b/src/Microsoft.AspNet.Http.Core/HttpResponseFeature.cs @@ -4,7 +4,7 @@ using System; using System.Collections.Generic; using System.IO; -using Microsoft.AspNet.HttpFeature; +using Microsoft.AspNet.Http.Interfaces; namespace Microsoft.AspNet.Http.Core { diff --git a/src/Microsoft.AspNet.Http.Core/QueryFeature.cs b/src/Microsoft.AspNet.Http.Core/QueryFeature.cs index e1b05f6b84..d9bfb76406 100644 --- a/src/Microsoft.AspNet.Http.Core/QueryFeature.cs +++ b/src/Microsoft.AspNet.Http.Core/QueryFeature.cs @@ -4,7 +4,7 @@ using System.Collections.Generic; using Microsoft.AspNet.FeatureModel; using Microsoft.AspNet.Http; -using Microsoft.AspNet.HttpFeature; +using Microsoft.AspNet.Http.Interfaces; using Microsoft.AspNet.Http.Core.Collections; using Microsoft.AspNet.Http.Core.Infrastructure; using Microsoft.AspNet.WebUtilities; diff --git a/src/Microsoft.AspNet.Http.Core/RequestCookiesFeature.cs b/src/Microsoft.AspNet.Http.Core/RequestCookiesFeature.cs index e57ff741ce..ea0aa45cf7 100644 --- a/src/Microsoft.AspNet.Http.Core/RequestCookiesFeature.cs +++ b/src/Microsoft.AspNet.Http.Core/RequestCookiesFeature.cs @@ -6,7 +6,7 @@ using System.Collections.Generic; using Microsoft.AspNet.FeatureModel; using Microsoft.AspNet.Http; using Microsoft.AspNet.Http.Infrastructure; -using Microsoft.AspNet.HttpFeature; +using Microsoft.AspNet.Http.Interfaces; using Microsoft.AspNet.Http.Core.Collections; using Microsoft.AspNet.Http.Core.Infrastructure; diff --git a/src/Microsoft.AspNet.Http.Core/ResponseCookiesFeature.cs b/src/Microsoft.AspNet.Http.Core/ResponseCookiesFeature.cs index 801d6d4b05..d4bf2d180a 100644 --- a/src/Microsoft.AspNet.Http.Core/ResponseCookiesFeature.cs +++ b/src/Microsoft.AspNet.Http.Core/ResponseCookiesFeature.cs @@ -3,7 +3,7 @@ using Microsoft.AspNet.Http; using Microsoft.AspNet.FeatureModel; -using Microsoft.AspNet.HttpFeature; +using Microsoft.AspNet.Http.Interfaces; using Microsoft.AspNet.Http.Core.Collections; using Microsoft.AspNet.Http.Core.Infrastructure; diff --git a/src/Microsoft.AspNet.Http.Core/Security/AuthTypeContext.cs b/src/Microsoft.AspNet.Http.Core/Security/AuthTypeContext.cs index d9e516e756..df792fc74f 100644 --- a/src/Microsoft.AspNet.Http.Core/Security/AuthTypeContext.cs +++ b/src/Microsoft.AspNet.Http.Core/Security/AuthTypeContext.cs @@ -4,7 +4,7 @@ using System; using System.Collections.Generic; using Microsoft.AspNet.Http.Security; -using Microsoft.AspNet.HttpFeature.Security; +using Microsoft.AspNet.Http.Interfaces.Security; namespace Microsoft.AspNet.Http.Core.Security { diff --git a/src/Microsoft.AspNet.Http.Core/Security/AuthenticateContext.cs b/src/Microsoft.AspNet.Http.Core/Security/AuthenticateContext.cs index e04326b257..95dd14a8d2 100644 --- a/src/Microsoft.AspNet.Http.Core/Security/AuthenticateContext.cs +++ b/src/Microsoft.AspNet.Http.Core/Security/AuthenticateContext.cs @@ -8,7 +8,7 @@ using System.Security.Claims; using System.Text; using System.Threading.Tasks; using Microsoft.AspNet.Http.Security; -using Microsoft.AspNet.HttpFeature.Security; +using Microsoft.AspNet.Http.Interfaces.Security; namespace Microsoft.AspNet.Http.Core.Security { diff --git a/src/Microsoft.AspNet.Http.Core/Security/ChallengeContext.cs b/src/Microsoft.AspNet.Http.Core/Security/ChallengeContext.cs index 5c8b8ef74b..aa9a424fc4 100644 --- a/src/Microsoft.AspNet.Http.Core/Security/ChallengeContext.cs +++ b/src/Microsoft.AspNet.Http.Core/Security/ChallengeContext.cs @@ -6,7 +6,7 @@ using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; -using Microsoft.AspNet.HttpFeature.Security; +using Microsoft.AspNet.Http.Interfaces.Security; namespace Microsoft.AspNet.Http.Core.Security { diff --git a/src/Microsoft.AspNet.Http.Core/Security/HttpAuthenticationFeature.cs b/src/Microsoft.AspNet.Http.Core/Security/HttpAuthenticationFeature.cs index 8e40287b22..f0989c420c 100644 --- a/src/Microsoft.AspNet.Http.Core/Security/HttpAuthenticationFeature.cs +++ b/src/Microsoft.AspNet.Http.Core/Security/HttpAuthenticationFeature.cs @@ -2,7 +2,7 @@ // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System.Security.Claims; -using Microsoft.AspNet.HttpFeature.Security; +using Microsoft.AspNet.Http.Interfaces.Security; namespace Microsoft.AspNet.Http.Core.Security { diff --git a/src/Microsoft.AspNet.Http.Core/Security/SignInContext.cs b/src/Microsoft.AspNet.Http.Core/Security/SignInContext.cs index 9856e7ee1a..0e9383e97c 100644 --- a/src/Microsoft.AspNet.Http.Core/Security/SignInContext.cs +++ b/src/Microsoft.AspNet.Http.Core/Security/SignInContext.cs @@ -4,7 +4,7 @@ using System; using System.Collections.Generic; using System.Security.Claims; -using Microsoft.AspNet.HttpFeature.Security; +using Microsoft.AspNet.Http.Interfaces.Security; namespace Microsoft.AspNet.Http.Core.Security { diff --git a/src/Microsoft.AspNet.Http.Core/Security/SignOutContext.cs b/src/Microsoft.AspNet.Http.Core/Security/SignOutContext.cs index 4ebde735d2..c2ed25f04a 100644 --- a/src/Microsoft.AspNet.Http.Core/Security/SignOutContext.cs +++ b/src/Microsoft.AspNet.Http.Core/Security/SignOutContext.cs @@ -3,7 +3,7 @@ using System; using System.Collections.Generic; -using Microsoft.AspNet.HttpFeature.Security; +using Microsoft.AspNet.Http.Interfaces.Security; namespace Microsoft.AspNet.Http.Core.Security { diff --git a/src/Microsoft.AspNet.Http.Core/WebSocketAcceptContext.cs b/src/Microsoft.AspNet.Http.Core/WebSocketAcceptContext.cs index 3c935b52f8..48e3cd7c71 100644 --- a/src/Microsoft.AspNet.Http.Core/WebSocketAcceptContext.cs +++ b/src/Microsoft.AspNet.Http.Core/WebSocketAcceptContext.cs @@ -1,7 +1,7 @@ // 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 Microsoft.AspNet.HttpFeature; +using Microsoft.AspNet.Http.Interfaces; namespace Microsoft.AspNet.Http.Core { diff --git a/src/Microsoft.AspNet.Http.Core/project.json b/src/Microsoft.AspNet.Http.Core/project.json index 8eaabc9a33..91374fa6c6 100644 --- a/src/Microsoft.AspNet.Http.Core/project.json +++ b/src/Microsoft.AspNet.Http.Core/project.json @@ -5,7 +5,7 @@ "dependencies": { "Microsoft.AspNet.FeatureModel": "1.0.0-*", "Microsoft.AspNet.Http": "1.0.0-*", - "Microsoft.AspNet.HttpFeature": "1.0.0-*", + "Microsoft.AspNet.Http.Interfaces": "1.0.0-*", "Microsoft.AspNet.WebUtilities": "1.0.0-*", "Microsoft.Net.Http.Headers": "1.0.0-*" diff --git a/src/Microsoft.AspNet.HttpFeature/AssemblyNeutralAttribute.cs b/src/Microsoft.AspNet.Http.Interfaces/AssemblyNeutralAttribute.cs similarity index 100% rename from src/Microsoft.AspNet.HttpFeature/AssemblyNeutralAttribute.cs rename to src/Microsoft.AspNet.Http.Interfaces/AssemblyNeutralAttribute.cs diff --git a/src/Microsoft.AspNet.HttpFeature/IHttpApplicationFeature.cs b/src/Microsoft.AspNet.Http.Interfaces/IHttpApplicationFeature.cs similarity index 91% rename from src/Microsoft.AspNet.HttpFeature/IHttpApplicationFeature.cs rename to src/Microsoft.AspNet.Http.Interfaces/IHttpApplicationFeature.cs index 344f87352c..2a885ecbbb 100644 --- a/src/Microsoft.AspNet.HttpFeature/IHttpApplicationFeature.cs +++ b/src/Microsoft.AspNet.Http.Interfaces/IHttpApplicationFeature.cs @@ -4,7 +4,7 @@ using System.Threading; using Microsoft.Framework.Runtime; -namespace Microsoft.AspNet.HttpFeature +namespace Microsoft.AspNet.Http.Interfaces { [AssemblyNeutral] public interface IHttpApplicationFeature diff --git a/src/Microsoft.AspNet.HttpFeature/IHttpBufferingFeature.cs b/src/Microsoft.AspNet.Http.Interfaces/IHttpBufferingFeature.cs similarity index 89% rename from src/Microsoft.AspNet.HttpFeature/IHttpBufferingFeature.cs rename to src/Microsoft.AspNet.Http.Interfaces/IHttpBufferingFeature.cs index 53b7fd9894..83a6be21d0 100644 --- a/src/Microsoft.AspNet.HttpFeature/IHttpBufferingFeature.cs +++ b/src/Microsoft.AspNet.Http.Interfaces/IHttpBufferingFeature.cs @@ -3,7 +3,7 @@ using Microsoft.Framework.Runtime; -namespace Microsoft.AspNet.HttpFeature +namespace Microsoft.AspNet.Http.Interfaces { [AssemblyNeutral] public interface IHttpBufferingFeature diff --git a/src/Microsoft.AspNet.HttpFeature/IHttpClientCertificateFeature.cs b/src/Microsoft.AspNet.Http.Interfaces/IHttpClientCertificateFeature.cs similarity index 95% rename from src/Microsoft.AspNet.HttpFeature/IHttpClientCertificateFeature.cs rename to src/Microsoft.AspNet.Http.Interfaces/IHttpClientCertificateFeature.cs index b70fa027cf..556265c165 100644 --- a/src/Microsoft.AspNet.HttpFeature/IHttpClientCertificateFeature.cs +++ b/src/Microsoft.AspNet.Http.Interfaces/IHttpClientCertificateFeature.cs @@ -6,7 +6,7 @@ using System.Threading; using System.Threading.Tasks; using Microsoft.Framework.Runtime; -namespace Microsoft.AspNet.HttpFeature +namespace Microsoft.AspNet.Http.Interfaces { [AssemblyNeutral] public interface IHttpClientCertificateFeature diff --git a/src/Microsoft.AspNet.HttpFeature/IHttpConnectionFeature.cs b/src/Microsoft.AspNet.Http.Interfaces/IHttpConnectionFeature.cs similarity index 92% rename from src/Microsoft.AspNet.HttpFeature/IHttpConnectionFeature.cs rename to src/Microsoft.AspNet.Http.Interfaces/IHttpConnectionFeature.cs index 7598c9ce0b..51940f3709 100644 --- a/src/Microsoft.AspNet.HttpFeature/IHttpConnectionFeature.cs +++ b/src/Microsoft.AspNet.Http.Interfaces/IHttpConnectionFeature.cs @@ -4,7 +4,7 @@ using System.Net; using Microsoft.Framework.Runtime; -namespace Microsoft.AspNet.HttpFeature +namespace Microsoft.AspNet.Http.Interfaces { [AssemblyNeutral] public interface IHttpConnectionFeature diff --git a/src/Microsoft.AspNet.HttpFeature/IHttpRequestFeature.cs b/src/Microsoft.AspNet.Http.Interfaces/IHttpRequestFeature.cs similarity index 94% rename from src/Microsoft.AspNet.HttpFeature/IHttpRequestFeature.cs rename to src/Microsoft.AspNet.Http.Interfaces/IHttpRequestFeature.cs index 66b5c5fa82..ca178a7dfa 100644 --- a/src/Microsoft.AspNet.HttpFeature/IHttpRequestFeature.cs +++ b/src/Microsoft.AspNet.Http.Interfaces/IHttpRequestFeature.cs @@ -6,7 +6,7 @@ using System.Collections.Generic; using System.IO; using Microsoft.Framework.Runtime; -namespace Microsoft.AspNet.HttpFeature +namespace Microsoft.AspNet.Http.Interfaces { [AssemblyNeutral] public interface IHttpRequestFeature diff --git a/src/Microsoft.AspNet.HttpFeature/IHttpRequestLifetimeFeature.cs b/src/Microsoft.AspNet.Http.Interfaces/IHttpRequestLifetimeFeature.cs similarity index 90% rename from src/Microsoft.AspNet.HttpFeature/IHttpRequestLifetimeFeature.cs rename to src/Microsoft.AspNet.Http.Interfaces/IHttpRequestLifetimeFeature.cs index b6bcee9c32..498463dbc3 100644 --- a/src/Microsoft.AspNet.HttpFeature/IHttpRequestLifetimeFeature.cs +++ b/src/Microsoft.AspNet.Http.Interfaces/IHttpRequestLifetimeFeature.cs @@ -4,7 +4,7 @@ using System.Threading; using Microsoft.Framework.Runtime; -namespace Microsoft.AspNet.HttpFeature +namespace Microsoft.AspNet.Http.Interfaces { [AssemblyNeutral] public interface IHttpRequestLifetimeFeature diff --git a/src/Microsoft.AspNet.HttpFeature/IHttpResponseFeature.cs b/src/Microsoft.AspNet.Http.Interfaces/IHttpResponseFeature.cs similarity index 93% rename from src/Microsoft.AspNet.HttpFeature/IHttpResponseFeature.cs rename to src/Microsoft.AspNet.Http.Interfaces/IHttpResponseFeature.cs index aa7603be03..b28700df9c 100644 --- a/src/Microsoft.AspNet.HttpFeature/IHttpResponseFeature.cs +++ b/src/Microsoft.AspNet.Http.Interfaces/IHttpResponseFeature.cs @@ -6,7 +6,7 @@ using System.Collections.Generic; using System.IO; using Microsoft.Framework.Runtime; -namespace Microsoft.AspNet.HttpFeature +namespace Microsoft.AspNet.Http.Interfaces { [AssemblyNeutral] public interface IHttpResponseFeature diff --git a/src/Microsoft.AspNet.HttpFeature/IHttpSendFileFeature.cs b/src/Microsoft.AspNet.Http.Interfaces/IHttpSendFileFeature.cs similarity index 91% rename from src/Microsoft.AspNet.HttpFeature/IHttpSendFileFeature.cs rename to src/Microsoft.AspNet.Http.Interfaces/IHttpSendFileFeature.cs index be42442acc..dcb598e6ce 100644 --- a/src/Microsoft.AspNet.HttpFeature/IHttpSendFileFeature.cs +++ b/src/Microsoft.AspNet.Http.Interfaces/IHttpSendFileFeature.cs @@ -5,7 +5,7 @@ using System.Threading; using System.Threading.Tasks; using Microsoft.Framework.Runtime; -namespace Microsoft.AspNet.HttpFeature +namespace Microsoft.AspNet.Http.Interfaces { [AssemblyNeutral] public interface IHttpSendFileFeature diff --git a/src/Microsoft.AspNet.HttpFeature/IHttpUpgradeFeature.cs b/src/Microsoft.AspNet.Http.Interfaces/IHttpUpgradeFeature.cs similarity index 90% rename from src/Microsoft.AspNet.HttpFeature/IHttpUpgradeFeature.cs rename to src/Microsoft.AspNet.Http.Interfaces/IHttpUpgradeFeature.cs index 3d3f924cae..98d4e60cba 100644 --- a/src/Microsoft.AspNet.HttpFeature/IHttpUpgradeFeature.cs +++ b/src/Microsoft.AspNet.Http.Interfaces/IHttpUpgradeFeature.cs @@ -5,7 +5,7 @@ using System.IO; using System.Threading.Tasks; using Microsoft.Framework.Runtime; -namespace Microsoft.AspNet.HttpFeature +namespace Microsoft.AspNet.Http.Interfaces { [AssemblyNeutral] public interface IHttpUpgradeFeature diff --git a/src/Microsoft.AspNet.HttpFeature/IHttpWebSocketFeature.cs b/src/Microsoft.AspNet.Http.Interfaces/IHttpWebSocketFeature.cs similarity index 91% rename from src/Microsoft.AspNet.HttpFeature/IHttpWebSocketFeature.cs rename to src/Microsoft.AspNet.Http.Interfaces/IHttpWebSocketFeature.cs index 3d87e86bb4..d3aeaae642 100644 --- a/src/Microsoft.AspNet.HttpFeature/IHttpWebSocketFeature.cs +++ b/src/Microsoft.AspNet.Http.Interfaces/IHttpWebSocketFeature.cs @@ -5,7 +5,7 @@ using System.Net.WebSockets; using System.Threading.Tasks; using Microsoft.Framework.Runtime; -namespace Microsoft.AspNet.HttpFeature +namespace Microsoft.AspNet.Http.Interfaces { [AssemblyNeutral] public interface IHttpWebSocketFeature diff --git a/src/Microsoft.AspNet.HttpFeature/ISession.cs b/src/Microsoft.AspNet.Http.Interfaces/ISession.cs similarity index 93% rename from src/Microsoft.AspNet.HttpFeature/ISession.cs rename to src/Microsoft.AspNet.Http.Interfaces/ISession.cs index 6a0fe4035a..d18fca3e99 100644 --- a/src/Microsoft.AspNet.HttpFeature/ISession.cs +++ b/src/Microsoft.AspNet.Http.Interfaces/ISession.cs @@ -5,7 +5,7 @@ using System; using System.Collections.Generic; using Microsoft.Framework.Runtime; -namespace Microsoft.AspNet.HttpFeature +namespace Microsoft.AspNet.Http.Interfaces { [AssemblyNeutral] public interface ISession diff --git a/src/Microsoft.AspNet.HttpFeature/ISessionFactory.cs b/src/Microsoft.AspNet.Http.Interfaces/ISessionFactory.cs similarity index 89% rename from src/Microsoft.AspNet.HttpFeature/ISessionFactory.cs rename to src/Microsoft.AspNet.Http.Interfaces/ISessionFactory.cs index 1187696726..e8917db76d 100644 --- a/src/Microsoft.AspNet.HttpFeature/ISessionFactory.cs +++ b/src/Microsoft.AspNet.Http.Interfaces/ISessionFactory.cs @@ -4,7 +4,7 @@ using System; using Microsoft.Framework.Runtime; -namespace Microsoft.AspNet.HttpFeature +namespace Microsoft.AspNet.Http.Interfaces { [AssemblyNeutral] public interface ISessionFactory diff --git a/src/Microsoft.AspNet.HttpFeature/ISessionFeature.cs b/src/Microsoft.AspNet.Http.Interfaces/ISessionFeature.cs similarity index 91% rename from src/Microsoft.AspNet.HttpFeature/ISessionFeature.cs rename to src/Microsoft.AspNet.Http.Interfaces/ISessionFeature.cs index 400f365b68..360e407c65 100644 --- a/src/Microsoft.AspNet.HttpFeature/ISessionFeature.cs +++ b/src/Microsoft.AspNet.Http.Interfaces/ISessionFeature.cs @@ -3,7 +3,7 @@ using Microsoft.Framework.Runtime; -namespace Microsoft.AspNet.HttpFeature +namespace Microsoft.AspNet.Http.Interfaces { // TODO: Is there any reason not to flatten the Factory down into the Feature? [AssemblyNeutral] diff --git a/src/Microsoft.AspNet.HttpFeature/IWebSocketAcceptContext.cs b/src/Microsoft.AspNet.Http.Interfaces/IWebSocketAcceptContext.cs similarity index 88% rename from src/Microsoft.AspNet.HttpFeature/IWebSocketAcceptContext.cs rename to src/Microsoft.AspNet.Http.Interfaces/IWebSocketAcceptContext.cs index dbcc14c7ce..80becb585a 100644 --- a/src/Microsoft.AspNet.HttpFeature/IWebSocketAcceptContext.cs +++ b/src/Microsoft.AspNet.Http.Interfaces/IWebSocketAcceptContext.cs @@ -3,7 +3,7 @@ using Microsoft.Framework.Runtime; -namespace Microsoft.AspNet.HttpFeature +namespace Microsoft.AspNet.Http.Interfaces { [AssemblyNeutral] public interface IWebSocketAcceptContext diff --git a/src/Microsoft.AspNet.HttpFeature/Microsoft.AspNet.HttpFeature.kproj b/src/Microsoft.AspNet.Http.Interfaces/Microsoft.AspNet.Http.Interfaces.kproj similarity index 100% rename from src/Microsoft.AspNet.HttpFeature/Microsoft.AspNet.HttpFeature.kproj rename to src/Microsoft.AspNet.Http.Interfaces/Microsoft.AspNet.Http.Interfaces.kproj diff --git a/src/Microsoft.AspNet.HttpFeature/Security/IAuthTypeContext.cs b/src/Microsoft.AspNet.Http.Interfaces/Security/IAuthTypeContext.cs similarity index 88% rename from src/Microsoft.AspNet.HttpFeature/Security/IAuthTypeContext.cs rename to src/Microsoft.AspNet.Http.Interfaces/Security/IAuthTypeContext.cs index bf3627a3d8..d3308720c9 100644 --- a/src/Microsoft.AspNet.HttpFeature/Security/IAuthTypeContext.cs +++ b/src/Microsoft.AspNet.Http.Interfaces/Security/IAuthTypeContext.cs @@ -4,7 +4,7 @@ using System.Collections.Generic; using Microsoft.Framework.Runtime; -namespace Microsoft.AspNet.HttpFeature.Security +namespace Microsoft.AspNet.Http.Interfaces.Security { [AssemblyNeutral] public interface IAuthTypeContext diff --git a/src/Microsoft.AspNet.HttpFeature/Security/IAuthenticateContext.cs b/src/Microsoft.AspNet.Http.Interfaces/Security/IAuthenticateContext.cs similarity index 93% rename from src/Microsoft.AspNet.HttpFeature/Security/IAuthenticateContext.cs rename to src/Microsoft.AspNet.Http.Interfaces/Security/IAuthenticateContext.cs index 333c044f86..1e354a8149 100644 --- a/src/Microsoft.AspNet.HttpFeature/Security/IAuthenticateContext.cs +++ b/src/Microsoft.AspNet.Http.Interfaces/Security/IAuthenticateContext.cs @@ -5,7 +5,7 @@ using System.Collections.Generic; using System.Security.Claims; using Microsoft.Framework.Runtime; -namespace Microsoft.AspNet.HttpFeature.Security +namespace Microsoft.AspNet.Http.Interfaces.Security { [AssemblyNeutral] public interface IAuthenticateContext diff --git a/src/Microsoft.AspNet.HttpFeature/Security/IAuthenticationHandler.cs b/src/Microsoft.AspNet.Http.Interfaces/Security/IAuthenticationHandler.cs similarity index 92% rename from src/Microsoft.AspNet.HttpFeature/Security/IAuthenticationHandler.cs rename to src/Microsoft.AspNet.Http.Interfaces/Security/IAuthenticationHandler.cs index f081744b22..ca68c40668 100644 --- a/src/Microsoft.AspNet.HttpFeature/Security/IAuthenticationHandler.cs +++ b/src/Microsoft.AspNet.Http.Interfaces/Security/IAuthenticationHandler.cs @@ -5,7 +5,7 @@ using System.Collections.Generic; using System.Threading.Tasks; using Microsoft.Framework.Runtime; -namespace Microsoft.AspNet.HttpFeature.Security +namespace Microsoft.AspNet.Http.Interfaces.Security { [AssemblyNeutral] public interface IAuthenticationHandler diff --git a/src/Microsoft.AspNet.HttpFeature/Security/IChallengeContext.cs b/src/Microsoft.AspNet.Http.Interfaces/Security/IChallengeContext.cs similarity index 90% rename from src/Microsoft.AspNet.HttpFeature/Security/IChallengeContext.cs rename to src/Microsoft.AspNet.Http.Interfaces/Security/IChallengeContext.cs index 5f32f318df..94d64b6257 100644 --- a/src/Microsoft.AspNet.HttpFeature/Security/IChallengeContext.cs +++ b/src/Microsoft.AspNet.Http.Interfaces/Security/IChallengeContext.cs @@ -4,7 +4,7 @@ using System.Collections.Generic; using Microsoft.Framework.Runtime; -namespace Microsoft.AspNet.HttpFeature.Security +namespace Microsoft.AspNet.Http.Interfaces.Security { [AssemblyNeutral] public interface IChallengeContext diff --git a/src/Microsoft.AspNet.HttpFeature/Security/IHttpAuthenticationFeature.cs b/src/Microsoft.AspNet.Http.Interfaces/Security/IHttpAuthenticationFeature.cs similarity index 89% rename from src/Microsoft.AspNet.HttpFeature/Security/IHttpAuthenticationFeature.cs rename to src/Microsoft.AspNet.Http.Interfaces/Security/IHttpAuthenticationFeature.cs index 4ee676b095..74605995a9 100644 --- a/src/Microsoft.AspNet.HttpFeature/Security/IHttpAuthenticationFeature.cs +++ b/src/Microsoft.AspNet.Http.Interfaces/Security/IHttpAuthenticationFeature.cs @@ -4,7 +4,7 @@ using System.Security.Claims; using Microsoft.Framework.Runtime; -namespace Microsoft.AspNet.HttpFeature.Security +namespace Microsoft.AspNet.Http.Interfaces.Security { [AssemblyNeutral] public interface IHttpAuthenticationFeature diff --git a/src/Microsoft.AspNet.HttpFeature/Security/ISignInContext.cs b/src/Microsoft.AspNet.Http.Interfaces/Security/ISignInContext.cs similarity index 91% rename from src/Microsoft.AspNet.HttpFeature/Security/ISignInContext.cs rename to src/Microsoft.AspNet.Http.Interfaces/Security/ISignInContext.cs index 694ece38d2..eea09aa055 100644 --- a/src/Microsoft.AspNet.HttpFeature/Security/ISignInContext.cs +++ b/src/Microsoft.AspNet.Http.Interfaces/Security/ISignInContext.cs @@ -5,7 +5,7 @@ using System.Collections.Generic; using System.Security.Claims; using Microsoft.Framework.Runtime; -namespace Microsoft.AspNet.HttpFeature.Security +namespace Microsoft.AspNet.Http.Interfaces.Security { [AssemblyNeutral] public interface ISignInContext diff --git a/src/Microsoft.AspNet.HttpFeature/Security/ISignOutContext .cs b/src/Microsoft.AspNet.Http.Interfaces/Security/ISignOutContext .cs similarity index 89% rename from src/Microsoft.AspNet.HttpFeature/Security/ISignOutContext .cs rename to src/Microsoft.AspNet.Http.Interfaces/Security/ISignOutContext .cs index 232db13ad7..fd4fe89b11 100644 --- a/src/Microsoft.AspNet.HttpFeature/Security/ISignOutContext .cs +++ b/src/Microsoft.AspNet.Http.Interfaces/Security/ISignOutContext .cs @@ -4,7 +4,7 @@ using System.Collections.Generic; using Microsoft.Framework.Runtime; -namespace Microsoft.AspNet.HttpFeature.Security +namespace Microsoft.AspNet.Http.Interfaces.Security { [AssemblyNeutral] public interface ISignOutContext diff --git a/src/Microsoft.AspNet.HttpFeature/project.json b/src/Microsoft.AspNet.Http.Interfaces/project.json similarity index 100% rename from src/Microsoft.AspNet.HttpFeature/project.json rename to src/Microsoft.AspNet.Http.Interfaces/project.json diff --git a/src/Microsoft.AspNet.Owin/OwinEnvironment.cs b/src/Microsoft.AspNet.Owin/OwinEnvironment.cs index 9ff85950f2..4a97095eda 100644 --- a/src/Microsoft.AspNet.Owin/OwinEnvironment.cs +++ b/src/Microsoft.AspNet.Owin/OwinEnvironment.cs @@ -14,8 +14,8 @@ using System.Security.Principal; using System.Threading; using System.Threading.Tasks; using Microsoft.AspNet.Http; -using Microsoft.AspNet.HttpFeature; -using Microsoft.AspNet.HttpFeature.Security; +using Microsoft.AspNet.Http.Interfaces; +using Microsoft.AspNet.Http.Interfaces.Security; using Microsoft.AspNet.Http.Core.Security; namespace Microsoft.AspNet.Owin diff --git a/src/Microsoft.AspNet.Owin/OwinFeatureCollection.cs b/src/Microsoft.AspNet.Owin/OwinFeatureCollection.cs index 3332221be8..469933cf64 100644 --- a/src/Microsoft.AspNet.Owin/OwinFeatureCollection.cs +++ b/src/Microsoft.AspNet.Owin/OwinFeatureCollection.cs @@ -15,8 +15,8 @@ using System.Security.Principal; using System.Threading; using System.Threading.Tasks; using Microsoft.AspNet.FeatureModel; -using Microsoft.AspNet.HttpFeature; -using Microsoft.AspNet.HttpFeature.Security; +using Microsoft.AspNet.Http.Interfaces; +using Microsoft.AspNet.Http.Interfaces.Security; namespace Microsoft.AspNet.Owin { diff --git a/src/Microsoft.AspNet.Owin/WebSockets/OwinWebSocketAcceptAdapter.cs b/src/Microsoft.AspNet.Owin/WebSockets/OwinWebSocketAcceptAdapter.cs index 15ec0a01f8..20e73c2967 100644 --- a/src/Microsoft.AspNet.Owin/WebSockets/OwinWebSocketAcceptAdapter.cs +++ b/src/Microsoft.AspNet.Owin/WebSockets/OwinWebSocketAcceptAdapter.cs @@ -5,7 +5,7 @@ using System; using System.Collections.Generic; using System.Net.WebSockets; using System.Threading.Tasks; -using Microsoft.AspNet.HttpFeature; +using Microsoft.AspNet.Http.Interfaces; namespace Microsoft.AspNet.Owin { diff --git a/src/Microsoft.AspNet.Owin/WebSockets/OwinWebSocketAcceptContext.cs b/src/Microsoft.AspNet.Owin/WebSockets/OwinWebSocketAcceptContext.cs index 1e847e6264..063f43eead 100644 --- a/src/Microsoft.AspNet.Owin/WebSockets/OwinWebSocketAcceptContext.cs +++ b/src/Microsoft.AspNet.Owin/WebSockets/OwinWebSocketAcceptContext.cs @@ -2,7 +2,7 @@ // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System.Collections.Generic; -using Microsoft.AspNet.HttpFeature; +using Microsoft.AspNet.Http.Interfaces; namespace Microsoft.AspNet.Owin { diff --git a/src/Microsoft.AspNet.Owin/WebSockets/WebSocketAcceptAdapter.cs b/src/Microsoft.AspNet.Owin/WebSockets/WebSocketAcceptAdapter.cs index 9660a5d00e..1d43a8a85c 100644 --- a/src/Microsoft.AspNet.Owin/WebSockets/WebSocketAcceptAdapter.cs +++ b/src/Microsoft.AspNet.Owin/WebSockets/WebSocketAcceptAdapter.cs @@ -6,7 +6,7 @@ using System.Collections.Generic; using System.Net.WebSockets; using System.Threading; using System.Threading.Tasks; -using Microsoft.AspNet.HttpFeature; +using Microsoft.AspNet.Http.Interfaces; namespace Microsoft.AspNet.Owin { diff --git a/src/Microsoft.AspNet.Owin/project.json b/src/Microsoft.AspNet.Owin/project.json index ce8c5b3058..ffa82b3a9e 100644 --- a/src/Microsoft.AspNet.Owin/project.json +++ b/src/Microsoft.AspNet.Owin/project.json @@ -5,7 +5,7 @@ "Microsoft.AspNet.Http": "1.0.0-*", "Microsoft.AspNet.FeatureModel": "1.0.0-*", "Microsoft.AspNet.Http.Core": "1.0.0-*", - "Microsoft.AspNet.HttpFeature": { "version": "1.0.0-*", "type": "build" } + "Microsoft.AspNet.Http.Interfaces": { "version": "1.0.0-*", "type": "build" } }, "frameworks": { "aspnet50": { }, diff --git a/test/Microsoft.AspNet.FeatureModel.Tests/project.json b/test/Microsoft.AspNet.FeatureModel.Tests/project.json index 867e8889e6..6dfc567e0b 100644 --- a/test/Microsoft.AspNet.FeatureModel.Tests/project.json +++ b/test/Microsoft.AspNet.FeatureModel.Tests/project.json @@ -2,7 +2,7 @@ "dependencies": { "Microsoft.AspNet.FeatureModel": "1.0.0-*", "Microsoft.AspNet.Http": "1.0.0-*", - "Microsoft.AspNet.HttpFeature": "1.0.0-*", + "Microsoft.AspNet.Http.Interfaces": "1.0.0-*", "xunit.runner.kre": "1.0.0-*" }, "commands": { diff --git a/test/Microsoft.AspNet.Http.Core.Tests/DefaultHttpContextTests.cs b/test/Microsoft.AspNet.Http.Core.Tests/DefaultHttpContextTests.cs index 2b04823c53..146967c135 100644 --- a/test/Microsoft.AspNet.Http.Core.Tests/DefaultHttpContextTests.cs +++ b/test/Microsoft.AspNet.Http.Core.Tests/DefaultHttpContextTests.cs @@ -9,7 +9,7 @@ using System.Security.Claims; using System.Threading.Tasks; using Microsoft.AspNet.Http; using Microsoft.AspNet.FeatureModel; -using Microsoft.AspNet.HttpFeature; +using Microsoft.AspNet.Http.Interfaces; using Xunit; namespace Microsoft.AspNet.Http.Core.Tests diff --git a/test/Microsoft.AspNet.Http.Core.Tests/DefaultHttpRequestTests.cs b/test/Microsoft.AspNet.Http.Core.Tests/DefaultHttpRequestTests.cs index e2518fd10b..7c5557bb2a 100644 --- a/test/Microsoft.AspNet.Http.Core.Tests/DefaultHttpRequestTests.cs +++ b/test/Microsoft.AspNet.Http.Core.Tests/DefaultHttpRequestTests.cs @@ -5,7 +5,7 @@ using System; using System.Collections.Generic; using System.Globalization; using Microsoft.AspNet.Http; -using Microsoft.AspNet.HttpFeature; +using Microsoft.AspNet.Http.Interfaces; using Xunit; namespace Microsoft.AspNet.Http.Core.Tests diff --git a/test/Microsoft.AspNet.Http.Core.Tests/QueryFeatureTests.cs b/test/Microsoft.AspNet.Http.Core.Tests/QueryFeatureTests.cs index f76fdb25fe..91846bc000 100644 --- a/test/Microsoft.AspNet.Http.Core.Tests/QueryFeatureTests.cs +++ b/test/Microsoft.AspNet.Http.Core.Tests/QueryFeatureTests.cs @@ -2,7 +2,7 @@ // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using Microsoft.AspNet.FeatureModel; -using Microsoft.AspNet.HttpFeature; +using Microsoft.AspNet.Http.Interfaces; using Moq; using Xunit; diff --git a/test/Microsoft.AspNet.Http.Core.Tests/project.json b/test/Microsoft.AspNet.Http.Core.Tests/project.json index 3d6bee5edb..df6823dc6d 100644 --- a/test/Microsoft.AspNet.Http.Core.Tests/project.json +++ b/test/Microsoft.AspNet.Http.Core.Tests/project.json @@ -2,7 +2,7 @@ "dependencies": { "Microsoft.AspNet.FeatureModel": "1.0.0-*", "Microsoft.AspNet.Http": "1.0.0-*", - "Microsoft.AspNet.HttpFeature": "1.0.0-*", + "Microsoft.AspNet.Http.Interfaces": "1.0.0-*", "Microsoft.AspNet.Http.Core": "1.0.0-*", "xunit.runner.kre": "1.0.0-*" }, diff --git a/test/Microsoft.AspNet.Http.Extensions.Tests/project.json b/test/Microsoft.AspNet.Http.Extensions.Tests/project.json index 55f53f5fe7..ddf07a3826 100644 --- a/test/Microsoft.AspNet.Http.Extensions.Tests/project.json +++ b/test/Microsoft.AspNet.Http.Extensions.Tests/project.json @@ -2,7 +2,7 @@ "dependencies": { "Microsoft.AspNet.Http": "1.0.0-*", "Microsoft.AspNet.Http.Extensions": "1.0.0-*", - "Microsoft.AspNet.HttpFeature": "1.0.0-*", + "Microsoft.AspNet.Http.Interfaces": "1.0.0-*", "Microsoft.AspNet.Http.Core": "1.0.0-*", "xunit.runner.kre": "1.0.0-*" }, diff --git a/test/Microsoft.AspNet.Http.Tests/MapPathMiddlewareTests.cs b/test/Microsoft.AspNet.Http.Tests/MapPathMiddlewareTests.cs index e49637ed56..ae1a7c5985 100644 --- a/test/Microsoft.AspNet.Http.Tests/MapPathMiddlewareTests.cs +++ b/test/Microsoft.AspNet.Http.Tests/MapPathMiddlewareTests.cs @@ -4,7 +4,7 @@ using System; using System.Threading.Tasks; using Microsoft.AspNet.Http; -using Microsoft.AspNet.HttpFeature; +using Microsoft.AspNet.Http.Interfaces; using Microsoft.AspNet.Http.Core; using Shouldly; using Xunit; diff --git a/test/Microsoft.AspNet.Http.Tests/MapPredicateMiddlewareTests.cs b/test/Microsoft.AspNet.Http.Tests/MapPredicateMiddlewareTests.cs index b0ef791a3e..bc94b5717c 100644 --- a/test/Microsoft.AspNet.Http.Tests/MapPredicateMiddlewareTests.cs +++ b/test/Microsoft.AspNet.Http.Tests/MapPredicateMiddlewareTests.cs @@ -5,7 +5,7 @@ using System; using System.Collections.Generic; using System.Threading.Tasks; using Microsoft.AspNet.Http; -using Microsoft.AspNet.HttpFeature; +using Microsoft.AspNet.Http.Interfaces; using Microsoft.AspNet.Http.Core; using Xunit; diff --git a/test/Microsoft.AspNet.Http.Tests/project.json b/test/Microsoft.AspNet.Http.Tests/project.json index 134db2aeae..f6549fc724 100644 --- a/test/Microsoft.AspNet.Http.Tests/project.json +++ b/test/Microsoft.AspNet.Http.Tests/project.json @@ -1,7 +1,7 @@ { "dependencies": { "Microsoft.AspNet.Http": "1.0.0-*", - "Microsoft.AspNet.HttpFeature": "1.0.0-*", + "Microsoft.AspNet.Http.Interfaces": "1.0.0-*", "Microsoft.AspNet.Http.Core": "1.0.0-*", "Microsoft.AspNet.Testing": "1.0.0-*", "xunit.runner.kre": "1.0.0-*" diff --git a/test/Microsoft.AspNet.Owin.Tests/OwinFeatureCollectionTests.cs b/test/Microsoft.AspNet.Owin.Tests/OwinFeatureCollectionTests.cs index 663972150f..93becf8a60 100644 --- a/test/Microsoft.AspNet.Owin.Tests/OwinFeatureCollectionTests.cs +++ b/test/Microsoft.AspNet.Owin.Tests/OwinFeatureCollectionTests.cs @@ -4,7 +4,7 @@ using System.Collections.Generic; using System.Linq; using Microsoft.AspNet.FeatureModel; -using Microsoft.AspNet.HttpFeature; +using Microsoft.AspNet.Http.Interfaces; using Xunit; namespace Microsoft.AspNet.Owin diff --git a/test/Microsoft.AspNet.Owin.Tests/project.json b/test/Microsoft.AspNet.Owin.Tests/project.json index 4725d69b82..f0caf021ab 100644 --- a/test/Microsoft.AspNet.Owin.Tests/project.json +++ b/test/Microsoft.AspNet.Owin.Tests/project.json @@ -2,7 +2,7 @@ "dependencies": { "Microsoft.AspNet.FeatureModel": "1.0.0-*", "Microsoft.AspNet.Http": "1.0.0-*", - "Microsoft.AspNet.HttpFeature": "1.0.0-*", + "Microsoft.AspNet.Http.Interfaces": "1.0.0-*", "Microsoft.AspNet.Owin": "1.0.0-*", "Microsoft.AspNet.Http.Core": "1.0.0-*", "xunit.runner.kre": "1.0.0-*" From dceba03f4a3b9264df49643953d473b149d856b5 Mon Sep 17 00:00:00 2001 From: David Fowler Date: Mon, 19 Jan 2015 01:43:09 -0800 Subject: [PATCH 11/16] Removed unused dependencies #173 --- src/Microsoft.AspNet.FeatureModel/project.json | 1 - src/Microsoft.AspNet.Owin/project.json | 3 +-- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/src/Microsoft.AspNet.FeatureModel/project.json b/src/Microsoft.AspNet.FeatureModel/project.json index 6845cd91a4..72c7319d68 100644 --- a/src/Microsoft.AspNet.FeatureModel/project.json +++ b/src/Microsoft.AspNet.FeatureModel/project.json @@ -2,7 +2,6 @@ "version": "1.0.0-*", "description": "ASP.NET 5 HTTP feature infrastructure.", "dependencies": { - "Microsoft.Framework.Runtime.Interfaces": "1.0.0-*" }, "frameworks": { "aspnet50": {}, diff --git a/src/Microsoft.AspNet.Owin/project.json b/src/Microsoft.AspNet.Owin/project.json index ffa82b3a9e..19cc720380 100644 --- a/src/Microsoft.AspNet.Owin/project.json +++ b/src/Microsoft.AspNet.Owin/project.json @@ -4,8 +4,7 @@ "dependencies": { "Microsoft.AspNet.Http": "1.0.0-*", "Microsoft.AspNet.FeatureModel": "1.0.0-*", - "Microsoft.AspNet.Http.Core": "1.0.0-*", - "Microsoft.AspNet.Http.Interfaces": { "version": "1.0.0-*", "type": "build" } + "Microsoft.AspNet.Http.Core": "1.0.0-*" }, "frameworks": { "aspnet50": { }, From 236801ee6ec0c6d95822a93c8ab8f4feb367fca3 Mon Sep 17 00:00:00 2001 From: Pranav K Date: Tue, 20 Jan 2015 01:31:48 -0800 Subject: [PATCH 12/16] Updating build.cmd and build.sh to use dotnetsdk --- build.cmd | 6 +++--- build.sh | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/build.cmd b/build.cmd index 86ca5bbbf1..c8041fdd9d 100644 --- a/build.cmd +++ b/build.cmd @@ -20,9 +20,9 @@ IF EXIST packages\KoreBuild goto run .nuget\NuGet.exe install Sake -version 0.2 -o packages -ExcludeVersion IF "%SKIP_KRE_INSTALL%"=="1" goto run -CALL packages\KoreBuild\build\kvm upgrade -runtime CLR -x86 -CALL packages\KoreBuild\build\kvm install default -runtime CoreCLR -x86 +CALL packages\KoreBuild\build\dotnetsdk upgrade -runtime CLR -x86 +CALL packages\KoreBuild\build\dotnetsdk install default -runtime CoreCLR -x86 :run -CALL packages\KoreBuild\build\kvm use default -runtime CLR -x86 +CALL packages\KoreBuild\build\dotnetsdk use default -runtime CLR -x86 packages\Sake\tools\Sake.exe -I packages\KoreBuild\build -f makefile.shade %* diff --git a/build.sh b/build.sh index c7873ef58e..3f3c731c04 100755 --- a/build.sh +++ b/build.sh @@ -28,11 +28,11 @@ if test ! -d packages/KoreBuild; then fi if ! type k > /dev/null 2>&1; then - source packages/KoreBuild/build/kvm.sh + source setup/dotnetsdk.sh fi if ! type k > /dev/null 2>&1; then - kvm upgrade + dotnetsdk upgrade fi mono packages/Sake/tools/Sake.exe -I packages/KoreBuild/build -f makefile.shade "$@" From 382c3af065fd06c71686d1102723ac60fc003e7b Mon Sep 17 00:00:00 2001 From: Pranav K Date: Tue, 20 Jan 2015 01:36:08 -0800 Subject: [PATCH 13/16] Updating build.cmd and build.sh to use dotnetsdk --- build.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.sh b/build.sh index 3f3c731c04..350d7e389a 100755 --- a/build.sh +++ b/build.sh @@ -28,7 +28,7 @@ if test ! -d packages/KoreBuild; then fi if ! type k > /dev/null 2>&1; then - source setup/dotnetsdk.sh + source packages/KoreBuild/build/dotnetsdk.sh fi if ! type k > /dev/null 2>&1; then From 93c5b0f2c8fe7f5a14eab7475b916813271f453f Mon Sep 17 00:00:00 2001 From: Wei Wang Date: Tue, 20 Jan 2015 18:19:02 -0800 Subject: [PATCH 14/16] Rename SKIP_KRE_INSTALL to SKIP_DOTNET_INSTALL --- build.cmd | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.cmd b/build.cmd index c8041fdd9d..220a1ff561 100644 --- a/build.cmd +++ b/build.cmd @@ -19,7 +19,7 @@ IF EXIST packages\KoreBuild goto run .nuget\NuGet.exe install KoreBuild -ExcludeVersion -o packages -nocache -pre .nuget\NuGet.exe install Sake -version 0.2 -o packages -ExcludeVersion -IF "%SKIP_KRE_INSTALL%"=="1" goto run +IF "%SKIP_DOTNET_INSTALL%"=="1" goto run CALL packages\KoreBuild\build\dotnetsdk upgrade -runtime CLR -x86 CALL packages\KoreBuild\build\dotnetsdk install default -runtime CoreCLR -x86 From 97c9f8f479dc8071b7a3b402258ee50af38d51d8 Mon Sep 17 00:00:00 2001 From: Suhas Joshi Date: Wed, 21 Jan 2015 15:49:10 -0800 Subject: [PATCH 15/16] Updating to release NuGet.config --- NuGet.Config | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/NuGet.Config b/NuGet.Config index f41e9c631d..2d3b0cb857 100644 --- a/NuGet.Config +++ b/NuGet.Config @@ -1,7 +1,7 @@  - + From 15a51e423f52457159064333f238cf425a6340d2 Mon Sep 17 00:00:00 2001 From: Chris Ross Date: Thu, 22 Jan 2015 09:41:29 -0800 Subject: [PATCH 16/16] #175 - Decode multipart headers as UTF-8. --- .../BufferedReadStream.cs | 41 ++++--- .../MultipartReaderTests.cs | 111 ++++++++++++++++++ 2 files changed, 131 insertions(+), 21 deletions(-) diff --git a/src/Microsoft.AspNet.WebUtilities/BufferedReadStream.cs b/src/Microsoft.AspNet.WebUtilities/BufferedReadStream.cs index 944f126b93..847873e2d1 100644 --- a/src/Microsoft.AspNet.WebUtilities/BufferedReadStream.cs +++ b/src/Microsoft.AspNet.WebUtilities/BufferedReadStream.cs @@ -11,8 +11,8 @@ namespace Microsoft.AspNet.WebUtilities { internal class BufferedReadStream : Stream { - private const char CR = '\r'; - private const char LF = '\n'; + private const byte CR = (byte)'\r'; + private const byte LF = (byte)'\n'; private readonly Stream _inner; private readonly byte[] _buffer; @@ -310,8 +310,9 @@ namespace Microsoft.AspNet.WebUtilities public string ReadLine(int lengthLimit) { CheckDisposed(); - StringBuilder builder = new StringBuilder(); + var builder = new MemoryStream(200); bool foundCR = false, foundCRLF = false; + while (!foundCRLF && EnsureBuffered()) { if (builder.Length > lengthLimit) @@ -321,19 +322,15 @@ namespace Microsoft.AspNet.WebUtilities ProcessLineChar(builder, ref foundCR, ref foundCRLF); } - if (foundCRLF) - { - return builder.ToString(0, builder.Length - 2); // Drop the CRLF - } - // Stream ended with no CRLF. - return builder.ToString(); + return DecodeLine(builder, foundCRLF); } public async Task ReadLineAsync(int lengthLimit, CancellationToken cancellationToken) { CheckDisposed(); - StringBuilder builder = new StringBuilder(); + var builder = new MemoryStream(200); bool foundCR = false, foundCRLF = false; + while (!foundCRLF && await EnsureBufferedAsync(cancellationToken)) { if (builder.Length > lengthLimit) @@ -344,25 +341,20 @@ namespace Microsoft.AspNet.WebUtilities ProcessLineChar(builder, ref foundCR, ref foundCRLF); } - if (foundCRLF) - { - return builder.ToString(0, builder.Length - 2); // Drop the CRLF - } - // Stream ended with no CRLF. - return builder.ToString(); + return DecodeLine(builder, foundCRLF); } - private void ProcessLineChar(StringBuilder builder, ref bool foundCR, ref bool foundCRLF) + private void ProcessLineChar(MemoryStream builder, ref bool foundCR, ref bool foundCRLF) { - char ch = (char)_buffer[_bufferOffset]; // TODO: Encoding enforcement - builder.Append(ch); + var b = _buffer[_bufferOffset]; + builder.WriteByte(b); _bufferOffset++; _bufferCount--; - if (ch == CR) + if (b == CR) { foundCR = true; } - else if (ch == LF) + else if (b == LF) { if (foundCR) { @@ -375,6 +367,13 @@ namespace Microsoft.AspNet.WebUtilities } } + private string DecodeLine(MemoryStream builder, bool foundCRLF) + { + // Drop the final CRLF, if any + var length = foundCRLF ? builder.Length - 2 : builder.Length; + return Encoding.UTF8.GetString(builder.ToArray(), 0, (int)length); + } + private void CheckDisposed() { if (_disposed) diff --git a/test/Microsoft.AspNet.WebUtilities.Tests/MultipartReaderTests.cs b/test/Microsoft.AspNet.WebUtilities.Tests/MultipartReaderTests.cs index a642511a86..49a5abedd0 100644 --- a/test/Microsoft.AspNet.WebUtilities.Tests/MultipartReaderTests.cs +++ b/test/Microsoft.AspNet.WebUtilities.Tests/MultipartReaderTests.cs @@ -43,6 +43,19 @@ Content-Type: text/plain Content of a.txt. +--9051914041544843365972754266-- +"; + private const string TwoPartBodyWithUnicodeFileName = +@"--9051914041544843365972754266 +Content-Disposition: form-data; name=""text"" + +text default +--9051914041544843365972754266 +Content-Disposition: form-data; name=""file1""; filename=""a色.txt"" +Content-Type: text/plain + +Content of a.txt. + --9051914041544843365972754266-- "; private const string ThreePartBody = @@ -147,6 +160,32 @@ Content-Type: text/html Assert.Null(await reader.ReadNextSectionAsync()); } + [Fact] + public async Task MutipartReader_ReadTwoPartBodyWithUnicodeFileName_Success() + { + var stream = MakeStream(TwoPartBodyWithUnicodeFileName); + var reader = new MultipartReader(Boundary, stream); + + var section = await reader.ReadNextSectionAsync(); + Assert.NotNull(section); + Assert.Equal(1, section.Headers.Count); + Assert.Equal("form-data; name=\"text\"", section.Headers["Content-Disposition"][0]); + var buffer = new MemoryStream(); + await section.Body.CopyToAsync(buffer); + Assert.Equal("text default", Encoding.ASCII.GetString(buffer.ToArray())); + + section = await reader.ReadNextSectionAsync(); + Assert.NotNull(section); + Assert.Equal(2, section.Headers.Count); + Assert.Equal("form-data; name=\"file1\"; filename=\"a色.txt\"", section.Headers["Content-Disposition"][0]); + Assert.Equal("text/plain", section.Headers["Content-Type"][0]); + buffer = new MemoryStream(); + await section.Body.CopyToAsync(buffer); + Assert.Equal("Content of a.txt.\r\n", Encoding.ASCII.GetString(buffer.ToArray())); + + Assert.Null(await reader.ReadNextSectionAsync()); + } + [Fact] public async Task MutipartReader_ThreePartBody_Success() { @@ -181,5 +220,77 @@ Content-Type: text/html Assert.Null(await reader.ReadNextSectionAsync()); } + + [Fact] + public async Task MutipartReader_ReadInvalidUtf8Header_ReplacementCharacters() + { + var body1 = +@"--9051914041544843365972754266 +Content-Disposition: form-data; name=""text"" filename=""a"; + + var body2 = +@".txt"" + +text default +--9051914041544843365972754266-- +"; + var stream = new MemoryStream(); + var bytes = Encoding.UTF8.GetBytes(body1); + stream.Write(bytes, 0, bytes.Length); + + // Write an invalid utf-8 segment in the middle + stream.Write(new byte[] { 0xC1, 0x21 }, 0, 2); + + bytes = Encoding.UTF8.GetBytes(body2); + stream.Write(bytes, 0, bytes.Length); + stream.Seek(0, SeekOrigin.Begin); + var reader = new MultipartReader(Boundary, stream); + + var section = await reader.ReadNextSectionAsync(); + Assert.NotNull(section); + Assert.Equal(1, section.Headers.Count); + Assert.Equal("form-data; name=\"text\" filename=\"a\uFFFD!.txt\"", section.Headers["Content-Disposition"][0]); + var buffer = new MemoryStream(); + await section.Body.CopyToAsync(buffer); + Assert.Equal("text default", Encoding.ASCII.GetString(buffer.ToArray())); + + Assert.Null(await reader.ReadNextSectionAsync()); + } + + [Fact] + public async Task MutipartReader_ReadInvalidUtf8SurrogateHeader_ReplacementCharacters() + { + var body1 = +@"--9051914041544843365972754266 +Content-Disposition: form-data; name=""text"" filename=""a"; + + var body2 = +@".txt"" + +text default +--9051914041544843365972754266-- +"; + var stream = new MemoryStream(); + var bytes = Encoding.UTF8.GetBytes(body1); + stream.Write(bytes, 0, bytes.Length); + + // Write an invalid utf-8 segment in the middle + stream.Write(new byte[] { 0xED, 0xA0, 85 }, 0, 3); + + bytes = Encoding.UTF8.GetBytes(body2); + stream.Write(bytes, 0, bytes.Length); + stream.Seek(0, SeekOrigin.Begin); + var reader = new MultipartReader(Boundary, stream); + + var section = await reader.ReadNextSectionAsync(); + Assert.NotNull(section); + Assert.Equal(1, section.Headers.Count); + Assert.Equal("form-data; name=\"text\" filename=\"a\uFFFDU.txt\"", section.Headers["Content-Disposition"][0]); + var buffer = new MemoryStream(); + await section.Body.CopyToAsync(buffer); + Assert.Equal("text default", Encoding.ASCII.GetString(buffer.ToArray())); + + Assert.Null(await reader.ReadNextSectionAsync()); + } } } \ No newline at end of file