diff --git a/src/Microsoft.Net.Http.Headers/HeaderUtilities.cs b/src/Microsoft.Net.Http.Headers/HeaderUtilities.cs index e42240a1dd..630e02b93f 100644 --- a/src/Microsoft.Net.Http.Headers/HeaderUtilities.cs +++ b/src/Microsoft.Net.Http.Headers/HeaderUtilities.cs @@ -228,5 +228,13 @@ namespace Microsoft.Net.Http.Headers } return input; } + + internal static void ThrowIfReadOnly(bool isReadOnly) + { + if (isReadOnly) + { + throw new InvalidOperationException("The object cannot be modified because it is read-only."); + } + } } } diff --git a/src/Microsoft.Net.Http.Headers/MediaTypeHeaderValue.cs b/src/Microsoft.Net.Http.Headers/MediaTypeHeaderValue.cs index 173bf7c404..968eb4c6cc 100644 --- a/src/Microsoft.Net.Http.Headers/MediaTypeHeaderValue.cs +++ b/src/Microsoft.Net.Http.Headers/MediaTypeHeaderValue.cs @@ -5,6 +5,7 @@ using System; using System.Collections.Generic; using System.Diagnostics.Contracts; using System.Globalization; +using System.Linq; using System.Text; namespace Microsoft.Net.Http.Headers @@ -19,9 +20,10 @@ namespace Microsoft.Net.Http.Headers 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; + // Use a collection instead of a dictionary since we may have multiple parameters with the same name. + private ObjectCollection _parameters; private string _mediaType; + private bool _isReadOnly; private MediaTypeHeaderValue() { @@ -48,6 +50,7 @@ namespace Microsoft.Net.Http.Headers } set { + HeaderUtilities.ThrowIfReadOnly(IsReadOnly); // 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); @@ -93,6 +96,7 @@ namespace Microsoft.Net.Http.Headers } set { + HeaderUtilities.ThrowIfReadOnly(IsReadOnly); if (value == null) { Charset = null; @@ -112,6 +116,7 @@ namespace Microsoft.Net.Http.Headers } set { + HeaderUtilities.ThrowIfReadOnly(IsReadOnly); var boundaryParameter = NameValueHeaderValue.Find(_parameters, BoundaryString); if (string.IsNullOrEmpty(value)) { @@ -141,7 +146,14 @@ namespace Microsoft.Net.Http.Headers { if (_parameters == null) { - _parameters = new ObjectCollection(); + if (IsReadOnly) + { + _parameters = ObjectCollection.EmptyReadOnlyCollection; + } + else + { + _parameters = new ObjectCollection(); + } } return _parameters; } @@ -158,6 +170,7 @@ namespace Microsoft.Net.Http.Headers get { return _mediaType; } set { + HeaderUtilities.ThrowIfReadOnly(IsReadOnly); CheckMediaTypeFormat(value, "value"); _mediaType = value; } @@ -201,6 +214,11 @@ namespace Microsoft.Net.Http.Headers } } + public bool IsReadOnly + { + get { return _isReadOnly; } + } + public bool IsSubsetOf(MediaTypeHeaderValue otherMediaType) { if (otherMediaType == null) @@ -253,22 +271,47 @@ namespace Microsoft.Net.Http.Headers /// while avoiding the cost of revalidating the components. /// /// A deep copy. - public MediaTypeHeaderValue Clone() + public MediaTypeHeaderValue Copy() { + if (IsReadOnly) + { + return this; + } + var other = new MediaTypeHeaderValue(); other._mediaType = _mediaType; if (_parameters != null) { - other._parameters = new ObjectCollection(); - foreach (var pair in _parameters) - { - other._parameters.Add(pair.Clone()); - } + other._parameters = new ObjectCollection( + _parameters.Select(item => item.Copy())); } return other; } + /// + /// Performs a deep copy of this object and all of it's NameValueHeaderValue sub components, + /// while avoiding the cost of revalidating the components. This copy is read-only. + /// + /// A deep, read-only, copy. + public MediaTypeHeaderValue CopyAsReadOnly() + { + if (IsReadOnly) + { + return this; + } + + var other = new MediaTypeHeaderValue(); + other._mediaType = _mediaType; + if (_parameters != null) + { + other._parameters = new ObjectCollection( + _parameters.Select(item => item.CopyAsReadOnly()), isReadOnly: true); + } + other._isReadOnly = true; + return other; + } + public override string ToString() { return _mediaType + NameValueHeaderValue.ToString(_parameters, ';', true); diff --git a/src/Microsoft.Net.Http.Headers/NameValueHeaderValue.cs b/src/Microsoft.Net.Http.Headers/NameValueHeaderValue.cs index 08a08f96ef..a91b98e358 100644 --- a/src/Microsoft.Net.Http.Headers/NameValueHeaderValue.cs +++ b/src/Microsoft.Net.Http.Headers/NameValueHeaderValue.cs @@ -20,6 +20,7 @@ namespace Microsoft.Net.Http.Headers private string _name; private string _value; + private bool _isReadOnly; private NameValueHeaderValue() { @@ -49,17 +50,25 @@ namespace Microsoft.Net.Http.Headers get { return _value; } set { + HeaderUtilities.ThrowIfReadOnly(IsReadOnly); CheckValueFormat(value); _value = value; } } + public bool IsReadOnly { get { return _isReadOnly; } } + /// /// Provides a copy of this object without the cost of re-validating the values. /// /// A copy. - public NameValueHeaderValue Clone() + public NameValueHeaderValue Copy() { + if (IsReadOnly) + { + return this; + } + return new NameValueHeaderValue() { _name = _name, @@ -67,6 +76,21 @@ namespace Microsoft.Net.Http.Headers }; } + public NameValueHeaderValue CopyAsReadOnly() + { + if (IsReadOnly) + { + return this; + } + + return new NameValueHeaderValue() + { + _name = _name, + _value = _value, + _isReadOnly = true + }; + } + public override int GetHashCode() { Contract.Assert(_name != null); diff --git a/src/Microsoft.Net.Http.Headers/ObjectCollection.cs b/src/Microsoft.Net.Http.Headers/ObjectCollection.cs index 90e7718c4c..24237b6ff1 100644 --- a/src/Microsoft.Net.Http.Headers/ObjectCollection.cs +++ b/src/Microsoft.Net.Http.Headers/ObjectCollection.cs @@ -2,6 +2,8 @@ // 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.Collections.ObjectModel; namespace Microsoft.Net.Http.Headers @@ -10,40 +12,74 @@ namespace Microsoft.Net.Http.Headers // 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 + internal class ObjectCollection : ICollection where T : class { - private static readonly Action DefaultValidator = CheckNotNull; + internal static readonly Action DefaultValidator = CheckNotNull; + internal static readonly ObjectCollection EmptyReadOnlyCollection + = new ObjectCollection(DefaultValidator, isReadOnly: true); - private Action _validator; + private readonly Collection _collection = new Collection(); + private readonly Action _validator; + private readonly bool _isReadOnly; public ObjectCollection() : this(DefaultValidator) { } - public ObjectCollection(Action validator) + public ObjectCollection(Action validator, bool isReadOnly = false) { _validator = validator; + _isReadOnly = isReadOnly; } - protected override void InsertItem(int index, T item) + public ObjectCollection(IEnumerable other, bool isReadOnly = false) { - if (_validator != null) + _validator = DefaultValidator; + foreach (T item in other) { - _validator(item); + Add(item); } - base.InsertItem(index, item); + _isReadOnly = isReadOnly; } - protected override void SetItem(int index, T item) + public int Count { - if (_validator != null) - { - _validator(item); - } - base.SetItem(index, item); + get { return _collection.Count; } } + public bool IsReadOnly + { + get { return _isReadOnly; } + } + + public void Add(T item) + { + HeaderUtilities.ThrowIfReadOnly(IsReadOnly); + _validator(item); + _collection.Add(item); + } + + public bool Remove(T item) + { + HeaderUtilities.ThrowIfReadOnly(IsReadOnly); + return _collection.Remove(item); + } + + public void Clear() + { + HeaderUtilities.ThrowIfReadOnly(IsReadOnly); + _collection.Clear(); + } + + public bool Contains(T item) => _collection.Contains(item); + + public void CopyTo(T[] array, int arrayIndex) => _collection.CopyTo(array, arrayIndex); + + public IEnumerator GetEnumerator() => _collection.GetEnumerator(); + + IEnumerator IEnumerable.GetEnumerator() => _collection.GetEnumerator(); + private static void CheckNotNull(T item) { // null values cannot be added to the collection. diff --git a/src/Microsoft.Net.Http.Headers/project.json b/src/Microsoft.Net.Http.Headers/project.json index 15f82024e2..fcbada8643 100644 --- a/src/Microsoft.Net.Http.Headers/project.json +++ b/src/Microsoft.Net.Http.Headers/project.json @@ -12,6 +12,7 @@ "System.Diagnostics.Contracts": "4.0.0-beta-*", "System.Globalization": "4.0.10-beta-*", "System.Globalization.Extensions": "4.0.0-beta-*", + "System.Linq": "4.0.0-beta-*", "System.Text.Encoding": "4.0.10-beta-*", "System.Runtime": "4.0.20-beta-*" } diff --git a/test/Microsoft.Framework.WebEncoders.Tests/project.json b/test/Microsoft.Framework.WebEncoders.Tests/project.json index b0ba4aa9fe..e44f880a72 100644 --- a/test/Microsoft.Framework.WebEncoders.Tests/project.json +++ b/test/Microsoft.Framework.WebEncoders.Tests/project.json @@ -20,5 +20,5 @@ } } }, - "resources": "..\\..\\unicode\\UnicodeData.txt" + "resource": "..\\..\\unicode\\UnicodeData.txt" } diff --git a/test/Microsoft.Net.Http.Headers.Tests/MediaTypeHeaderValueTest.cs b/test/Microsoft.Net.Http.Headers.Tests/MediaTypeHeaderValueTest.cs index 92ec8f9726..cfd389e4d4 100644 --- a/test/Microsoft.Net.Http.Headers.Tests/MediaTypeHeaderValueTest.cs +++ b/test/Microsoft.Net.Http.Headers.Tests/MediaTypeHeaderValueTest.cs @@ -65,10 +65,10 @@ namespace Microsoft.Net.Http.Headers } [Fact] - public void Clone_SimpleMediaType_Copied() + public void Copy_SimpleMediaType_Copied() { var mediaType0 = new MediaTypeHeaderValue("text/plain"); - var mediaType1 = mediaType0.Clone(); + var mediaType1 = mediaType0.Copy(); Assert.NotSame(mediaType0, mediaType1); Assert.Same(mediaType0.MediaType, mediaType1.MediaType); Assert.NotSame(mediaType0.Parameters, mediaType1.Parameters); @@ -76,11 +76,26 @@ namespace Microsoft.Net.Http.Headers } [Fact] - public void Clone_WithParameters_Copied() + public void CopyAsReadOnly_SimpleMediaType_CopiedAndReadOnly() + { + var mediaType0 = new MediaTypeHeaderValue("text/plain"); + var mediaType1 = mediaType0.CopyAsReadOnly(); + Assert.NotSame(mediaType0, mediaType1); + Assert.Same(mediaType0.MediaType, mediaType1.MediaType); + Assert.NotSame(mediaType0.Parameters, mediaType1.Parameters); + Assert.Equal(mediaType0.Parameters.Count, mediaType1.Parameters.Count); + + Assert.False(mediaType0.IsReadOnly); + Assert.True(mediaType1.IsReadOnly); + Assert.Throws(() => { mediaType1.MediaType = "some/value"; }); + } + + [Fact] + public void Copy_WithParameters_Copied() { var mediaType0 = new MediaTypeHeaderValue("text/plain"); mediaType0.Parameters.Add(new NameValueHeaderValue("name", "value")); - var mediaType1 = mediaType0.Clone(); + var mediaType1 = mediaType0.Copy(); Assert.NotSame(mediaType0, mediaType1); Assert.Same(mediaType0.MediaType, mediaType1.MediaType); Assert.NotSame(mediaType0.Parameters, mediaType1.Parameters); @@ -92,6 +107,34 @@ namespace Microsoft.Net.Http.Headers Assert.Same(pair0.Value, pair1.Value); } + [Fact] + public void CopyAsReadOnly_WithParameters_CopiedAndReadOnly() + { + var mediaType0 = new MediaTypeHeaderValue("text/plain"); + mediaType0.Parameters.Add(new NameValueHeaderValue("name", "value")); + var mediaType1 = mediaType0.CopyAsReadOnly(); + Assert.NotSame(mediaType0, mediaType1); + Assert.False(mediaType0.IsReadOnly); + Assert.True(mediaType1.IsReadOnly); + Assert.Same(mediaType0.MediaType, mediaType1.MediaType); + + Assert.NotSame(mediaType0.Parameters, mediaType1.Parameters); + Assert.False(mediaType0.Parameters.IsReadOnly); + Assert.True(mediaType1.Parameters.IsReadOnly); + Assert.Equal(mediaType0.Parameters.Count, mediaType1.Parameters.Count); + Assert.Throws(() => mediaType1.Parameters.Add(new NameValueHeaderValue("name"))); + Assert.Throws(() => mediaType1.Parameters.Remove(new NameValueHeaderValue("name"))); + Assert.Throws(() => mediaType1.Parameters.Clear()); + + var pair0 = mediaType0.Parameters.First(); + var pair1 = mediaType1.Parameters.First(); + Assert.NotSame(pair0, pair1); + Assert.False(pair0.IsReadOnly); + Assert.True(pair1.IsReadOnly); + Assert.Same(pair0.Name, pair1.Name); + Assert.Same(pair0.Value, pair1.Value); + } + [Fact] public void MediaType_SetAndGetMediaType_MatchExpectations() { diff --git a/test/Microsoft.Net.Http.Headers.Tests/NameValueHeaderValueTest.cs b/test/Microsoft.Net.Http.Headers.Tests/NameValueHeaderValueTest.cs index a1e1a935f9..a5bf8c9b66 100644 --- a/test/Microsoft.Net.Http.Headers.Tests/NameValueHeaderValueTest.cs +++ b/test/Microsoft.Net.Http.Headers.Tests/NameValueHeaderValueTest.cs @@ -60,29 +60,71 @@ namespace Microsoft.Net.Http.Headers } [Fact] - public void Clone_NameOnly_SuccesfullyCopied() + public void Copy_NameOnly_SuccesfullyCopied() { var pair0 = new NameValueHeaderValue("name"); - var pair1 = pair0.Clone(); + var pair1 = pair0.Copy(); Assert.NotSame(pair0, pair1); Assert.Same(pair0.Name, pair1.Name); Assert.Null(pair0.Value); Assert.Null(pair1.Value); + + // Change one value and verify the other is unchanged. + pair0.Value = "othervalue"; + Assert.Equal("othervalue", pair0.Value); + Assert.Equal(null, pair1.Value); } [Fact] - public void Clone_NameAndValue_SuccesfullyCopied() + public void CopyAsReadOnly_NameOnly_CopiedAndReadOnly() + { + var pair0 = new NameValueHeaderValue("name"); + var pair1 = pair0.CopyAsReadOnly(); + Assert.NotSame(pair0, pair1); + Assert.Same(pair0.Name, pair1.Name); + Assert.Null(pair0.Value); + Assert.Null(pair1.Value); + Assert.False(pair0.IsReadOnly); + Assert.True(pair1.IsReadOnly); + + // Change one value and verify the other is unchanged. + pair0.Value = "othervalue"; + Assert.Equal("othervalue", pair0.Value); + Assert.Equal(null, pair1.Value); + Assert.Throws(() => { pair1.Value = "othervalue"; }); + } + + [Fact] + public void Copy_NameAndValue_SuccesfullyCopied() { var pair0 = new NameValueHeaderValue("name", "value"); - var pair1 = pair0.Clone(); + var pair1 = pair0.Copy(); Assert.NotSame(pair0, pair1); Assert.Same(pair0.Name, pair1.Name); Assert.Same(pair0.Value, pair1.Value); // Change one value and verify the other is unchanged. - pair1.Value = "othervalue"; - Assert.Equal("value", pair0.Value); - Assert.Equal("othervalue", pair1.Value); + pair0.Value = "othervalue"; + Assert.Equal("othervalue", pair0.Value); + Assert.Equal("value", pair1.Value); + } + + [Fact] + public void CopyAsReadOnly_NameAndValue_CopiedAndReadOnly() + { + var pair0 = new NameValueHeaderValue("name", "value"); + var pair1 = pair0.CopyAsReadOnly(); + Assert.NotSame(pair0, pair1); + Assert.Same(pair0.Name, pair1.Name); + Assert.Same(pair0.Value, pair1.Value); + Assert.False(pair0.IsReadOnly); + Assert.True(pair1.IsReadOnly); + + // Change one value and verify the other is unchanged. + pair0.Value = "othervalue"; + Assert.Equal("othervalue", pair0.Value); + Assert.Equal("value", pair1.Value); + Assert.Throws(() => { pair1.Value = "othervalue"; }); } [Fact]