diff --git a/src/Microsoft.Extensions.Primitives/InplaceStringBuilder.cs b/src/Microsoft.Extensions.Primitives/InplaceStringBuilder.cs new file mode 100644 index 00000000000..bf1d0cd9baa --- /dev/null +++ b/src/Microsoft.Extensions.Primitives/InplaceStringBuilder.cs @@ -0,0 +1,82 @@ +// Copyright (c) .NET Foundation. 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.Runtime.CompilerServices; + +namespace Microsoft.Extensions.Primitives +{ + [DebuggerDisplay("Value = {_value}")] + public struct InplaceStringBuilder + { + private int _capacity; + private int _offset; + private bool _writing; + private string _value; + + public InplaceStringBuilder(int capacity) : this() + { + _capacity = capacity; + } + + public int Capacity + { + get { return _capacity; } + set + { + if (value < 0) + { + throw new ArgumentOutOfRangeException(nameof(value)); + } + if (_writing) + { + throw new InvalidOperationException("Cannot change capacity after write started."); + } + _capacity = value; + } + } + + public unsafe void Append(string s) + { + EnsureCapacity(s.Length); + fixed (char* destination = _value) + fixed (char* source = s) + { + //TODO: https://github.com/aspnet/Common/issues/158 + Unsafe.CopyBlock(destination + _offset, source, (uint)s.Length * 2); + _offset += s.Length; + } + } + public unsafe void Append(char c) + { + EnsureCapacity(1); + fixed (char* destination = _value) + { + destination[_offset++] = c; + } + } + + private void EnsureCapacity(int length) + { + if (_value == null) + { + _writing = true; + _value = new string('\0', _capacity); + } + if (_offset + length > _capacity) + { + throw new InvalidOperationException($"Not enough capacity to write '{length}' characters, only '{_capacity - _offset}' left."); + } + } + + public override string ToString() + { + if (_offset != _capacity) + { + throw new InvalidOperationException($"Entire reserved capacity was not used. Capacity: '{_capacity}', written '{_offset}'."); + } + return _value; + } + } +} diff --git a/src/Microsoft.Extensions.Primitives/project.json b/src/Microsoft.Extensions.Primitives/project.json index eae12cfe4d7..b7e5d8fb63f 100644 --- a/src/Microsoft.Extensions.Primitives/project.json +++ b/src/Microsoft.Extensions.Primitives/project.json @@ -16,13 +16,16 @@ "nowarn": [ "CS1591" ], - "xmlDoc": true + "xmlDoc": true, + "allowUnsafe": true }, "dependencies": { "Microsoft.Extensions.HashCodeCombiner.Sources": { "type": "build", "version": "1.1.0-*" - } + }, + "System.Diagnostics.Debug": "4.0.11", + "System.Runtime.CompilerServices.Unsafe": "4.0.0" }, "frameworks": { "netstandard1.0": { diff --git a/test/Microsoft.Extensions.Primitives.Tests/InplaceStringBuilderTest.cs b/test/Microsoft.Extensions.Primitives.Tests/InplaceStringBuilderTest.cs new file mode 100644 index 00000000000..9c9542e406e --- /dev/null +++ b/test/Microsoft.Extensions.Primitives.Tests/InplaceStringBuilderTest.cs @@ -0,0 +1,52 @@ +using System; +using Microsoft.Extensions.Primitives; +using Xunit; + +namespace Microsoft.AspNetCore.Http.Tests.Internal +{ + public class InplaceStringBuilderTest + { + [Fact] + public void ToString_ReturnsStringWithAllAppendedValues() + { + var s1 = "123"; + var c1 = '4'; + var s2 = "56789"; + + var formatter = new InplaceStringBuilder(); + formatter.Capacity += s1.Length + 1 + s2.Length; + formatter.Append(s1); + formatter.Append(c1); + formatter.Append(s2); + Assert.Equal("123456789", formatter.ToString()); + } + + [Fact] + public void Build_ThrowsIfNotEnoughWritten() + { + var formatter = new InplaceStringBuilder(5); + formatter.Append("123"); + var exception = Assert.Throws(() => formatter.ToString()); + Assert.Equal(exception.Message, "Entire reserved capacity was not used. Capacity: '5', written '3'."); + } + + [Fact] + public void Capacity_ThrowsIfAppendWasCalled() + { + var formatter = new InplaceStringBuilder(3); + formatter.Append("123"); + + var exception = Assert.Throws(() => formatter.Capacity = 5); + Assert.Equal(exception.Message, "Cannot change capacity after write started."); + } + + [Fact] + public void Append_ThrowsIfNotEnoughSpace() + { + var formatter = new InplaceStringBuilder(1); + + var exception = Assert.Throws(() => formatter.Append("123")); + Assert.Equal(exception.Message, "Not enough capacity to write '3' characters, only '1' left."); + } + } +}