diff --git a/Solutions/Corvus.HighPerformance.Specs/Corvus.HighPerformance.Specs.csproj b/Solutions/Corvus.HighPerformance.Specs/Corvus.HighPerformance.Specs.csproj index 136ce1f..53a550b 100644 --- a/Solutions/Corvus.HighPerformance.Specs/Corvus.HighPerformance.Specs.csproj +++ b/Solutions/Corvus.HighPerformance.Specs/Corvus.HighPerformance.Specs.csproj @@ -55,8 +55,4 @@ - - - - diff --git a/Solutions/Corvus.HighPerformance.Specs/ValueStringBuilderFeatures/Utf8/AppendUtf8.feature b/Solutions/Corvus.HighPerformance.Specs/ValueStringBuilderFeatures/Utf8/AppendUtf8.feature new file mode 100644 index 0000000..633cf2c --- /dev/null +++ b/Solutions/Corvus.HighPerformance.Specs/ValueStringBuilderFeatures/Utf8/AppendUtf8.feature @@ -0,0 +1,61 @@ +Feature: Append content into a UTF-8 ValueStringBuilder + +Scenario Outline: Fits in available space + Given a UTF-8 ValueStringBuilder initialized with '' of length + And I append '' to the UTF-8 ValueStringBuilder + And I append '' to the UTF-8 ValueStringBuilder + When I get the string from the UTF-8 ValueStringBuilder + Then the UTF-8 ValueStringBuilder string should be '' + Examples: + | InitializationType | InitialLength | FirstValue | SecondValue | + | Span | 11 | Hello | World! | + | Capacity | 11 | Hello | World! | + | Span | 20 | Hello | World! | + | Capacity | 20 | Hello | World! | + +Scenario Outline: Grows + Given a UTF-8 ValueStringBuilder initialized with '' of length + And I append '' to the UTF-8 ValueStringBuilder + And I append '' to the UTF-8 ValueStringBuilder + When I get the string from the UTF-8 ValueStringBuilder via '' + Then the UTF-8 ValueStringBuilder string should be '' + Examples: + | InitializationType | InitialLength | FirstValue | SecondValue | GetStringMechanism | + | Span | 11 | Hello, world! | It is mighty fine to see you today. | CreateStringAndDispose | + | Capacity | 11 | Hello, world! | It is mighty fine to see you today. | CreateStringAndDispose | + | Span | 11 | Hello, world! | It is mighty fine to see you today. | RentedBuffer | + | Capacity | 11 | Hello, world! | It is mighty fine to see you today. | RentedBuffer | + +Scenario Outline: Append number + Given a UTF-8 ValueStringBuilder initialized with '' of length + And I append '' to the UTF-8 ValueStringBuilder + And I append the Int32 to the UTF-8 ValueStringBuilder + When I get the string from the UTF-8 ValueStringBuilder + Then the UTF-8 ValueStringBuilder string should be '' + Examples: + | InitializationType | InitialLength | FirstValue | SecondValue | + | Span | 13 | Hello, world! | 42 | + | Capacity | 13 | Hello, world! | 42 | + | Span | 15 | Hello, world! | 42 | + | Capacity | 15 | Hello, world! | 42 | + | Span | 13 | Hello, world! | -42 | + | Capacity | 13 | Hello, world! | -42 | + | Span | 15 | Hello, world! | -42 | + | Capacity | 15 | Hello, world! | -42 | + +Scenario Outline: Append number then string + Given a UTF-8 ValueStringBuilder initialized with '' of length + And I append the Int32 to the UTF-8 ValueStringBuilder + And I append '' to the UTF-8 ValueStringBuilder + When I get the string from the UTF-8 ValueStringBuilder + Then the UTF-8 ValueStringBuilder string should be '' + Examples: + | InitializationType | InitialLength | FirstValue | SecondValue | + | Span | 13 | 42 | Hello, world! | + | Capacity | 13 | 42 | Hello, world! | + | Span | 15 | 42 | Hello, world! | + | Capacity | 15 | 42 | Hello, world! | + | Span | 13 | -42 | Hello, world! | + | Capacity | 13 | -42 | Hello, world! | + | Span | 15 | -42 | Hello, world! | + | Capacity | 15 | -42 | Hello, world! | diff --git a/Solutions/Corvus.HighPerformance.Specs/ValueStringBuilderFeatures/Utf8/AsMemoryUtf8.feature b/Solutions/Corvus.HighPerformance.Specs/ValueStringBuilderFeatures/Utf8/AsMemoryUtf8.feature new file mode 100644 index 0000000..a19e560 --- /dev/null +++ b/Solutions/Corvus.HighPerformance.Specs/ValueStringBuilderFeatures/Utf8/AsMemoryUtf8.feature @@ -0,0 +1,67 @@ +Feature: Retrieve a Memory from a UTF-8 ValueStringBuilder + +Scenario Outline: Retrieve a Memory from a UTF-8 ValueStringBuilder + Given a UTF-8 ValueStringBuilder initialized with '' of length + And I append 'Hello' to the UTF-8 ValueStringBuilder + And I append 'World!' to the UTF-8 ValueStringBuilder + When I get the Memory from the UTF-8 ValueStringBuilder + Then the UTF-8 ValueStringBuilder string should be 'HelloWorld!' + Examples: + | InitializationType | InitialLength | + | Span | 10 | + | Capacity | 10 | + | Span | 11 | + | Capacity | 11 | + | Span | 20 | + | Capacity | 20 | + +Scenario Outline: Retrieve a Memory with a start position from a UTF-8 ValueStringBuilder + Given a UTF-8 ValueStringBuilder initialized with '' of length + And I append 'Hello' to the UTF-8 ValueStringBuilder + And I append 'World!' to the UTF-8 ValueStringBuilder + When I get the Memory starting at from the UTF-8 ValueStringBuilder + Then the UTF-8 ValueStringBuilder string should be '' + Examples: + | InitializationType | InitialLength | Start | ExpectedValue | + | Span | 10 | 0 | HelloWorld! | + | Capacity | 10 | 0 | HelloWorld! | + | Span | 20 | 0 | HelloWorld! | + | Capacity | 20 | 0 | HelloWorld! | + | Span | 10 | 3 | loWorld! | + | Capacity | 10 | 3 | loWorld! | + | Span | 20 | 3 | loWorld! | + | Capacity | 20 | 3 | loWorld! | + + +Scenario Outline: Retrieve a Memory with a start position and length from a UTF-8 ValueStringBuilder + Given a UTF-8 ValueStringBuilder initialized with '' of length + And I append 'Hello' to the UTF-8 ValueStringBuilder + And I append 'World!' to the UTF-8 ValueStringBuilder + When I get the Memory starting at with length from the UTF-8 ValueStringBuilder + Then the UTF-8 ValueStringBuilder string should be '' + Examples: + | InitializationType | InitialLength | Start | Length | ExpectedValue | + | Span | 10 | 0 | 11 | HelloWorld! | + | Capacity | 10 | 0 | 11 | HelloWorld! | + | Span | 20 | 0 | 11 | HelloWorld! | + | Capacity | 20 | 0 | 11 | HelloWorld! | + | Span | 10 | 0 | 0 | | + | Capacity | 10 | 0 | 0 | | + | Span | 20 | 0 | 0 | | + | Capacity | 20 | 0 | 0 | | + | Span | 10 | 5 | 0 | | + | Capacity | 10 | 5 | 0 | | + | Span | 20 | 5 | 0 | | + | Capacity | 20 | 5 | 0 | | + | Span | 10 | 3 | 8 | loWorld! | + | Capacity | 10 | 3 | 8 | loWorld! | + | Span | 20 | 3 | 8 | loWorld! | + | Capacity | 20 | 3 | 8 | loWorld! | + | Span | 10 | 3 | 3 | loW | + | Capacity | 10 | 3 | 3 | loW | + | Span | 20 | 3 | 3 | loW | + | Capacity | 20 | 3 | 3 | loW | + | Span | 10 | 10 | 1 | ! | + | Capacity | 10 | 10 | 1 | ! | + | Span | 20 | 10 | 1 | ! | + | Capacity | 20 | 10 | 1 | ! | diff --git a/Solutions/Corvus.HighPerformance.Specs/ValueStringBuilderFeatures/Utf8/AsSpanUtf8.feature b/Solutions/Corvus.HighPerformance.Specs/ValueStringBuilderFeatures/Utf8/AsSpanUtf8.feature new file mode 100644 index 0000000..34e76bc --- /dev/null +++ b/Solutions/Corvus.HighPerformance.Specs/ValueStringBuilderFeatures/Utf8/AsSpanUtf8.feature @@ -0,0 +1,67 @@ +Feature: Retrieve a Span from a UTF-8 ValueStringBuilder + +Scenario Outline: Retrieve a Span from a UTF-8 ValueStringBuilder + Given a UTF-8 ValueStringBuilder initialized with '' of length + And I append 'Hello' to the UTF-8 ValueStringBuilder + And I append 'World!' to the UTF-8 ValueStringBuilder + When I get the Span from the UTF-8 ValueStringBuilder + Then the UTF-8 ValueStringBuilder string should be 'HelloWorld!' + Examples: + | InitializationType | InitialLength | + | Span | 10 | + | Capacity | 10 | + | Span | 11 | + | Capacity | 11 | + | Span | 20 | + | Capacity | 20 | + +Scenario Outline: Retrieve a Span with a start position from a UTF-8 ValueStringBuilder + Given a UTF-8 ValueStringBuilder initialized with '' of length + And I append 'Hello' to the UTF-8 ValueStringBuilder + And I append 'World!' to the UTF-8 ValueStringBuilder + When I get the Span starting at from the UTF-8 ValueStringBuilder + Then the UTF-8 ValueStringBuilder string should be '' + Examples: + | InitializationType | InitialLength | Start | ExpectedValue | + | Span | 10 | 0 | HelloWorld! | + | Capacity | 10 | 0 | HelloWorld! | + | Span | 20 | 0 | HelloWorld! | + | Capacity | 20 | 0 | HelloWorld! | + | Span | 10 | 3 | loWorld! | + | Capacity | 10 | 3 | loWorld! | + | Span | 20 | 3 | loWorld! | + | Capacity | 20 | 3 | loWorld! | + + +Scenario Outline: Retrieve a Span with a start position and length from a UTF-8 ValueStringBuilder + Given a UTF-8 ValueStringBuilder initialized with '' of length + And I append 'Hello' to the UTF-8 ValueStringBuilder + And I append 'World!' to the UTF-8 ValueStringBuilder + When I get the Span starting at with length from the UTF-8 ValueStringBuilder + Then the UTF-8 ValueStringBuilder string should be '' + Examples: + | InitializationType | InitialLength | Start | Length | ExpectedValue | + | Span | 10 | 0 | 11 | HelloWorld! | + | Capacity | 10 | 0 | 11 | HelloWorld! | + | Span | 20 | 0 | 11 | HelloWorld! | + | Capacity | 20 | 0 | 11 | HelloWorld! | + | Span | 10 | 0 | 0 | | + | Capacity | 10 | 0 | 0 | | + | Span | 20 | 0 | 0 | | + | Capacity | 20 | 0 | 0 | | + | Span | 10 | 5 | 0 | | + | Capacity | 10 | 5 | 0 | | + | Span | 20 | 5 | 0 | | + | Capacity | 20 | 5 | 0 | | + | Span | 10 | 3 | 8 | loWorld! | + | Capacity | 10 | 3 | 8 | loWorld! | + | Span | 20 | 3 | 8 | loWorld! | + | Capacity | 20 | 3 | 8 | loWorld! | + | Span | 10 | 3 | 3 | loW | + | Capacity | 10 | 3 | 3 | loW | + | Span | 20 | 3 | 3 | loW | + | Capacity | 20 | 3 | 3 | loW | + | Span | 10 | 10 | 1 | ! | + | Capacity | 10 | 10 | 1 | ! | + | Span | 20 | 10 | 1 | ! | + | Capacity | 20 | 10 | 1 | ! | diff --git a/Solutions/Corvus.HighPerformance.Specs/ValueStringBuilderFeatures/Utf8/ReplaceUtf8.feature b/Solutions/Corvus.HighPerformance.Specs/ValueStringBuilderFeatures/Utf8/ReplaceUtf8.feature new file mode 100644 index 0000000..42e92b2 --- /dev/null +++ b/Solutions/Corvus.HighPerformance.Specs/ValueStringBuilderFeatures/Utf8/ReplaceUtf8.feature @@ -0,0 +1,197 @@ +Feature: Replace content in a UTF-8 ValueStringBuilder + +Scenario Outline: No change + Given a UTF-8 ValueStringBuilder initialized with '' of length 13 + And I append 'Hello, World!' to the UTF-8 ValueStringBuilder + And I replace 'notpresent' with 'notused' at index 0 with count 13 in the UTF-8 ValueStringBuilder + When I get the string from the UTF-8 ValueStringBuilder + Then the UTF-8 ValueStringBuilder string should be 'Hello, World!' + Examples: + | InitializationType | + | Span | + | Capacity | + +Scenario Outline: Shrinks + Given a UTF-8 ValueStringBuilder initialized with '' of length 23 + And I append 'Hello, World or wherever!' to the UTF-8 ValueStringBuilder + And I replace '' with '' at index with count in the UTF-8 ValueStringBuilder + When I get the string from the UTF-8 ValueStringBuilder + Then the UTF-8 ValueStringBuilder string should be '' + Examples: + | InitializationType | Find | Replacement | Start | Length | Result | + | Span | ell | i | 0 | 13 | Hio, World or wherever! | + | Capacity | ell | i | 0 | 13 | Hio, World or wherever! | + | Span | ell | i | 1 | 12 | Hio, World or wherever! | + | Capacity | ell | i | 1 | 12 | Hio, World or wherever! | + | Span | ell | i | 1 | 3 | Hio, World or wherever! | + | Capacity | ell | i | 1 | 3 | Hio, World or wherever! | + | Span | ell | i | 1 | 2 | Hello, World or wherever! | + | Capacity | ell | i | 1 | 2 | Hello, World or wherever! | + | Span | ell | i | 2 | 11 | Hello, World or wherever! | + | Capacity | ell | i | 2 | 11 | Hello, World or wherever! | + | Span | or | x | 2 | 23 | Hello, Wxld x wherever! | + | Capacity | or | x | 2 | 23 | Hello, Wxld x wherever! | + | Span | or | x | 2 | 10 | Hello, Wxld or wherever! | + | Capacity | or | x | 2 | 10 | Hello, Wxld or wherever! | + | Span | or | x | 9 | 6 | Hello, World x wherever! | + | Capacity | or | x | 9 | 6 | Hello, World x wherever! | + +Scenario Outline: Changes without length change + Given a UTF-8 ValueStringBuilder initialized with '' of length 23 + And I append 'Hello, World or wherever!' to the UTF-8 ValueStringBuilder + And I replace '' with '' at index with count in the UTF-8 ValueStringBuilder + When I get the string from the UTF-8 ValueStringBuilder + Then the UTF-8 ValueStringBuilder string should be '' + Examples: + | InitializationType | Find | Replacement | Start | Length | Result | + | Span | ell | ijk | 0 | 13 | Hijko, World or wherever! | + | Capacity | ell | ijk | 0 | 13 | Hijko, World or wherever! | + | Span | ell | ijk | 1 | 12 | Hijko, World or wherever! | + | Capacity | ell | ijk | 1 | 12 | Hijko, World or wherever! | + | Span | ell | ijk | 1 | 3 | Hijko, World or wherever! | + | Capacity | ell | ijk | 1 | 3 | Hijko, World or wherever! | + | Span | ell | ijk | 1 | 2 | Hello, World or wherever! | + | Capacity | ell | ijk | 1 | 2 | Hello, World or wherever! | + | Span | ell | ijk | 2 | 11 | Hello, World or wherever! | + | Capacity | ell | ijk | 2 | 11 | Hello, World or wherever! | + | Span | or | al | 2 | 23 | Hello, Walld al wherever! | + | Capacity | or | al | 2 | 23 | Hello, Walld al wherever! | + | Span | or | al | 2 | 10 | Hello, Walld or wherever! | + | Capacity | or | al | 2 | 10 | Hello, Walld or wherever! | + | Span | or | al | 9 | 6 | Hello, World al wherever! | + | Capacity | or | al | 9 | 6 | Hello, World al wherever! | + +Scenario Outline: Grows but fits in available space + Given a UTF-8 ValueStringBuilder initialized with '' of length 25 + And I append 'Hello, World or wherever!' to the UTF-8 ValueStringBuilder + And I replace '' with '' at index with count in the UTF-8 ValueStringBuilder + When I get the string from the UTF-8 ValueStringBuilder + Then the UTF-8 ValueStringBuilder string should be '' + Examples: + | InitializationType | Find | Replacement | Start | Length | Result | + | Span | ell | ijkx | 0 | 13 | Hijkxo, World or wherever! | + | Capacity | ell | ijkx | 0 | 13 | Hijkxo, World or wherever! | + | Span | ell | ijkx | 1 | 12 | Hijkxo, World or wherever! | + | Capacity | ell | ijkx | 1 | 12 | Hijkxo, World or wherever! | + | Span | ell | ijkx | 1 | 3 | Hijkxo, World or wherever! | + | Capacity | ell | ijkx | 1 | 3 | Hijkxo, World or wherever! | + | Span | ell | ijkx | 1 | 2 | Hello, World or wherever! | + | Capacity | ell | ijkx | 1 | 2 | Hello, World or wherever! | + | Span | ell | ijkx | 2 | 11 | Hello, World or wherever! | + | Capacity | ell | ijkx | 2 | 11 | Hello, World or wherever! | + | Span | or | alz | 2 | 23 | Hello, Walzld alz wherever! | + | Capacity | or | alz | 2 | 23 | Hello, Walzld alz wherever! | + | Span | or | alz | 2 | 10 | Hello, Walzld or wherever! | + | Capacity | or | alz | 2 | 10 | Hello, Walzld or wherever! | + | Span | or | alz | 9 | 6 | Hello, World alz wherever! | + | Capacity | or | alz | 9 | 6 | Hello, World alz wherever! | + | Span | ell | ijkxwv | 0 | 13 | Hijkxwvo, World or wherever! | + | Capacity | ell | ijkxwv | 0 | 13 | Hijkxwvo, World or wherever! | + | Span | ell | ijkxwv | 1 | 12 | Hijkxwvo, World or wherever! | + | Capacity | ell | ijkxwv | 1 | 12 | Hijkxwvo, World or wherever! | + | Span | ell | ijkxwv | 1 | 3 | Hijkxwvo, World or wherever! | + | Capacity | ell | ijkxwv | 1 | 3 | Hijkxwvo, World or wherever! | + | Span | ell | ijkxwv | 1 | 2 | Hello, World or wherever! | + | Capacity | ell | ijkxwv | 1 | 2 | Hello, World or wherever! | + | Span | ell | ijkxwv | 2 | 11 | Hello, World or wherever! | + | Capacity | ell | ijkxwv | 2 | 11 | Hello, World or wherever! | + | Span | or | alzyh | 2 | 23 | Hello, Walzyhld alzyh wherever! | + | Capacity | or | alzyh | 2 | 23 | Hello, Walzyhld alzyh wherever! | + | Span | or | alzyh | 2 | 10 | Hello, Walzyhld or wherever! | + | Capacity | or | alzyh | 2 | 10 | Hello, Walzyhld or wherever! | + | Span | or | alzyh | 9 | 6 | Hello, World alzyh wherever! | + | Capacity | or | alzyh | 9 | 6 | Hello, World alzyh wherever! | + + +Scenario Outline: Grows and requires resize + Given a UTF-8 ValueStringBuilder initialized with '' of length 32 + And I append 'Hello, World or wherever!padpadp' to the UTF-8 ValueStringBuilder + And I replace '' with '' at index with count in the UTF-8 ValueStringBuilder + When I get the string from the UTF-8 ValueStringBuilder + Then the UTF-8 ValueStringBuilder string should be '' + Examples: + | InitializationType | Find | Replacement | Start | Length | Result | + | Span | ell | ijkx | 0 | 13 | Hijkxo, World or wherever!padpadp | + | Capacity | ell | ijkx | 0 | 13 | Hijkxo, World or wherever!padpadp | + | Span | ell | ijkx | 1 | 12 | Hijkxo, World or wherever!padpadp | + | Capacity | ell | ijkx | 1 | 12 | Hijkxo, World or wherever!padpadp | + | Span | ell | ijkx | 1 | 3 | Hijkxo, World or wherever!padpadp | + | Capacity | ell | ijkx | 1 | 3 | Hijkxo, World or wherever!padpadp | + | Span | ell | ijkx | 1 | 2 | Hello, World or wherever!padpadp | + | Capacity | ell | ijkx | 1 | 2 | Hello, World or wherever!padpadp | + | Span | ell | ijkx | 2 | 11 | Hello, World or wherever!padpadp | + | Capacity | ell | ijkx | 2 | 11 | Hello, World or wherever!padpadp | + | Span | or | alz | 2 | 23 | Hello, Walzld alz wherever!padpadp | + | Capacity | or | alz | 2 | 23 | Hello, Walzld alz wherever!padpadp | + | Span | or | alz | 2 | 10 | Hello, Walzld or wherever!padpadp | + | Capacity | or | alz | 2 | 10 | Hello, Walzld or wherever!padpadp | + | Span | or | alz | 9 | 6 | Hello, World alz wherever!padpadp | + | Capacity | or | alz | 9 | 6 | Hello, World alz wherever!padpadp | + | Span | ell | ijkxwv | 0 | 13 | Hijkxwvo, World or wherever!padpadp | + | Capacity | ell | ijkxwv | 0 | 13 | Hijkxwvo, World or wherever!padpadp | + | Span | ell | ijkxwv | 1 | 12 | Hijkxwvo, World or wherever!padpadp | + | Capacity | ell | ijkxwv | 1 | 12 | Hijkxwvo, World or wherever!padpadp | + | Span | ell | ijkxwv | 1 | 3 | Hijkxwvo, World or wherever!padpadp | + | Capacity | ell | ijkxwv | 1 | 3 | Hijkxwvo, World or wherever!padpadp | + | Span | ell | ijkxwv | 1 | 2 | Hello, World or wherever!padpadp | + | Capacity | ell | ijkxwv | 1 | 2 | Hello, World or wherever!padpadp | + | Span | ell | ijkxwv | 2 | 11 | Hello, World or wherever!padpadp | + | Capacity | ell | ijkxwv | 2 | 11 | Hello, World or wherever!padpadp | + | Span | or | alzyh | 2 | 23 | Hello, Walzyhld alzyh wherever!padpadp | + | Capacity | or | alzyh | 2 | 23 | Hello, Walzyhld alzyh wherever!padpadp | + | Span | or | alzyh | 2 | 10 | Hello, Walzyhld or wherever!padpadp | + | Capacity | or | alzyh | 2 | 10 | Hello, Walzyhld or wherever!padpadp | + | Span | or | alzyh | 9 | 6 | Hello, World alzyh wherever!padpadp | + | Capacity | or | alzyh | 9 | 6 | Hello, World alzyh wherever!padpadp | + + +Scenario Outline: Text not present + Given a UTF-8 ValueStringBuilder initialized with '' of length 25 + And I append 'Hello, World or wherever!' to the UTF-8 ValueStringBuilder + And I replace '' with '' at index with count in the UTF-8 ValueStringBuilder + When I get the string from the UTF-8 ValueStringBuilder + Then the UTF-8 ValueStringBuilder string should be 'Hello, World or wherever!' + Examples: + | InitializationType | Find | Replacement | Start | Length | + | Span | foo | ijkx | 0 | 25 | + | Capacity | foo | ijkx | 0 | 25 | + | Span | elloo | ijkx | 0 | 25 | + | Capacity | elloo | ijkx | 0 | 25 | + + +Scenario Outline: Text not within specified range + Given a UTF-8 ValueStringBuilder initialized with '' of length 25 + And I append 'Hello, World or wherever!' to the UTF-8 ValueStringBuilder + And I replace '' with '' at index with count in the UTF-8 ValueStringBuilder + When I get the string from the UTF-8 ValueStringBuilder + Then the UTF-8 ValueStringBuilder string should be 'Hello, World or wherever!' + Examples: + | InitializationType | Find | Replacement | Start | Length | + | Span | orld | ijkx | 0 | 8 | + | Capacity | orld | ijkx | 0 | 8 | + | Span | orld | ijkx | 0 | 8 | + | Capacity | orld | ijkx | 0 | 11 | + | Span | orld | ijkx | 8 | 3 | + | Capacity | orld | ijkx | 8 | 3 | + | Span | orld | ijkx | 9 | 16 | + | Capacity | orld | ijkx | 9 | 16 | + | Span | orld | ijkx | 12 | 13 | + | Capacity | orld | ijkx | 12 | 13 | + | Span | orld | ijkx | 0 | 0 | + | Capacity | orld | ijkx | 0 | 0 | + | Span | orld | ijkx | 8 | 0 | + | Capacity | orld | ijkx | 8 | 0 | + +Scenario Outline: Specified range is out of bounds + Given a UTF-8 ValueStringBuilder initialized with '' of length 25 + And I append 'Hello, World or wherever!' to the UTF-8 ValueStringBuilder + And I attempt to replace '' with '' at index with count in the UTF-8 ValueStringBuilder + Then the attempt should have thrown a 'System.ArgumentOutOfRangeException' + Examples: + | InitializationType | Find | Replacement | Start | Length | + | Span | orld | ijkx | 0 | 26 | + | Capacity | orld | ijkx | 0 | 26 | + | Span | orld | ijkx | 1 | 25 | + | Capacity | orld | ijkx | 1 | 25 | + | Span | orld | ijkx | 10 | -4 | + | Capacity | orld | ijkx | 10 | -4 | diff --git a/Solutions/Corvus.HighPerformance.Specs/ValueStringBuilderFeatures/Utf8/Utf8ValueStringBuilderStepDefinitions.cs b/Solutions/Corvus.HighPerformance.Specs/ValueStringBuilderFeatures/Utf8/Utf8ValueStringBuilderStepDefinitions.cs new file mode 100644 index 0000000..ef91683 --- /dev/null +++ b/Solutions/Corvus.HighPerformance.Specs/ValueStringBuilderFeatures/Utf8/Utf8ValueStringBuilderStepDefinitions.cs @@ -0,0 +1,109 @@ +// +// Copyright (c) Endjin Limited. All rights reserved. +// + +using System.Text; + +using Corvus.HighPerformance.Specs; + +using Reqnroll; + +namespace ValueStringBuilderFeatures.Utf8; + +[Binding] +public class Utf8ValueStringBuilderStepDefinitions(ExceptionStepDefinitions exceptionSteps) +{ + private Utf8ValueStringBuilderTestDriver? driver; + + private Utf8ValueStringBuilderTestDriver Driver => this.driver ?? throw new InvalidOperationException("No step that initialized the driver has been executed"); + + [Given("a UTF-8 ValueStringBuilder initialized with {string} of length {int}")] + public void GivenAUtf8ValueStringBuilderInitializedWithOfLength( + Utf8ValueStringBuilderTestDriver.InitType intType, int initialCapacity) + { + this.driver = new(intType, initialCapacity); + } + + [Given("I append {string} to the UTF-8 ValueStringBuilder")] + public void GivenIAppendToTheUtf8ValueStringBuilder(string text) + { + this.Driver.AddOperation(new Utf8ValueStringBuilderTestDriver.AppendOperation(Encoding.UTF8.GetBytes(text).AsMemory())); + } + + [Given("I append the Int32 {int} to the UTF-8 ValueStringBuilder")] + public void GivenIAppendTheIntToTheUtf8ValueStringBuilder(int v) + { + this.Driver.AddOperation(new Utf8ValueStringBuilderTestDriver.AppendInt32Operation(v)); + } + + [Given("I replace {string} with {string} at index {int} with count {int} in the UTF-8 ValueStringBuilder")] + public void GivenIReplaceWithAtIndexWithCount( + string oldValue, string newValue, int startIndex, int count) + { + this.Driver.AddOperation(new Utf8ValueStringBuilderTestDriver.ReplaceOperation( + Encoding.UTF8.GetBytes(oldValue).AsMemory(), Encoding.UTF8.GetBytes(newValue).AsMemory(), startIndex, count)); + } + + [Given("I attempt to replace {string} with {string} at index {int} with count {int} in the UTF-8 ValueStringBuilder")] + public void GivenIAttemptToReplaceWithAtIndexWithCount( + string oldValue, string newValue, int startIndex, int count) + { + this.Driver.AddOperation(new Utf8ValueStringBuilderTestDriver.AttemptReplaceOperation( + Encoding.UTF8.GetBytes(oldValue).AsMemory(), Encoding.UTF8.GetBytes(newValue).AsMemory(), startIndex, count, exceptionSteps)); + this.Driver.Execute(); + } + + [When("I get the string from the UTF-8 ValueStringBuilder via {string}")] + public void WhenIGetTheStringFromTheUtf8ValueStringBuilderVia(Utf8ValueStringBuilderValueFrom mechanism) + { + this.Driver.Execute(valueFrom: mechanism); + } + + [When("I get the string from the UTF-8 ValueStringBuilder")] + public void WhenIGetTheStringFromTheUtf8ValueStringBuilder() + { + this.WhenIGetTheStringFromTheUtf8ValueStringBuilderVia(Utf8ValueStringBuilderValueFrom.CreateStringAndDispose); + } + + [When("I get the Memory starting at {int} with length {int} from the UTF-8 ValueStringBuilder")] + public void WhenIGetTheMemoryStartingAtWithLengthFromTheUtf8ValueStringBuilder(int start, int length) + { + this.Driver.Execute(Utf8ValueStringBuilderValueFrom.Memory, start, length); + } + + [When("I get the Memory starting at {int} from the UTF-8 ValueStringBuilder")] + public void WhenIGetTheMemoryStartingAtFromTheUtf8ValueStringBuilder(int start) + { + this.Driver.Execute(Utf8ValueStringBuilderValueFrom.Memory, start); + } + + [When("I get the Memory from the UTF-8 ValueStringBuilder")] + public void WhenIGetTheMemoryFromTheUtf8ValueStringBuilder() + { + this.Driver.Execute(Utf8ValueStringBuilderValueFrom.Memory); + } + + [When("I get the Span starting at {int} with length {int} from the UTF-8 ValueStringBuilder")] + public void WhenIGetTheSpanStartingAtWithLengthFromTheUtf8ValueStringBuilder(int start, int length) + { + this.Driver.Execute(Utf8ValueStringBuilderValueFrom.Span, start, length); + } + + [When("I get the Span starting at {int} from the UTF-8 ValueStringBuilder")] + public void WhenIGetTheSpanStartingAtFromTheUtf8ValueStringBuilder(int start) + { + this.Driver.Execute(Utf8ValueStringBuilderValueFrom.Span, start); + } + + [When("I get the Span from the UTF-8 ValueStringBuilder")] + public void WhenIGetTheSpanFromTheUtf8ValueStringBuilder() + { + this.Driver.Execute(Utf8ValueStringBuilderValueFrom.Span); + } + + [Then("the UTF-8 ValueStringBuilder string should be {string}")] + public void ThenTheUtf8ValueStringBuilderStringShouldBe(string expectedResult) + { + Assert.IsTrue(Encoding.UTF8.GetBytes(expectedResult).AsMemory().Span.SequenceEqual(this.Driver.Result.Span)); + } +} \ No newline at end of file diff --git a/Solutions/Corvus.HighPerformance.Specs/ValueStringBuilderFeatures/Utf8/Utf8ValueStringBuilderTestDriver.cs b/Solutions/Corvus.HighPerformance.Specs/ValueStringBuilderFeatures/Utf8/Utf8ValueStringBuilderTestDriver.cs new file mode 100644 index 0000000..25367f6 --- /dev/null +++ b/Solutions/Corvus.HighPerformance.Specs/ValueStringBuilderFeatures/Utf8/Utf8ValueStringBuilderTestDriver.cs @@ -0,0 +1,163 @@ +// +// Copyright (c) Endjin Limited. All rights reserved. +// + +using System.Globalization; +using System.Text; + +using Corvus.HighPerformance.Specs; +using Corvus.HighPerformance.Utf8; + +namespace ValueStringBuilderFeatures.Utf8; + +public class Utf8ValueStringBuilderTestDriver( + Utf8ValueStringBuilderTestDriver.InitType initType, + int initialCapacity) +{ + private readonly List operations = []; + private ReadOnlyMemory? result; + + public enum InitType + { + Span, + Capacity, + } + + public ReadOnlyMemory Result => this.result ?? throw new InvalidOperationException("Result requires Execute to have run"); + + public void AddOperation(OperationBase operation) + { + this.operations.Add(operation); + } + + public void Execute( + Utf8ValueStringBuilderValueFrom valueFrom = Utf8ValueStringBuilderValueFrom.CreateStringAndDispose, + int? start = null, + int? length = null) + { + ValueStringBuilder sb = initType switch + { + InitType.Span => new ValueStringBuilder(stackalloc byte[initialCapacity]), + InitType.Capacity => new ValueStringBuilder(initialCapacity), + _ => throw new InvalidOperationException(), + }; + + foreach (OperationBase op in this.operations) + { + op.Execute(ref sb); + } + + switch (valueFrom) + { + case Utf8ValueStringBuilderValueFrom.CreateStringAndDispose: + this.result = sb.CreateMemoryAndDispose(); + break; + + case Utf8ValueStringBuilderValueFrom.RentedBuffer: + (byte[]? rentedBuffer, int returnedLength) = sb.GetRentedBufferAndLengthAndDispose(); + this.result = rentedBuffer.AsSpan(0, returnedLength).ToArray(); + ValueStringBuilder.ReturnRentedBuffer(rentedBuffer); + break; + + case Utf8ValueStringBuilderValueFrom.Memory: + if (length.HasValue) + { + if (!start.HasValue) + { + throw new ArgumentException("If length is specified, start must also be specified.", nameof(length)); + } + + this.result = sb.AsMemory(start.Value, length.Value); + } + else if (start.HasValue) + { + this.result = sb.AsMemory(start.Value); + } + else + { + this.result = sb.AsMemory(); + } + + break; + + case Utf8ValueStringBuilderValueFrom.Span: + if (length.HasValue) + { + if (!start.HasValue) + { + throw new ArgumentException("If length is specified, start must also be specified.", nameof(length)); + } + + this.result = sb.AsSpan(start.Value, length.Value).ToArray(); + } + else if (start.HasValue) + { + this.result = sb.AsSpan(start.Value).ToArray(); + } + else + { + this.result = sb.AsSpan().ToArray(); + } + + break; + + default: + throw new ArgumentOutOfRangeException(nameof(valueFrom), valueFrom, null); + } + } + + public abstract class OperationBase + { + public abstract void Execute(ref ValueStringBuilder sb); + } + + public class AppendOperation(ReadOnlyMemory value) : OperationBase + { + public override void Execute(ref ValueStringBuilder sb) + { + sb.Append(value.Span); + } + } + + public class AppendInt32Operation(int value) : OperationBase + { + public override void Execute(ref ValueStringBuilder sb) + { + sb.Append(value); + } + } + + public class ReplaceOperation( + ReadOnlyMemory oldValue, + ReadOnlyMemory newValue, + int startIndex, + int count) + : OperationBase + { + public override void Execute(ref ValueStringBuilder sb) + { + sb.Replace(oldValue.Span, newValue.Span, startIndex, count); + } + } + + public class AttemptReplaceOperation( + ReadOnlyMemory oldValue, + ReadOnlyMemory newValue, + int startIndex, + int count, + ExceptionStepDefinitions exceptionSteps) + : OperationBase + { + public override void Execute(ref ValueStringBuilder sb) + { + try + { + sb.Replace(oldValue.Span, newValue.Span, startIndex, count); + } + catch (Exception ex) + { + exceptionSteps.Exception = ex; + } + } + } +} \ No newline at end of file diff --git a/Solutions/Corvus.HighPerformance.Specs/ValueStringBuilderFeatures/Utf8/Utf8ValueStringBuilderValueFrom.cs b/Solutions/Corvus.HighPerformance.Specs/ValueStringBuilderFeatures/Utf8/Utf8ValueStringBuilderValueFrom.cs new file mode 100644 index 0000000..09e6d60 --- /dev/null +++ b/Solutions/Corvus.HighPerformance.Specs/ValueStringBuilderFeatures/Utf8/Utf8ValueStringBuilderValueFrom.cs @@ -0,0 +1,13 @@ +// +// Copyright (c) Endjin Limited. All rights reserved. +// + +namespace ValueStringBuilderFeatures.Utf8; + +public enum Utf8ValueStringBuilderValueFrom +{ + RentedBuffer, + CreateStringAndDispose, + Memory, + Span, +} \ No newline at end of file diff --git a/Solutions/Corvus.HighPerformance.Specs/packages.lock.json b/Solutions/Corvus.HighPerformance.Specs/packages.lock.json index dbca46b..a9d2df7 100644 --- a/Solutions/Corvus.HighPerformance.Specs/packages.lock.json +++ b/Solutions/Corvus.HighPerformance.Specs/packages.lock.json @@ -27,15 +27,6 @@ "Microsoft.CodeCoverage": "17.11.1" } }, - "Microsoft.NETFramework.ReferenceAssemblies": { - "type": "Direct", - "requested": "[1.0.3, )", - "resolved": "1.0.3", - "contentHash": "vUc9Npcs14QsyOD01tnv/m8sQUnGTGOw1BCmKcv77LBJY7OxhJ+zJF7UD/sCL3lYNFuqmQEVlkfS4Quif6FyYg==", - "dependencies": { - "Microsoft.NETFramework.ReferenceAssemblies.net481": "1.0.3" - } - }, "MSTest.TestAdapter": { "type": "Direct", "requested": "[3.6.0, )", @@ -138,11 +129,6 @@ "System.Text.Json": "8.0.4" } }, - "Microsoft.NETFramework.ReferenceAssemblies.net481": { - "type": "Transitive", - "resolved": "1.0.3", - "contentHash": "Vv/20vgHS7VglVOVh8J3Iz/MA+VYKVRp9f7r2qiKBMuzviTOmocG70yq0Q8T5OTmCONkEAIJwETD1zhEfLkAXQ==" - }, "Microsoft.SourceLink.Common": { "type": "Transitive", "resolved": "1.1.1", diff --git a/Solutions/Corvus.HighPerformance/Corvus.HighPerformance/Utf8/ValueStringBuilder.Replace.cs b/Solutions/Corvus.HighPerformance/Corvus.HighPerformance/Utf8/ValueStringBuilder.Replace.cs new file mode 100644 index 0000000..f33089f --- /dev/null +++ b/Solutions/Corvus.HighPerformance/Corvus.HighPerformance/Utf8/ValueStringBuilder.Replace.cs @@ -0,0 +1,155 @@ +// +// Copyright (c) Endjin Limited. All rights reserved. +// +// +// Derived from code Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See https://github.com/dotnet/runtime/blob/c1049390d5b33483203f058b0e1457d2a1f62bf4/src/libraries/Common/src/System/Text/ValueStringBuilder.cs +// + +#pragma warning disable // Currently this is a straight copy of the original code, so we disable warnings to avoid diagnostic problems. + +#if !NET8_0_OR_GREATER +using System.Runtime.CompilerServices; +#endif + +namespace Corvus.HighPerformance.Utf8; + +public ref partial struct ValueStringBuilder +{ + public void Replace( + scoped ReadOnlySpan oldValue, + scoped ReadOnlySpan newValue, + int startIndex, + int count) + { + if (startIndex < 0 || (startIndex + count) > _pos) + { + throw new ArgumentOutOfRangeException(nameof(startIndex)); + } + + if (count == 0) + { + return; + } + + Span rangeBuffer = _bytes.Slice(startIndex, count); + + int diff = newValue.Length - oldValue.Length; + if (diff == 0) + { + int matchIndex = rangeBuffer.IndexOf(oldValue); + if (matchIndex == -1) + { + return; + } + + Span remainingBuffer = rangeBuffer; + do + { + remainingBuffer = remainingBuffer[matchIndex..]; + newValue.CopyTo(remainingBuffer); + remainingBuffer = remainingBuffer[oldValue.Length..]; + + matchIndex = remainingBuffer.IndexOf(oldValue); + } while (matchIndex != -1); + + return; + } + + if (diff < 0) + { + int matchIndex = rangeBuffer.IndexOf(oldValue); + if (matchIndex == -1) + { + return; + } + + // We will never need to grow the buffer, but we might need to shift byteacters + // down. + Span remainingTargetBuffer = _bytes[(startIndex + matchIndex)..this._pos]; + Span remainingSourceBuffer = remainingTargetBuffer; + int endOfSearchRangeRelativeToRemainingSourceBuffer = count - matchIndex; + do + { + this._pos += diff; + + newValue.CopyTo(remainingTargetBuffer); + + remainingSourceBuffer = remainingSourceBuffer[oldValue.Length..]; + endOfSearchRangeRelativeToRemainingSourceBuffer -= oldValue.Length; + remainingTargetBuffer = remainingTargetBuffer[newValue.Length..]; + + matchIndex = remainingSourceBuffer[..endOfSearchRangeRelativeToRemainingSourceBuffer] + .IndexOf(oldValue); + + int lengthOfChunkToRelocate = matchIndex == -1 + ? remainingSourceBuffer.Length + : matchIndex; + remainingSourceBuffer[..lengthOfChunkToRelocate].CopyTo(remainingTargetBuffer); + + remainingSourceBuffer = remainingSourceBuffer[lengthOfChunkToRelocate..]; + endOfSearchRangeRelativeToRemainingSourceBuffer -= lengthOfChunkToRelocate; + remainingTargetBuffer = remainingTargetBuffer[lengthOfChunkToRelocate..]; + } while (matchIndex != -1); + + return; + } + else + { + int matchIndex = rangeBuffer.IndexOf(oldValue); + if (matchIndex == -1) + { + return; + } + + Span matchIndexes = stackalloc int[(rangeBuffer.Length + oldValue.Length - 1) / oldValue.Length]; + + int matchCount = 0; + int currentRelocationDistance = 0; + while (matchIndex != -1) + { + matchIndexes[matchCount++] = matchIndex; + currentRelocationDistance += diff; + + int nextIndex = rangeBuffer[(matchIndex + oldValue.Length)..].IndexOf(oldValue); + matchIndex = nextIndex == -1 ? -1 : matchIndex + nextIndex + oldValue.Length; + } + + int relocationRangeEndIndex = this._pos; + + int growBy = (this._pos + currentRelocationDistance) - _bytes.Length; + if (growBy > 0) + { + Grow(growBy); + } + this._pos += currentRelocationDistance; + + + // We work from the back of the string when growing to avoid having to + // shift anything more than once. + do + { + matchIndex = matchIndexes[matchCount - 1]; + + int relocationTargetStart = startIndex + matchIndex + oldValue.Length + currentRelocationDistance; + int relocationSourceStart = startIndex + matchIndex + oldValue.Length; + int endOfSearchRangeRelativeToRemainingSourceBuffer = count - matchIndex; + + Span relocationTargetBuffer = _bytes[relocationTargetStart..]; + Span sourceBuffer = _bytes[relocationSourceStart..relocationRangeEndIndex]; + + sourceBuffer.CopyTo(relocationTargetBuffer); + + currentRelocationDistance -= diff; + Span replaceTargetBuffer = this._bytes.Slice(startIndex + matchIndex + currentRelocationDistance); + newValue.CopyTo(replaceTargetBuffer); + + relocationRangeEndIndex = matchIndex + startIndex; + matchIndex = rangeBuffer[..matchIndex].LastIndexOf(oldValue); + + matchCount -= 1; + } while (matchCount > 0); + } + } +} diff --git a/Solutions/Corvus.HighPerformance/Corvus.HighPerformance/Utf8/ValueStringBuilder.cs b/Solutions/Corvus.HighPerformance/Corvus.HighPerformance/Utf8/ValueStringBuilder.cs new file mode 100644 index 0000000..516bbb6 --- /dev/null +++ b/Solutions/Corvus.HighPerformance/Corvus.HighPerformance/Utf8/ValueStringBuilder.cs @@ -0,0 +1,414 @@ +// +// Copyright (c) Endjin Limited. All rights reserved. +// +// +// Derived from code Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See https://github.com/dotnet/runtime/blob/c1049390d5b33483203f058b0e1457d2a1f62bf4/src/libraries/Common/src/System/Text/ValueStringBuilder.cs +// + +#pragma warning disable // Currently this is a straight copy of the original code, so we disable warnings to avoid diagnostic problems. + +using System.Buffers; +using System.Buffers.Text; +using System.ComponentModel; +using System.Diagnostics; +using System.Diagnostics.Tracing; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +#nullable enable + +namespace Corvus.HighPerformance.Utf8; + +public ref partial struct ValueStringBuilder +{ + private byte[]? _arrayToReturnToPool; + private Span _bytes; + private int _pos; + + public ValueStringBuilder(Span initialBuffer) + { + _arrayToReturnToPool = null; + _bytes = initialBuffer; + _pos = 0; + } + + public ValueStringBuilder(int initialCapacity) + { + _arrayToReturnToPool = ArrayPool.Shared.Rent(initialCapacity); + _bytes = _arrayToReturnToPool; + _pos = 0; + } + + public int Length + { + get + { + if (_pos < 0) { throw new ObjectDisposedException("Either Dispose or GetRentedBuffer or GetRentedBufferAndLength has already been called"); } + return _pos; + } + + set + { + Debug.Assert(value >= 0); + Debug.Assert(value <= _bytes.Length); + _pos = value; + } + } + + public int Capacity => _bytes.Length; + + /// + /// Return the underlying rented buffer, if any, and the length of the string. This also + /// disposes the instance. + /// + /// + /// + /// Once you have retrieved this, you must not make any further use of + /// . You should call + /// once you no longer require the buffer. + /// + /// + public (byte[]? Buffer, int Length) GetRentedBufferAndLengthAndDispose() + { + byte[]? result = _arrayToReturnToPool; + int length = Length; + SetToDisposed(); + return (result, length); + } + + /// + /// Returns the buffer retrieved from . + /// + /// The buffer to return. + public static void ReturnRentedBuffer(byte[]? buffer) + { + ReturnRentedBuffer(buffer, false); + } + + /// + /// Returns the buffer retrieved from . + /// + /// The buffer to return. + /// If then clear the buffer when returned. + public static void ReturnRentedBuffer(byte[]? buffer, bool clearBuffer) + { + if (buffer is byte[] b) + { + ArrayPool.Shared.Return(b, clearBuffer); + } + } + + public void EnsureCapacity(int capacity) + { + // This is not expected to be called this with negative capacity + Debug.Assert(capacity >= 0); + + // If the caller has a bug and calls this with negative capacity, make sure to call Grow to throw an exception. + if ((uint)capacity > (uint)_bytes.Length) + Grow(capacity - _pos); + } + + /// + /// Get a pinnable reference to the builder. + /// Does not ensure there is a null byte after + /// This overload is pattern matched in the C# 7.3+ compiler so you can omit + /// the explicit method call, and write eg "fixed (byte* c = builder)" + /// + public ref byte GetPinnableReference() + { + return ref MemoryMarshal.GetReference(_bytes); + } + + /// + /// Get a pinnable reference to the builder. + /// + /// Ensures that the builder has a null byte after + public ref byte GetPinnableReference(bool terminate) + { + if (terminate) + { + EnsureCapacity(Length + 1); + _bytes[Length] = (byte)'\0'; + } + return ref MemoryMarshal.GetReference(_bytes); + } + + public ref byte this[int index] + { + get + { + Debug.Assert(index < _pos); + return ref _bytes[index]; + } + } + + public ReadOnlyMemory CreateMemoryAndDispose() + { + ReadOnlyMemory s = _bytes.Slice(0, _pos).ToArray(); + Dispose(); + return s; + } + + /// Returns the underlying storage of the builder. + public Span RawBytes => _bytes; + + /// + /// Returns a span around the contents of the builder. + /// + /// Ensures that the builder has a null byte after + public ReadOnlySpan AsSpan(bool terminate) + { + if (terminate) + { + EnsureCapacity(Length + 1); + _bytes[Length] = (byte)'\0'; + } + return _bytes.Slice(0, _pos); + } + + public ReadOnlySpan AsSpan() => _bytes.Slice(0, _pos); + public ReadOnlySpan AsSpan(int start) => _bytes.Slice(start, _pos - start); + public ReadOnlySpan AsSpan(int start, int length) => _bytes.Slice(start, length); + + /// + /// Returns the contents of the builder as a , renting the underlying buffer if it + /// is not already backed by a rented buffer. + /// + /// The value as a + public ReadOnlyMemory AsMemory() + { + this.EnsureRented(); + return _arrayToReturnToPool.AsMemory(0, _pos); + } + + /// + /// Returns the contents of the builder as a , renting the underlying buffer if it + /// is not already backed by a rented buffer. + /// + /// The index within the string at which to start. + /// The value as a + public ReadOnlyMemory AsMemory(int start) + { + this.EnsureRented(); + return _arrayToReturnToPool.AsMemory(start, _pos - start); + } + + /// + /// Returns the contents of the builder as a , renting the underlying buffer if it + /// is not already backed by a rented buffer. + /// + /// The index within the string at which to start. + /// The length of the span to return. + /// The value as a + public ReadOnlyMemory AsMemory(int start, int length) + { + this.EnsureRented(); + return _arrayToReturnToPool.AsMemory(start, length); + } + + public bool TryCopyTo(Span destination, out int bytesWritten) + { + if (_bytes.Slice(0, _pos).TryCopyTo(destination)) + { + bytesWritten = _pos; + Dispose(); + return true; + } + else + { + bytesWritten = 0; + Dispose(); + return false; + } + } + + public void Insert(int index, byte value, int count) + { + if (_pos > _bytes.Length - count) + { + Grow(count); + } + + int remaining = _pos - index; + _bytes.Slice(index, remaining).CopyTo(_bytes.Slice(index + count)); + _bytes.Slice(index, count).Fill(value); + _pos += count; + } + + public void Insert(int index, scoped ReadOnlySpan s) + { + int count = s.Length; + + if (_pos > _bytes.Length - count) + { + Grow(count); + } + + int remaining = _pos - index; + _bytes.Slice(index, remaining).CopyTo(_bytes.Slice(index + count)); + s.CopyTo(_bytes.Slice(index)); + _pos += count; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void Append(byte c) + { + int pos = _pos; + Span bytes = _bytes; + if ((uint)pos < (uint)bytes.Length) + { + bytes[pos] = c; + _pos = pos + 1; + } + else + { + GrowAndAppend(c); + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void Append(scoped ReadOnlySpan s) + { + int pos = _pos; + if (s.Length == 1 && (uint)pos < (uint)_bytes.Length) + { + _bytes[pos] = s[0]; + _pos = pos + 1; + } + else + { + AppendSlow(s); + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void Append(int value, StandardFormat format = default) + { + const int ExpandInt32String = 14; + int bytesWritten; + while(!Utf8Formatter.TryFormat(value, _bytes.Slice(_pos), out bytesWritten, format)) + { + Grow(ExpandInt32String); + } + + _pos += bytesWritten; + } + + private void AppendSlow(scoped ReadOnlySpan s) + { + int pos = _pos; + if (pos > _bytes.Length - s.Length) + { + Grow(s.Length); + } + + s.CopyTo(_bytes.Slice(pos)); + _pos += s.Length; + } + + public void Append(byte c, int count) + { + if (_pos > _bytes.Length - count) + { + Grow(count); + } + + Span dst = _bytes.Slice(_pos, count); + for (int i = 0; i < dst.Length; i++) + { + dst[i] = c; + } + _pos += count; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public Span AppendSpan(int length) + { + int origPos = _pos; + if (origPos > _bytes.Length - length) + { + Grow(length); + } + + _pos = origPos + length; + return _bytes.Slice(origPos, length); + } + + [MethodImpl(MethodImplOptions.NoInlining)] + private void GrowAndAppend(byte c) + { + Grow(1); + Append(c); + } + + /// + /// Resize the internal buffer either by doubling current buffer size or + /// by adding to + /// whichever is greater. + /// + /// + /// Number of bytes requested beyond current position. + /// + [MethodImpl(MethodImplOptions.NoInlining)] + private void Grow(int additionalCapacityBeyondPos) + { + Debug.Assert(additionalCapacityBeyondPos > 0); + Debug.Assert(_bytes.Length < _pos + additionalCapacityBeyondPos, "Grow called incorrectly, no resize is needed."); + //Debug.Assert(_pos > _bytes.Length - additionalCapacityBeyondPos, "Grow called incorrectly, no resize is needed."); + + const uint ArrayMaxLength = 0x7FFFFFC7; // same as Array.MaxLength + + // Increase to at least the required size (_pos + additionalCapacityBeyondPos), but try + // to double the size if possible, bounding the doubling to not go beyond the max array length. + int newCapacity = (int)Math.Max( + (uint)(_pos + additionalCapacityBeyondPos), + Math.Min((uint)_bytes.Length * 2, ArrayMaxLength)); + + // Make sure to let Rent throw an exception if the caller has a bug and the desired capacity is negative. + // This could also go negative if the actual required length wraps around. + byte[] poolArray = ArrayPool.Shared.Rent(newCapacity); + + _bytes.Slice(0, _pos).CopyTo(poolArray); + + byte[]? toReturn = _arrayToReturnToPool; + _bytes = _arrayToReturnToPool = poolArray; + if (toReturn != null) + { + ArrayPool.Shared.Return(toReturn); + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void Dispose() + { + byte[]? toReturn = _arrayToReturnToPool; + SetToDisposed(); + if (toReturn != null) + { + ArrayPool.Shared.Return(toReturn, true); + } + } + + /// + /// Puts the object into a state preventing accidental continued use an a pool already returned + /// to an array, or attempting to retrieve information from the object after it has either + /// been disposed, or had its buffer returned to the pool as a result of calling + /// . + /// + private void SetToDisposed() + { + this = default(ValueStringBuilder) with { _pos = -1 }; + } + + private void EnsureRented() + { + if (_arrayToReturnToPool == null) + { + _arrayToReturnToPool = ArrayPool.Shared.Rent(_bytes.Length); + _bytes.Slice(0, _pos).CopyTo(_arrayToReturnToPool); + _bytes = _arrayToReturnToPool; + } + } +}