From 26d58edf6d089cd815932a743d4023c4d6c10d1e Mon Sep 17 00:00:00 2001 From: Soreepeong Date: Wed, 20 Nov 2024 14:02:32 +0900 Subject: [PATCH 1/2] Add SeStringBuilder.GetViewAsMemory/Span `SeString`s created from `SeStringBuilder` is often used right away and discarded without escaping the code scope `.ToArray` is being called. This commit adds new functions that will expose a read-only view of the internal buffer. --- src/Lumina/Text/SeStringBuilder.cs | 34 ++++++++++++++++++++++++++---- 1 file changed, 30 insertions(+), 4 deletions(-) diff --git a/src/Lumina/Text/SeStringBuilder.cs b/src/Lumina/Text/SeStringBuilder.cs index 4793ff3a..57188b6e 100644 --- a/src/Lumina/Text/SeStringBuilder.cs +++ b/src/Lumina/Text/SeStringBuilder.cs @@ -94,21 +94,47 @@ public SeStringBuilder Clear( bool zeroBuffer = false ) return this; } - /// Gets the SeString as a new byte array. - /// A new byte array. - public byte[] ToArray() + /// Gets the view of SeString being built. + /// View of the SeString being built. + /// + ///

Returned view is invalidated upon any mutation to this builder, including and . If + /// is being used, then returning to the pool also will invalidate the returned view.

+ ///

After the last element (right after the end of the returned memory/span), NUL is present. You can pin the returned value and use the pointer + /// to the first element as a pointer to null-terminated string.

+ ///
+ public ReadOnlyMemory< byte > GetViewAsMemory() { if( _mss.Count != 1 ) throw new InvalidOperationException( "The string is incomplete, due to non-empty stack." ); - return _mss[ 0 ].Stream.ToArray(); + + var stream = _mss[ 0 ].Stream; + + // Force null termination. + stream.Capacity = Math.Max( stream.Capacity, (int) stream.Length + 1 ); + stream.GetBuffer()[ stream.Length ] = 0; + + return stream.GetBuffer().AsMemory( 0, (int) stream.Length ); } + /// + public ReadOnlySpan< byte > GetViewAsSpan() => GetViewAsMemory().Span; + + /// Gets the SeString as a new byte array. + /// A new byte array. + /// If the created value does not escape the code scope this function is being called, consider using or + /// . + public byte[] ToArray() => GetViewAsMemory().ToArray(); + /// Gets the SeString as a new instance of . /// A new instance of . + /// If the created value does not escape the code scope this function is being called, consider using or + /// . public SeString ToSeString() => new( ToArray() ); /// Gets the SeString as a new instance of . /// A new instance of . + /// If the created value does not escape the code scope this function is being called, consider using or + /// . public ReadOnlySeString ToReadOnlySeString() => ToArray(); /// From 3675b80cf8408539ef1fa727306e692f68530f3b Mon Sep 17 00:00:00 2001 From: Soreepeong Date: Wed, 20 Nov 2024 14:12:59 +0900 Subject: [PATCH 2/2] Add null termination test --- src/Lumina.Tests/SeStringBuilderTests.cs | 21 +++++++++++++++++++-- 1 file changed, 19 insertions(+), 2 deletions(-) diff --git a/src/Lumina.Tests/SeStringBuilderTests.cs b/src/Lumina.Tests/SeStringBuilderTests.cs index f008a888..f954bcb1 100644 --- a/src/Lumina.Tests/SeStringBuilderTests.cs +++ b/src/Lumina.Tests/SeStringBuilderTests.cs @@ -443,6 +443,24 @@ public void AddonIsParsedCorrectly() } } + [Fact] + public unsafe void SpanViewNullTerminationTest() + { + var test = new SeStringBuilder() + .AppendBold( "Test" ) + .Append( "Asdf" ) + .AppendItalicized( "Aaaaa" ); + var expected = + "\x02\x19\x02\x02\x03"u8 + "Test"u8 + "\x02\x19\x02\x01\x03"u8 + + "Asdf"u8 + + "\x02\x1A\x02\x02\x03"u8 + "Aaaaa"u8 + "\x02\x1A\x02\x01\x03"u8; + + var span = test.GetViewAsSpan(); + Assert.True( span.SequenceEqual( expected ) ); + fixed( byte* p = span ) + Assert.Equal( 0 , p[ span.Length ]); + } + [Fact] public unsafe void InterpolationHandlerTest1() { @@ -600,8 +618,7 @@ public void AllSheetsTextColumnCodec() var languages = header?.Languages ?? [Language.None]; foreach( var language in languages ) { - if( gameData.Excel.GetSheet( language, sheetName ) is not { } sheet ) - continue; + var sheet = gameData.Excel.GetSheet< RawRow >( language, sheetName ); var stringColumns = sheet.Columns.Where( c => c.Type == ExcelColumnDataType.String ).Select( c => c.Offset ).ToArray(); foreach( var row in sheet )