From 82ba1c613c0d77a316853bbc5380e79f80876847 Mon Sep 17 00:00:00 2001 From: axunonb Date: Sat, 18 Jan 2025 22:04:02 +0100 Subject: [PATCH 1/3] Improve line folding and serialization performance - Update line folding to use StringBuilder and ArrayPool for better performance and memory efficiency. - Ensure compliance with RFC 5545 by handling multi-byte characters correctly. - Add System.Buffers package for netstandard2.0 target framework. Resolves #693 --- Ical.Net.Tests/PeriodListWrapperTests.cs | 6 +- Ical.Net.Tests/RecurrenceWithRDateTests.cs | 5 +- Ical.Net.Tests/TextUtilTests.cs | 75 +++++++---- Ical.Net/Ical.Net.csproj | 3 + Ical.Net/Serialization/ComponentSerializer.cs | 6 +- Ical.Net/Serialization/PropertySerializer.cs | 120 ++++++++++-------- Ical.Net/Utility/TextUtil.cs | 65 ++++++++-- 7 files changed, 185 insertions(+), 95 deletions(-) diff --git a/Ical.Net.Tests/PeriodListWrapperTests.cs b/Ical.Net.Tests/PeriodListWrapperTests.cs index 5c1771aae..4d5062359 100644 --- a/Ical.Net.Tests/PeriodListWrapperTests.cs +++ b/Ical.Net.Tests/PeriodListWrapperTests.cs @@ -213,9 +213,9 @@ public void AddRPeriod_ShouldCreate_DedicatePeriodList() Assert.That(serialized, Does.Contain( - "RDATE;VALUE=PERIOD:20250102/20250105,20250501/20250510,20250501/P9D\r\n" + - "RDATE;VALUE=PERIOD:20250202T000000Z/20250202T060000Z,20250601T120000Z/2025\r\n" + - " 0601T140000Z\r\n" + + "RDATE;VALUE=PERIOD:20250102/20250105,20250501/20250510,20250501/P9D" + SerializationConstants.LineBreak + + "RDATE;VALUE=PERIOD:20250202T000000Z/20250202T060000Z,20250601T120000Z/20250" + SerializationConstants.LineBreak + + " 601T140000Z" + SerializationConstants.LineBreak + "RDATE;TZID=Europe/Vienna;VALUE=PERIOD:20250601T120000/PT8H")); // A flattened list of all dates diff --git a/Ical.Net.Tests/RecurrenceWithRDateTests.cs b/Ical.Net.Tests/RecurrenceWithRDateTests.cs index 56d645e5c..25785a1d7 100644 --- a/Ical.Net.Tests/RecurrenceWithRDateTests.cs +++ b/Ical.Net.Tests/RecurrenceWithRDateTests.cs @@ -10,6 +10,7 @@ using Ical.Net.Collections; using Ical.Net.DataTypes; using Ical.Net.Serialization; +using Ical.Net.Utility; using NUnit.Framework; namespace Ical.Net.Tests; @@ -152,7 +153,7 @@ public void RDate_PeriodsWithTimezone_AreProcessedCorrectly() Assert.That(occurrences[1].Period.EffectiveDuration, Is.EqualTo(new Duration(hours: 4))); Assert.That(occurrences[2].Period.EffectiveDuration, Is.EqualTo(new Duration(hours: 5))); // Line folding is used for long lines - Assert.That(ics, Does.Contain("RDATE;TZID=America/New_York;VALUE=PERIOD:20231002T100000/PT4H,20231003T100\r\n 000/PT5H")); + Assert.That(ics, Does.Contain("RDATE;TZID=America/New_York;VALUE=PERIOD:20231002T100000/PT4H,20231003T1000\r\n 00/PT5H")); }); // Deserialization @@ -342,7 +343,7 @@ public void RDate_LargeNumberOfDates_ShouldBeLineFolded() // First folded line is 75 characters long Assert.That(ics, Does.Contain("RDATE:20231002T100000,20231003T100000,20231004T100000,20231005T100000,2023")); // Last folded line - Assert.That(ics, Does.Contain(" T100000,20240106T100000,20240107T100000,20240108T100000,20240109T100000")); + Assert.That(ics, Does.Contain(" 00,20240107T100000,20240108T100000,20240109T100000")); }); } diff --git a/Ical.Net.Tests/TextUtilTests.cs b/Ical.Net.Tests/TextUtilTests.cs index 89b02f327..b6b4afc7d 100644 --- a/Ical.Net.Tests/TextUtilTests.cs +++ b/Ical.Net.Tests/TextUtilTests.cs @@ -4,6 +4,8 @@ // using System.Collections; +using System.Linq; +using System.Text; using Ical.Net.Utility; using NUnit.Framework; @@ -12,38 +14,63 @@ namespace Ical.Net.Tests; public class TextUtilTests { [Test, TestCaseSource(nameof(FoldLines_TestCases))] - public string FoldLines_Tests(string incoming) => TextUtil.FoldLines(incoming); + public string FoldLines_Tests(string input) + { + var sb = new StringBuilder(input.Length); + sb.FoldLines(input); + return sb.ToString(); + } public static IEnumerable FoldLines_TestCases() { - yield return new TestCaseData("Short") - .Returns("Short" + SerializationConstants.LineBreak) + yield return new TestCaseData("No folding") + .Returns("No folding" + SerializationConstants.LineBreak) .SetName("Short string remains unfolded"); - const string moderatelyLongReturns = - "HelloWorldHelloWorldHelloWorldHelloWorldHelloWorldHelloWorldHelloWorldHell" + SerializationConstants.LineBreak - + " oWorld" + SerializationConstants.LineBreak; + const string exactly85OctetsReturns = + "HelloWorldHelloWorldHelloWorldHelloWorldHelloWorldHelloWorldHelloWorldHello" + SerializationConstants.LineBreak + + " WorldHello" + SerializationConstants.LineBreak; + + yield return new TestCaseData("HelloWorldHelloWorldHelloWorldHelloWorldHelloWorldHelloWorldHelloWorldHelloWorldHello") + .Returns(exactly85OctetsReturns) + .SetName("10 Octets remainder gets folded"); + + yield return new TestCaseData( + " HelloWorldHelloWorldHelloWorldHelloWorldHelloWorldHelloWorld ") + .Returns(" HelloWorldHelloWorldHelloWorldHelloWorldHelloWorldHelloWorld " + + SerializationConstants.LineBreak + " " + SerializationConstants.LineBreak) + .SetName("85 Octets long string with leading and trailing whitespace is folded without trimming"); + + var exactly310Octets = string.Concat(Enumerable.Repeat("HelloWorld", 31)); + + const string exactly310OctetsReturns = + "HelloWorldHelloWorldHelloWorldHelloWorldHelloWorldHelloWorldHelloWorldHello" + SerializationConstants.LineBreak + + " WorldHelloWorldHelloWorldHelloWorldHelloWorldHelloWorldHelloWorldHelloWorl" + SerializationConstants.LineBreak + + " dHelloWorldHelloWorldHelloWorldHelloWorldHelloWorldHelloWorldHelloWorldHel" + SerializationConstants.LineBreak + + " loWorldHelloWorldHelloWorldHelloWorldHelloWorldHelloWorldHelloWorldHelloWo" + SerializationConstants.LineBreak + + " rldHelloWorld" + SerializationConstants.LineBreak; - yield return new TestCaseData("HelloWorldHelloWorldHelloWorldHelloWorldHelloWorldHelloWorldHelloWorldHelloWorld") - .Returns(moderatelyLongReturns) - .SetName("Long string is folded"); + yield return new TestCaseData(exactly310Octets) + .Returns(exactly310OctetsReturns) + .SetName("310 Octets long string is split onto multiple lines at a width of 75 octets"); - yield return new TestCaseData(" HelloWorldHelloWorldHelloWorldHelloWorldHelloWorld ") - .Returns("HelloWorldHelloWorldHelloWorldHelloWorldHelloWorld" + SerializationConstants.LineBreak) - .SetName("Long string with front and rear whitespace is trimmed and fits in the allotted width"); + const string exactly75Octets = "HelloWorldHelloWorldHelloWorldHelloWorldHelloWorldHelloWorldHelloWorldHello"; + const string exactly75OctetsReturns = "HelloWorldHelloWorldHelloWorldHelloWorldHelloWorldHelloWorldHelloWorldHello" + SerializationConstants.LineBreak; - const string reallyLong = - "HelloWorldHelloWorldHelloWorldHelloWorldHelloWorldHelloWorldHelloWorldHelloWorldHelloWorldHelloWorldHelloWorldHelloWorldHelloWorldHelloWorldHelloWorldHelloWorldHelloWorldHelloWorldHelloWorldHelloWorldHelloWorldHelloWorldHelloWorldHelloWorldHelloWorldHelloWorldHelloWorldHelloWorldHelloWorldHelloWorldHelloWorld"; + yield return new TestCaseData(exactly75Octets) + .Returns(exactly75OctetsReturns) + .SetName("String with exactly 75 octets remains unfolded"); - const string reallyLongReturns = - "HelloWorldHelloWorldHelloWorldHelloWorldHelloWorldHelloWorldHelloWorldHell" + SerializationConstants.LineBreak - + " oWorldHelloWorldHelloWorldHelloWorldHelloWorldHelloWorldHelloWorldHelloWo" + SerializationConstants.LineBreak - + " rldHelloWorldHelloWorldHelloWorldHelloWorldHelloWorldHelloWorldHelloWorld" + SerializationConstants.LineBreak - + " HelloWorldHelloWorldHelloWorldHelloWorldHelloWorldHelloWorldHelloWorldHel" + SerializationConstants.LineBreak - + " loWorldHelloWorld" + SerializationConstants.LineBreak; + // String containing multi-byte characters with 210 Octets + // (Chinese "Hello World" repeated 10 times) + const string multiByteString210Octets = "こんにちは世界こんにちは世界こんにちは世界こんにちは世界こんにちは世界こんにちは世界こんにちは世界こんにちは世界こんにちは世界こんにちは世界"; + const string multiByteString210OctetsReturns = + "こんにちは世界こんにちは世界こんにちは世界こんにち" + SerializationConstants.LineBreak + + " は世界こんにちは世界こんにちは世界こんにちは世界" + SerializationConstants.LineBreak + + " こんにちは世界こんにちは世界こんにちは世界" + SerializationConstants.LineBreak; - yield return new TestCaseData(reallyLong) - .Returns(reallyLongReturns) - .SetName("Really long string is split onto multiple lines at a width of 75 chars, prefixed with a space"); + yield return new TestCaseData(multiByteString210Octets) + .Returns(multiByteString210OctetsReturns) + .SetName("String with multi-byte characters exceeding 75 octets is folded correctly"); } -} \ No newline at end of file +} diff --git a/Ical.Net/Ical.Net.csproj b/Ical.Net/Ical.Net.csproj index 0a5146336..74ff34d37 100644 --- a/Ical.Net/Ical.Net.csproj +++ b/Ical.Net/Ical.Net.csproj @@ -11,6 +11,9 @@ + + + <_Parameter1>Ical.Net.Tests, PublicKey=0024000004800000940000000602000000240000525341310004000001000100a1f790f70176d52efbd248577bdb292be2d0acc62f3227c523e267d64767f207f81536c77bb91d17031a5afbc2d69cd3b5b3b9c98fa8df2cd363ec90a08639a1213ad70079eff666bcc14cf6574b899f4ad0eac672c8f763291cb1e0a2304d371053158cb398b2e6f9eeb45db7d1b4d2bbba1f985676c5ca4602fab3671d34bf diff --git a/Ical.Net/Serialization/ComponentSerializer.cs b/Ical.Net/Serialization/ComponentSerializer.cs index e41af4322..90f9b4c7c 100644 --- a/Ical.Net/Serialization/ComponentSerializer.cs +++ b/Ical.Net/Serialization/ComponentSerializer.cs @@ -32,7 +32,7 @@ public override string SerializeToString(object obj) var sb = new StringBuilder(); var upperName = c.Name.ToUpperInvariant(); - sb.Append(TextUtil.FoldLines($"BEGIN:{upperName}")); + sb.FoldLines($"BEGIN:{upperName}"); // Get a serializer factory var sf = GetService(); @@ -56,7 +56,7 @@ public override string SerializeToString(object obj) sb.Append(serializer.SerializeToString(child)); } - sb.Append(TextUtil.FoldLines($"END:{upperName}")); + sb.FoldLines($"END:{upperName}"); return sb.ToString(); } @@ -79,4 +79,4 @@ public int Compare(ICalendarProperty x, ICalendarProperty y) : string.Compare(x.Name, y.Name, StringComparison.OrdinalIgnoreCase); } } -} \ No newline at end of file +} diff --git a/Ical.Net/Serialization/PropertySerializer.cs b/Ical.Net/Serialization/PropertySerializer.cs index 9784d6c97..4979c929c 100644 --- a/Ical.Net/Serialization/PropertySerializer.cs +++ b/Ical.Net/Serialization/PropertySerializer.cs @@ -39,65 +39,83 @@ public PropertySerializer(SerializationContext ctx) : base(ctx) { } var result = new StringBuilder(); foreach (var v in prop.Values.Where(value => value != null)) { - // Get a serializer to serialize the property's value. - // If we can't serialize the property's value, the next step is worthless anyway. - var valueSerializer = sf.Build(v.GetType(), SerializationContext) as IStringSerializer; - - // Iterate through each value to be serialized, - // and give it a property (with parameters). - // FIXME: this isn't always the way this is accomplished. - // Multiple values can often be serialized within the - // same property. How should we fix this? - - // NOTE: - // We Serialize the property's value first, as during - // serialization it may modify our parameters. - // FIXME: the "parameter modification" operation should - // be separated from serialization. Perhaps something - // like PreSerialize(), etc. - var value = valueSerializer?.SerializeToString(v); - - // Get the list of parameters we'll be serializing - var parameterList = prop.Parameters; - if (v is ICalendarDataType) - { - parameterList = ((ICalendarDataType) v).Parameters; - } + SerializeValue(result, prop, v, sf); + } - // The TZID property of an RDATE/EXDATE collection is owned by the PeriodList that contains it. - // It is allowed to have multiple EXDATE or RDATE collections, each with a different TZID. - // Using RecurrenceDate and ExceptionDate classes ensures, that all Periods in the - // PeriodList have the same TZID and PeriodKind, which allows PeriodList be serialized in one go. - // Here, to determine the timezone, we can safely use the first Period's timezone. - if (v is PeriodList periodList && periodList[0].TzId != null && periodList[0].TzId != "UTC" && - parameterList.All(p => string.Equals("TZID", p.Value, StringComparison.OrdinalIgnoreCase))) - { - parameterList.Set("TZID", periodList[0].TzId); - } + // Pop the object off the serialization context. + SerializationContext.Pop(); + return result.ToString(); + } + + private void SerializeValue(StringBuilder result, ICalendarProperty prop, object value, ISerializerFactory sf) + { + // Get a serializer to serialize the property's value. + // If we can't serialize the property's value, the next step is worthless anyway. + var valueSerializer = sf.Build(value.GetType(), SerializationContext) as IStringSerializer; + + // Iterate through each value to be serialized, + // and give it a property (with parameters). + // FIXME: this isn't always the way this is accomplished. + // Multiple values can often be serialized within the + // same property. How should we fix this? + + // NOTE: + // We Serialize the property's value first, as during + // serialization it may modify our parameters. + // FIXME: the "parameter modification" operation should + // be separated from serialization. Perhaps something + // like PreSerialize(), etc. + var serializedValue = valueSerializer?.SerializeToString(value); + + // Get the list of parameters we'll be serializing + var parameterList = GetParameterList(prop, value); + + // The TZID property of an RDATE/EXDATE collection is owned by the PeriodList that contains it. + // It is allowed to have multiple EXDATE or RDATE collections, each with a different TZID. + // Using RecurrenceDate and ExceptionDate classes ensures, that all Periods in the + // PeriodList have the same TZID and PeriodKind, which allows PeriodList be serialized in one go. + // Here, to determine the timezone, we can safely use the first Period's timezone. + if (value is PeriodList periodList) + { + UpdateTzId(parameterList, periodList); + } - var sb = new StringBuilder(); - sb.Append(prop.Name); - if (parameterList.Any()) + var sb = new StringBuilder(); + sb.Append(prop.Name); + if (parameterList.Count != 0) + { + // Get a serializer for parameters + var parameterSerializer = sf.Build(typeof(CalendarParameter), SerializationContext) as IStringSerializer; + if (parameterSerializer != null) { - // Get a serializer for parameters - var parameterSerializer = sf.Build(typeof(CalendarParameter), SerializationContext) as IStringSerializer; - if (parameterSerializer != null) + sb.Append(';'); + var first = true; + // Serialize each parameter and append to the StringBuilder + foreach (var param in parameterList) { - // Serialize each parameter - // Separate parameters with semicolons - sb.Append(";"); - sb.Append(string.Join(";", parameterList.Select(param => parameterSerializer.SerializeToString(param)))); + if (!first) sb.Append(';'); + + sb.Append(parameterSerializer.SerializeToString(param)); + first = false; } } - sb.Append(":"); - sb.Append(value); - - result.Append(TextUtil.FoldLines(sb.ToString())); } + sb.Append(':'); + sb.Append(serializedValue); - // Pop the object off the serialization context. - SerializationContext.Pop(); - return result.ToString(); + result.FoldLines(sb.ToString()); + } + + private static IParameterCollection GetParameterList(ICalendarProperty prop, object value) + => value is ICalendarDataType dataType ? dataType.Parameters : prop.Parameters; + + private static void UpdateTzId(IParameterCollection parameterList, PeriodList periodList) + { + if (periodList[0].TzId != null && periodList[0].TzId != "UTC" && + parameterList.All(p => string.Equals("TZID", p.Value, StringComparison.OrdinalIgnoreCase))) + { + parameterList.Set("TZID", periodList[0].TzId); + } } public override object? Deserialize(TextReader tr) => null; diff --git a/Ical.Net/Utility/TextUtil.cs b/Ical.Net/Utility/TextUtil.cs index 66c0cb0f5..31c72810b 100644 --- a/Ical.Net/Utility/TextUtil.cs +++ b/Ical.Net/Utility/TextUtil.cs @@ -4,8 +4,10 @@ // using System; +using System.Buffers; using System.Collections.Generic; using System.IO; +using System.Text; using System.Text.RegularExpressions; using Ical.Net.Serialization; @@ -13,23 +15,62 @@ namespace Ical.Net.Utility; internal static class TextUtil { - /// Folds lines at 75 characters, and prepends the next line with a space per RFC https://tools.ietf.org/html/rfc5545#section-3.1 - public static string FoldLines(string incoming) + /// + /// Folds lines to 75 octets. It appends a CrLf, and prepends the next line with a space. + /// per RFC https://tools.ietf.org/html/rfc5545#section-3.1 + /// + public static void FoldLines(this StringBuilder result, string inputLine) { - //The spec says nothing about trimming, but it seems reasonable... - var trimmed = incoming.Trim(); - if (trimmed.Length <= 75) + if (Encoding.UTF8.GetByteCount(inputLine) <= 75) { - return trimmed + SerializationConstants.LineBreak; + result.Append(inputLine); + result.Append(SerializationConstants.LineBreak); + return; } - const int takeLimit = 74; + // Use ArrayPool to rent arrays for processing + // Cannot use Span and stackalloc because netstandard2.0 does not support it + var arrayPool = ArrayPool.Shared; + var currentLineArray = arrayPool.Rent(76); // 75 characters + 1 space + + try + { + var currentLineIndex = 0; + var byteCount = 0; + var charCount = 0; + + while (charCount < inputLine.Length) + { + var currentCharByteCount = Encoding.UTF8.GetByteCount([inputLine[charCount]]); + + if (byteCount + currentCharByteCount > 75) + { + result.Append(currentLineArray, 0, currentLineIndex); + result.Append(SerializationConstants.LineBreak); - var firstLine = trimmed.Substring(0, takeLimit); - var remainder = trimmed.Substring(takeLimit, trimmed.Length - takeLimit); + currentLineIndex = 0; + byteCount = 1; + currentLineArray[currentLineIndex++] = ' '; + } - var chunkedRemainder = string.Join(SerializationConstants.LineBreak + " ", Chunk(remainder)); - return firstLine + SerializationConstants.LineBreak + " " + chunkedRemainder + SerializationConstants.LineBreak; + currentLineArray[currentLineIndex++] = inputLine[charCount]; + byteCount += currentCharByteCount; + charCount++; + } + + // Append the remaining characters to the result + if (currentLineIndex > 0) + { + result.Append(currentLineArray, 0, currentLineIndex); + } + + result.Append(SerializationConstants.LineBreak); + } + finally + { + // Return the rented array to the pool + arrayPool.Return(currentLineArray); + } } public static IEnumerable Chunk(string str, int chunkSize = 73) @@ -74,4 +115,4 @@ public static TextReader Normalize(string s, SerializationContext ctx) public static bool Contains(this string haystack, string needle, StringComparison stringComparison) => haystack.IndexOf(needle, stringComparison) >= 0; -} \ No newline at end of file +} From 1475742bfd9fc2580bbf3178a123c3626154d807 Mon Sep 17 00:00:00 2001 From: axunonb Date: Mon, 20 Jan 2025 23:28:20 +0100 Subject: [PATCH 2/3] Remove code redundant after #684 --- Ical.Net/Serialization/PropertySerializer.cs | 19 ------------------- 1 file changed, 19 deletions(-) diff --git a/Ical.Net/Serialization/PropertySerializer.cs b/Ical.Net/Serialization/PropertySerializer.cs index 4979c929c..96c036704 100644 --- a/Ical.Net/Serialization/PropertySerializer.cs +++ b/Ical.Net/Serialization/PropertySerializer.cs @@ -70,16 +70,6 @@ private void SerializeValue(StringBuilder result, ICalendarProperty prop, object // Get the list of parameters we'll be serializing var parameterList = GetParameterList(prop, value); - // The TZID property of an RDATE/EXDATE collection is owned by the PeriodList that contains it. - // It is allowed to have multiple EXDATE or RDATE collections, each with a different TZID. - // Using RecurrenceDate and ExceptionDate classes ensures, that all Periods in the - // PeriodList have the same TZID and PeriodKind, which allows PeriodList be serialized in one go. - // Here, to determine the timezone, we can safely use the first Period's timezone. - if (value is PeriodList periodList) - { - UpdateTzId(parameterList, periodList); - } - var sb = new StringBuilder(); sb.Append(prop.Name); if (parameterList.Count != 0) @@ -109,14 +99,5 @@ private void SerializeValue(StringBuilder result, ICalendarProperty prop, object private static IParameterCollection GetParameterList(ICalendarProperty prop, object value) => value is ICalendarDataType dataType ? dataType.Parameters : prop.Parameters; - private static void UpdateTzId(IParameterCollection parameterList, PeriodList periodList) - { - if (periodList[0].TzId != null && periodList[0].TzId != "UTC" && - parameterList.All(p => string.Equals("TZID", p.Value, StringComparison.OrdinalIgnoreCase))) - { - parameterList.Set("TZID", periodList[0].TzId); - } - } - public override object? Deserialize(TextReader tr) => null; } From cb09846a42fbe30f78925ce4455d0c969ee0df6f Mon Sep 17 00:00:00 2001 From: axunonb Date: Tue, 21 Jan 2025 10:07:05 +0100 Subject: [PATCH 3/3] Replace usage of ArrayPool, remove dependency System.Buffers --- Ical.Net/Ical.Net.csproj | 3 -- Ical.Net/Utility/TextUtil.cs | 54 +++++++++++++++--------------------- 2 files changed, 22 insertions(+), 35 deletions(-) diff --git a/Ical.Net/Ical.Net.csproj b/Ical.Net/Ical.Net.csproj index 74ff34d37..0a5146336 100644 --- a/Ical.Net/Ical.Net.csproj +++ b/Ical.Net/Ical.Net.csproj @@ -11,9 +11,6 @@ - - - <_Parameter1>Ical.Net.Tests, PublicKey=0024000004800000940000000602000000240000525341310004000001000100a1f790f70176d52efbd248577bdb292be2d0acc62f3227c523e267d64767f207f81536c77bb91d17031a5afbc2d69cd3b5b3b9c98fa8df2cd363ec90a08639a1213ad70079eff666bcc14cf6574b899f4ad0eac672c8f763291cb1e0a2304d371053158cb398b2e6f9eeb45db7d1b4d2bbba1f985676c5ca4602fab3671d34bf diff --git a/Ical.Net/Utility/TextUtil.cs b/Ical.Net/Utility/TextUtil.cs index 31c72810b..17182d8ed 100644 --- a/Ical.Net/Utility/TextUtil.cs +++ b/Ical.Net/Utility/TextUtil.cs @@ -4,7 +4,6 @@ // using System; -using System.Buffers; using System.Collections.Generic; using System.IO; using System.Text; @@ -30,47 +29,38 @@ public static void FoldLines(this StringBuilder result, string inputLine) // Use ArrayPool to rent arrays for processing // Cannot use Span and stackalloc because netstandard2.0 does not support it - var arrayPool = ArrayPool.Shared; - var currentLineArray = arrayPool.Rent(76); // 75 characters + 1 space + var currentLineArray = new char[76]; // 75 characters + 1 space - try - { - var currentLineIndex = 0; - var byteCount = 0; - var charCount = 0; - - while (charCount < inputLine.Length) - { - var currentCharByteCount = Encoding.UTF8.GetByteCount([inputLine[charCount]]); + var currentLineIndex = 0; + var byteCount = 0; + var charCount = 0; - if (byteCount + currentCharByteCount > 75) - { - result.Append(currentLineArray, 0, currentLineIndex); - result.Append(SerializationConstants.LineBreak); - - currentLineIndex = 0; - byteCount = 1; - currentLineArray[currentLineIndex++] = ' '; - } - - currentLineArray[currentLineIndex++] = inputLine[charCount]; - byteCount += currentCharByteCount; - charCount++; - } + while (charCount < inputLine.Length) + { + var currentCharByteCount = Encoding.UTF8.GetByteCount([inputLine[charCount]]); - // Append the remaining characters to the result - if (currentLineIndex > 0) + if (byteCount + currentCharByteCount > 75) { result.Append(currentLineArray, 0, currentLineIndex); + result.Append(SerializationConstants.LineBreak); + + currentLineIndex = 0; + byteCount = 1; + currentLineArray[currentLineIndex++] = ' '; } - result.Append(SerializationConstants.LineBreak); + currentLineArray[currentLineIndex++] = inputLine[charCount]; + byteCount += currentCharByteCount; + charCount++; } - finally + + // Append the remaining characters to the result + if (currentLineIndex > 0) { - // Return the rented array to the pool - arrayPool.Return(currentLineArray); + result.Append(currentLineArray, 0, currentLineIndex); } + + result.Append(SerializationConstants.LineBreak); } public static IEnumerable Chunk(string str, int chunkSize = 73)