feat: Adds Latin1 text support (#2317)
## Summary - Adds proper Latin1 encoding support, replacing CP1252 usage throughout the codebase - Adds specialized, optimized string decoding methods with safe string filtering for each encoding type - Filters invalid Unicode characters (C0/C1 control codes, non-characters) by removal rather than replacement since the UO client renders nothing for these characters - Fixes UTF-16 null terminator position handling to correctly advance by 2 bytes ## Changes TextEncoding.cs - Added SearchValues-based invalid byte/char detection for efficient filtering - Added encoding-specific GetString methods: GetStringAscii, GetStringLatin1, GetStringUtf8, GetStringBigUni, GetStringLittleUni - Each method supports a safeString parameter for filtering invalid characters - Little-endian UTF-16 uses direct memory cast for zero-copy decoding on LE systems - Invalid characters are removed (not replaced with U+FFFD) since the client renders nothing for them SpanReader.cs - Added ReadLatin1() and ReadLatin1Safe() methods - Rewrote encoding-specific read methods to use optimized TextEncoding.GetString* methods - Fixed UTF-16 null terminator handling: position now correctly advances by byteLength (2) instead of 1 SpanWriter.cs - Added WriteLatin1 and WriteLatin1Null methods ## Packet Updates - Updated all packet code to use Latin1 encoding instead of CP1252 - Affected: account packets, equipment packets, menu packets, message packets, mobile packets, player packets, secure trade packets, vendor packets, gump packets, book packets, mahjong packets ## Filtering Behavior Invalid characters filtered in safe mode: ``` ┌───────────────┬────────────────────────┐ │ Range │ Description │ ├───────────────┼────────────────────────┤ │ 0x00-0x1F │ C0 control codes │ ├───────────────┼────────────────────────┤ │ 0x7F │ DEL │ ├───────────────┼────────────────────────┤ │ 0x80-0x9F │ C1 control codes │ ├───────────────┼────────────────────────┤ │ 0xFFFE-0xFFFF │ Unicode non-characters │ └───────────────┴────────────────────────┘ ``` Note: Surrogate pairs (0xD800-0xDFFF) are not filtered because proper validation requires context checking for paired vs unpaired surrogates. The UO client renders nothing for these anyway. ## Test Plan - All 631 Server.Tests pass - Verified client rendering behavior using TestUnicodeGump command (pages 1-5) - Confirmed U+FFFD, unpaired surrogates, and non-characters all render as blank in client - Verified Latin1 characters (0xA0-0xFF) display correctly - Verified C1 control codes (0x80-0x9F) are filtered and don't display
This commit is contained in:
parent
1e4c32c809
commit
0404251638
27 changed files with 735 additions and 96 deletions
|
|
@ -291,7 +291,8 @@ public class SpanReaderTests
|
|||
var result = reader.ReadLittleUni();
|
||||
|
||||
Assert.Equal("Hi", result);
|
||||
Assert.Equal(5, reader.Position);
|
||||
// Position 6: 4 bytes for "Hi" + 2 bytes for UTF-16 null terminator
|
||||
Assert.Equal(6, reader.Position);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
|
|
@ -309,12 +310,14 @@ public class SpanReaderTests
|
|||
[Fact]
|
||||
public void TestReadLittleUniSafe()
|
||||
{
|
||||
ReadOnlySpan<byte> buffer = [(byte)'H', 0, 0xFF, 0xD8, (byte)'i', 0];
|
||||
// Test with C1 control code (0x85 = NEL) which should be filtered
|
||||
ReadOnlySpan<byte> buffer = [(byte)'H', 0, 0x85, 0x00, (byte)'i', 0];
|
||||
var reader = new SpanReader(buffer);
|
||||
|
||||
var result = reader.ReadLittleUniSafe();
|
||||
|
||||
Assert.Equal("H\uFFFDi", result);
|
||||
// C1 control (0x0085) removed - client renders nothing for invalid chars
|
||||
Assert.Equal("Hi", result);
|
||||
Assert.Equal(6, reader.Position);
|
||||
}
|
||||
|
||||
|
|
@ -339,7 +342,8 @@ public class SpanReaderTests
|
|||
var result = reader.ReadBigUni();
|
||||
|
||||
Assert.Equal("Hi", result);
|
||||
Assert.Equal(5, reader.Position);
|
||||
// Position 6: 4 bytes for "Hi" + 2 bytes for UTF-16 null terminator
|
||||
Assert.Equal(6, reader.Position);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
|
|
@ -357,12 +361,14 @@ public class SpanReaderTests
|
|||
[Fact]
|
||||
public void TestReadBigUniSafe()
|
||||
{
|
||||
ReadOnlySpan<byte> buffer = [0, (byte)'H', 0xD8, 0xFF, 0, (byte)'i'];
|
||||
// Test with C1 control code (0x0085 = NEL) which should be filtered
|
||||
ReadOnlySpan<byte> buffer = [0, (byte)'H', 0x00, 0x85, 0, (byte)'i'];
|
||||
var reader = new SpanReader(buffer);
|
||||
|
||||
var result = reader.ReadBigUniSafe();
|
||||
|
||||
Assert.Equal("H\uFFFDi", result);
|
||||
// C1 control (0x0085) removed - client renders nothing for invalid chars
|
||||
Assert.Equal("Hi", result);
|
||||
Assert.Equal(6, reader.Position);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
/*************************************************************************
|
||||
* ModernUO *
|
||||
* Copyright 2019-2025 - ModernUO Development Team *
|
||||
* Copyright 2019-2026 - ModernUO Development Team *
|
||||
* Email: hi@modernuo.com *
|
||||
* File: SpanReader.cs *
|
||||
* *
|
||||
|
|
@ -207,49 +207,241 @@ public ref struct SpanReader
|
|||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public string ReadLittleUniSafe(int fixedLength) => ReadString(TextEncoding.UnicodeLE, true, fixedLength);
|
||||
public string ReadLittleUniSafe(int fixedLength) => ReadStringLittleUni(true, fixedLength);
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public string ReadLittleUniSafe() => ReadString(TextEncoding.UnicodeLE, true);
|
||||
public string ReadLittleUniSafe() => ReadStringLittleUni(true);
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public string ReadLittleUni(int fixedLength) => ReadString(TextEncoding.UnicodeLE, false, fixedLength);
|
||||
public string ReadLittleUni(int fixedLength) => ReadStringLittleUni(false, fixedLength);
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public string ReadLittleUni() => ReadString(TextEncoding.UnicodeLE);
|
||||
public string ReadLittleUni() => ReadStringLittleUni(false);
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public string ReadBigUniSafe(int fixedLength) => ReadString(TextEncoding.Unicode, true, fixedLength);
|
||||
public string ReadBigUniSafe(int fixedLength) => ReadStringBigUni(true, fixedLength);
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public string ReadBigUniSafe() => ReadString(TextEncoding.Unicode, true);
|
||||
public string ReadBigUniSafe() => ReadStringBigUni(true);
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public string ReadBigUni(int fixedLength) => ReadString(TextEncoding.Unicode, false, fixedLength);
|
||||
public string ReadBigUni(int fixedLength) => ReadStringBigUni(false, fixedLength);
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public string ReadBigUni() => ReadString(TextEncoding.Unicode);
|
||||
public string ReadBigUni() => ReadStringBigUni(false);
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public string ReadUTF8Safe(int fixedLength) => ReadString(TextEncoding.UTF8, true, fixedLength);
|
||||
public string ReadUTF8Safe(int fixedLength) => ReadStringUtf8(true, fixedLength);
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public string ReadUTF8Safe() => ReadString(TextEncoding.UTF8, true);
|
||||
public string ReadUTF8Safe() => ReadStringUtf8(true);
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public string ReadUTF8() => ReadString(TextEncoding.UTF8);
|
||||
public string ReadUTF8(int fixedLength) => ReadStringUtf8(false, fixedLength);
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public string ReadAsciiSafe(int fixedLength) => ReadString(Encoding.ASCII, true, fixedLength);
|
||||
public string ReadUTF8() => ReadStringUtf8(false);
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public string ReadAsciiSafe() => ReadString(Encoding.ASCII, true);
|
||||
public string ReadAsciiSafe(int fixedLength) => ReadStringAscii(true, fixedLength);
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public string ReadAscii(int fixedLength) => ReadString(Encoding.ASCII, false, fixedLength);
|
||||
public string ReadAsciiSafe() => ReadStringAscii(true);
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public string ReadAscii() => ReadString(Encoding.ASCII);
|
||||
public string ReadAscii(int fixedLength) => ReadStringAscii(false, fixedLength);
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public string ReadAscii() => ReadStringAscii(false);
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public string ReadLatin1Safe(int fixedLength) => ReadStringLatin1(true, fixedLength);
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public string ReadLatin1Safe() => ReadStringLatin1(true);
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public string ReadLatin1(int fixedLength) => ReadStringLatin1(false, fixedLength);
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public string ReadLatin1() => ReadStringLatin1(false);
|
||||
|
||||
/// <summary>
|
||||
/// Reads a big-endian UTF-16 string, filtering control codes and non-characters in safe mode.
|
||||
/// </summary>
|
||||
private string ReadStringBigUni(bool safeString, int fixedLength = -1)
|
||||
{
|
||||
if (fixedLength == 0)
|
||||
{
|
||||
return "";
|
||||
}
|
||||
|
||||
const int byteLength = 2;
|
||||
var isFixedLength = fixedLength > -1;
|
||||
|
||||
var remaining = Remaining;
|
||||
int size;
|
||||
if (isFixedLength)
|
||||
{
|
||||
size = fixedLength * byteLength;
|
||||
if (size > remaining)
|
||||
{
|
||||
throw new EndOfStreamException("Cannot read past the end of the buffer.");
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// Ensure even number of bytes
|
||||
size = remaining - (remaining & 1);
|
||||
}
|
||||
|
||||
var span = _buffer.Slice(Position, size);
|
||||
var index = span.IndexOfTerminator(byteLength);
|
||||
|
||||
if (index > -1)
|
||||
{
|
||||
span = _buffer.Slice(Position, index);
|
||||
}
|
||||
|
||||
Position += isFixedLength || index < 0 ? size : index + byteLength;
|
||||
|
||||
return TextEncoding.GetStringBigUni(span, safeString);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Reads a little-endian UTF-16 string, filtering control codes and non-characters in safe mode.
|
||||
/// </summary>
|
||||
private string ReadStringLittleUni(bool safeString, int fixedLength = -1)
|
||||
{
|
||||
if (fixedLength == 0)
|
||||
{
|
||||
return "";
|
||||
}
|
||||
|
||||
const int byteLength = 2;
|
||||
var isFixedLength = fixedLength > -1;
|
||||
|
||||
var remaining = Remaining;
|
||||
int size;
|
||||
if (isFixedLength)
|
||||
{
|
||||
size = fixedLength * byteLength;
|
||||
if (size > remaining)
|
||||
{
|
||||
throw new EndOfStreamException("Cannot read past the end of the buffer.");
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// Ensure even number of bytes
|
||||
size = remaining - (remaining & 1);
|
||||
}
|
||||
|
||||
var span = _buffer.Slice(Position, size);
|
||||
var index = span.IndexOfTerminator(byteLength);
|
||||
|
||||
if (index > -1)
|
||||
{
|
||||
span = _buffer.Slice(Position, index);
|
||||
}
|
||||
|
||||
Position += isFixedLength || index < 0 ? size : index + byteLength;
|
||||
|
||||
return TextEncoding.GetStringLittleUni(span, safeString);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Reads an ASCII string, filtering C0 control codes and DEL in safe mode.
|
||||
/// </summary>
|
||||
private string ReadStringAscii(bool safeString, int fixedLength = -1)
|
||||
{
|
||||
if (fixedLength == 0)
|
||||
{
|
||||
return "";
|
||||
}
|
||||
|
||||
var isFixedLength = fixedLength > -1;
|
||||
|
||||
int size = isFixedLength ? fixedLength : Remaining;
|
||||
if (size > Remaining)
|
||||
{
|
||||
throw new EndOfStreamException("Cannot read past the end of the buffer.");
|
||||
}
|
||||
|
||||
var span = _buffer.Slice(Position, size);
|
||||
var index = span.IndexOf((byte)0);
|
||||
|
||||
if (index > -1)
|
||||
{
|
||||
span = _buffer.Slice(Position, index);
|
||||
}
|
||||
|
||||
Position += isFixedLength || index < 0 ? size : index + 1;
|
||||
|
||||
return TextEncoding.GetStringAscii(span, safeString);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Reads a UTF-8 string, filtering control codes and non-characters in safe mode.
|
||||
/// </summary>
|
||||
private string ReadStringUtf8(bool safeString, int fixedLength = -1)
|
||||
{
|
||||
if (fixedLength == 0)
|
||||
{
|
||||
return "";
|
||||
}
|
||||
|
||||
var isFixedLength = fixedLength > -1;
|
||||
|
||||
int size = isFixedLength ? fixedLength : Remaining;
|
||||
if (size > Remaining)
|
||||
{
|
||||
throw new EndOfStreamException("Cannot read past the end of the buffer.");
|
||||
}
|
||||
|
||||
var span = _buffer.Slice(Position, size);
|
||||
var index = span.IndexOf((byte)0);
|
||||
|
||||
if (index > -1)
|
||||
{
|
||||
span = _buffer.Slice(Position, index);
|
||||
}
|
||||
|
||||
Position += isFixedLength || index < 0 ? size : index + 1;
|
||||
|
||||
return TextEncoding.GetStringUtf8(span, safeString);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Reads a Latin1 string, filtering C0/C1 control codes in safe mode.
|
||||
/// </summary>
|
||||
private string ReadStringLatin1(bool safeString, int fixedLength = -1)
|
||||
{
|
||||
if (fixedLength == 0)
|
||||
{
|
||||
return "";
|
||||
}
|
||||
|
||||
var isFixedLength = fixedLength > -1;
|
||||
|
||||
int size = isFixedLength ? fixedLength : Remaining;
|
||||
if (size > Remaining)
|
||||
{
|
||||
throw new EndOfStreamException("Cannot read past the end of the buffer.");
|
||||
}
|
||||
|
||||
var span = _buffer.Slice(Position, size);
|
||||
var index = span.IndexOf((byte)0);
|
||||
|
||||
if (index > -1)
|
||||
{
|
||||
span = _buffer.Slice(Position, index);
|
||||
}
|
||||
|
||||
Position += isFixedLength || index < 0 ? size : index + 1;
|
||||
|
||||
return TextEncoding.GetStringLatin1(span, safeString);
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public int Seek(int offset, SeekOrigin origin)
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
/*************************************************************************
|
||||
* ModernUO *
|
||||
* Copyright 2019-2025 - ModernUO Development Team *
|
||||
* Copyright 2019-2026 - ModernUO Development Team *
|
||||
* Email: hi@modernuo.com *
|
||||
* File: SpanWriter.cs *
|
||||
* *
|
||||
|
|
@ -273,8 +273,7 @@ public ref struct SpanWriter
|
|||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public void WriteAscii(char chr) => Write((byte)chr);
|
||||
|
||||
public void WriteAscii(
|
||||
ref RawInterpolatedStringHandler handler)
|
||||
public void WriteAscii(ref RawInterpolatedStringHandler handler)
|
||||
{
|
||||
Write(handler.Text, Encoding.ASCII);
|
||||
handler.Clear();
|
||||
|
|
@ -289,9 +288,22 @@ public ref struct SpanWriter
|
|||
handler.Clear();
|
||||
}
|
||||
|
||||
public void Write(
|
||||
Encoding encoding,
|
||||
public void WriteLatin1(ref RawInterpolatedStringHandler handler)
|
||||
{
|
||||
Write(handler.Text, Encoding.Latin1);
|
||||
handler.Clear();
|
||||
}
|
||||
|
||||
public void WriteLatin1(
|
||||
IFormatProvider? formatProvider,
|
||||
[InterpolatedStringHandlerArgument("formatProvider")]
|
||||
ref RawInterpolatedStringHandler handler)
|
||||
{
|
||||
Write(handler.Text, Encoding.Latin1);
|
||||
handler.Clear();
|
||||
}
|
||||
|
||||
public void Write(Encoding encoding, ref RawInterpolatedStringHandler handler)
|
||||
{
|
||||
Write(handler.Text, encoding);
|
||||
handler.Clear();
|
||||
|
|
@ -388,6 +400,19 @@ public ref struct SpanWriter
|
|||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public void WriteAscii(string value, int fixedLength) => Write(value, Encoding.ASCII, fixedLength);
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public void WriteLatin1(string value) => Write(value, Encoding.Latin1);
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public void WriteLatin1(string value, int fixedLength) => Write(value, Encoding.Latin1, fixedLength);
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public void WriteLatin1Null(string value)
|
||||
{
|
||||
Write(value, Encoding.Latin1);
|
||||
Write((byte)0); // '\0'
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public void Clear(int count)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -89,7 +89,7 @@ public static class OutgoingAccountPackets
|
|||
var name = (m.RawName?.Trim()).DefaultIfNullOrEmpty("-no name-");
|
||||
|
||||
count++;
|
||||
writer.WriteAscii(name, 30);
|
||||
writer.WriteLatin1(name, 30);
|
||||
writer.Clear(30); // Password (empty)
|
||||
}
|
||||
}
|
||||
|
|
@ -276,7 +276,7 @@ public static class OutgoingAccountPackets
|
|||
else
|
||||
{
|
||||
var name = (m.RawName?.Trim()).DefaultIfNullOrEmpty("-no name-");
|
||||
writer.WriteAscii(name, 30);
|
||||
writer.WriteLatin1(name, 30);
|
||||
writer.Clear(30); // password
|
||||
}
|
||||
}
|
||||
|
|
@ -341,7 +341,7 @@ public static class OutgoingAccountPackets
|
|||
else
|
||||
{
|
||||
var name = (m.RawName?.Trim()).DefaultIfNullOrEmpty("-no name-");
|
||||
writer.WriteAscii(name, 30);
|
||||
writer.WriteLatin1(name, 30);
|
||||
writer.Clear(30); // password
|
||||
}
|
||||
}
|
||||
|
|
@ -353,8 +353,8 @@ public static class OutgoingAccountPackets
|
|||
var ci = cityInfo[i];
|
||||
|
||||
writer.Write((byte)i);
|
||||
writer.WriteAscii(ci.City, textLength);
|
||||
writer.WriteAscii(ci.Building, textLength);
|
||||
writer.WriteLatin1(ci.City, textLength);
|
||||
writer.WriteLatin1(ci.Building, textLength);
|
||||
if (client70130)
|
||||
{
|
||||
writer.Write(ci.X);
|
||||
|
|
@ -428,7 +428,7 @@ public static class OutgoingAccountPackets
|
|||
var si = info[i];
|
||||
|
||||
writer.Write((ushort)i);
|
||||
writer.WriteAscii(si.Name, 32);
|
||||
writer.WriteLatin1(si.Name, 32);
|
||||
writer.Write((byte)si.FullPercent);
|
||||
writer.Write((sbyte)si.TimeZone);
|
||||
// UO only supports IPv4
|
||||
|
|
|
|||
|
|
@ -63,7 +63,7 @@ public static class OutgoingEquipmentPackets
|
|||
writer.Write(-3); // crafted by
|
||||
|
||||
writer.Write((ushort)crafterName.Length);
|
||||
writer.WriteAscii(crafterName);
|
||||
writer.WriteLatin1(crafterName);
|
||||
}
|
||||
|
||||
if (unidentified)
|
||||
|
|
|
|||
|
|
@ -62,7 +62,7 @@ public static class OutgoingMenuPackets
|
|||
|
||||
if (question != null)
|
||||
{
|
||||
writer.WriteAscii(question);
|
||||
writer.WriteLatin1(question);
|
||||
}
|
||||
|
||||
writer.Write((byte)entriesLength);
|
||||
|
|
@ -84,7 +84,7 @@ public static class OutgoingMenuPackets
|
|||
{
|
||||
var nameLength = name.Length;
|
||||
writer.Write((byte)nameLength);
|
||||
writer.WriteAscii(name);
|
||||
writer.WriteLatin1(name);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -120,7 +120,7 @@ public static class OutgoingMenuPackets
|
|||
|
||||
if (question != null)
|
||||
{
|
||||
writer.WriteAscii(question);
|
||||
writer.WriteLatin1(question);
|
||||
}
|
||||
|
||||
writer.Write((byte)answersLength);
|
||||
|
|
@ -139,7 +139,7 @@ public static class OutgoingMenuPackets
|
|||
{
|
||||
var nameLength = answer.Length;
|
||||
writer.Write((byte)nameLength);
|
||||
writer.WriteAscii(answer);
|
||||
writer.WriteLatin1(answer);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -79,7 +79,7 @@ public static class OutgoingMessagePackets
|
|||
writer.Write((short)hue);
|
||||
writer.Write((short)font);
|
||||
writer.Write(number);
|
||||
writer.WriteAscii(name, 30);
|
||||
writer.WriteLatin1(name, 30);
|
||||
writer.WriteLittleUniNull(args);
|
||||
|
||||
writer.WritePacketLength();
|
||||
|
|
@ -139,8 +139,8 @@ public static class OutgoingMessagePackets
|
|||
writer.Write((short)font);
|
||||
writer.Write(number);
|
||||
writer.Write((byte)affixType);
|
||||
writer.WriteAscii(name, 30);
|
||||
writer.WriteAsciiNull(affix);
|
||||
writer.WriteLatin1(name, 30);
|
||||
writer.WriteLatin1Null(affix);
|
||||
writer.WriteBigUniNull(args);
|
||||
|
||||
writer.WritePacketLength();
|
||||
|
|
@ -214,13 +214,13 @@ public static class OutgoingMessagePackets
|
|||
writer.Write((short)font);
|
||||
if (ascii)
|
||||
{
|
||||
writer.WriteAscii(name, 30);
|
||||
writer.WriteAsciiNull(text);
|
||||
writer.WriteLatin1(name, 30);
|
||||
writer.WriteLatin1Null(text);
|
||||
}
|
||||
else
|
||||
{
|
||||
writer.WriteAscii(lang, 4);
|
||||
writer.WriteAscii(name, 30);
|
||||
writer.WriteLatin1(name, 30);
|
||||
writer.WriteBigUniNull(text);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -309,7 +309,7 @@ public static class OutgoingMobilePackets
|
|||
writer.Write((byte)0x98); // Packet ID
|
||||
writer.Write((ushort)37);
|
||||
writer.Write(m.Serial);
|
||||
writer.WriteAscii(m.Name ?? "", 29);
|
||||
writer.WriteLatin1(m.Name ?? "", 29);
|
||||
writer.Write((byte)0); // Null terminator
|
||||
|
||||
ns.Send(writer.Span);
|
||||
|
|
@ -507,7 +507,7 @@ public static class OutgoingMobilePackets
|
|||
writer.Write((byte)0x11); // Packet ID
|
||||
writer.Seek(2, SeekOrigin.Current);
|
||||
writer.Write(beheld.Serial);
|
||||
writer.WriteAscii(name, 30);
|
||||
writer.WriteLatin1(name, 30);
|
||||
writer.WriteAttribute(beheld.HitsMax, beheld.Hits, version == 0, true);
|
||||
writer.Write(canBeRenamed);
|
||||
writer.Write((byte)version);
|
||||
|
|
|
|||
|
|
@ -77,7 +77,7 @@ public static class OutgoingPlayerPackets
|
|||
writer.Write((byte)0xB8); // Packet ID
|
||||
writer.Write((ushort)length);
|
||||
writer.Write(m);
|
||||
writer.WriteAsciiNull(header);
|
||||
writer.WriteLatin1Null(header);
|
||||
writer.WriteBigUniNull(footer);
|
||||
writer.WriteBigUniNull(body);
|
||||
|
||||
|
|
@ -264,7 +264,7 @@ public static class OutgoingPlayerPackets
|
|||
var writer = new SpanWriter(stackalloc byte[66]);
|
||||
writer.Write((byte)0x88); // Packet ID
|
||||
writer.Write(m);
|
||||
writer.WriteAscii(title, 60);
|
||||
writer.WriteLatin1(title, 60);
|
||||
writer.Write(flags);
|
||||
|
||||
ns.Send(writer.Span);
|
||||
|
|
@ -302,7 +302,7 @@ public static class OutgoingPlayerPackets
|
|||
writer.Write((byte)type);
|
||||
writer.Write(tip);
|
||||
writer.Write((ushort)text.Length);
|
||||
writer.WriteAscii(text);
|
||||
writer.WriteLatin1(text);
|
||||
|
||||
ns.Send(writer.Span);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -45,7 +45,7 @@ public static class OutgoingSecureTradePackets
|
|||
writer.Write(second.Serial);
|
||||
writer.Write(true);
|
||||
|
||||
writer.WriteAscii(name ?? "", 30);
|
||||
writer.WriteLatin1(name ?? "", 30);
|
||||
|
||||
ns.Send(writer.Span);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -102,7 +102,7 @@ public static class OutgoingVendorBuyPackets
|
|||
var desc = bis.Description ?? "";
|
||||
|
||||
writer.Write((byte)(desc.Length + 1));
|
||||
writer.WriteAsciiNull(desc);
|
||||
writer.WriteLatin1Null(desc);
|
||||
}
|
||||
|
||||
ns.Send(writer.Span);
|
||||
|
|
|
|||
|
|
@ -55,7 +55,7 @@ public static class OutgoingVendorSellPackets
|
|||
var name = (item.Name?.Trim()).DefaultIfNullOrEmpty(sis.Name ?? "");
|
||||
|
||||
writer.Write((ushort)name.Length);
|
||||
writer.WriteAscii(name);
|
||||
writer.WriteLatin1(name);
|
||||
}
|
||||
|
||||
writer.WritePacketLength();
|
||||
|
|
|
|||
|
|
@ -37,7 +37,7 @@
|
|||
<PackageReference Include="CommunityToolkit.HighPerformance" Version="8.4.0" />
|
||||
<PackageReference Include="LibDeflate.Bindings" Version="1.0.2.120" />
|
||||
<PackageReference Include="PollGroup" Version="1.7.1" />
|
||||
<PackageReference Include="System.IO.Hashing" Version="10.0.1" />
|
||||
<PackageReference Include="System.IO.Hashing" Version="10.0.2" />
|
||||
|
||||
<PackageReference Include="ModernUO.Serialization.Annotations" Version="2.14.0" />
|
||||
<PackageReference Include="ModernUO.Serialization.Generator" Version="2.14.1" />
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
/*************************************************************************
|
||||
* ModernUO *
|
||||
* Copyright 2019-2025 - ModernUO Development Team *
|
||||
* Copyright 2019-2026 - ModernUO Development Team *
|
||||
* Email: hi@modernuo.com *
|
||||
* File: TextEncoding.cs *
|
||||
* *
|
||||
|
|
@ -14,7 +14,9 @@
|
|||
*************************************************************************/
|
||||
|
||||
using System;
|
||||
using System.Buffers;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Text;
|
||||
using Server.Buffers;
|
||||
|
||||
|
|
@ -22,11 +24,46 @@ namespace Server.Text;
|
|||
|
||||
public static class TextEncoding
|
||||
{
|
||||
private static Encoding m_UTF8, m_Unicode, m_UnicodeLE;
|
||||
private static Encoding _utf8, _unicode, _unicodeLE;
|
||||
|
||||
public static Encoding UTF8 => m_UTF8 ??= new UTF8Encoding(false, false);
|
||||
public static Encoding Unicode => m_Unicode ??= new UnicodeEncoding(true, false, false);
|
||||
public static Encoding UnicodeLE => m_UnicodeLE ??= new UnicodeEncoding(false, false, false);
|
||||
// SearchValues for invalid ASCII display bytes (C0: 0x00-0x1F, DEL: 0x7F)
|
||||
// Note: Bytes >= 0x80 are invalid ASCII and become '?' when decoded, which is acceptable
|
||||
private static readonly SearchValues<byte> InvalidAsciiBytes = SearchValues.Create(
|
||||
0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07,
|
||||
0x08, 0x09, 0x0A, 0x0B, 0x0C, 0x0D, 0x0E, 0x0F,
|
||||
0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17,
|
||||
0x18, 0x19, 0x1A, 0x1B, 0x1C, 0x1D, 0x1E, 0x1F,
|
||||
0x7F
|
||||
);
|
||||
|
||||
// SearchValues for invalid Latin1 bytes (C0: 0x00-0x1F, C1: 0x80-0x9F)
|
||||
private static readonly SearchValues<byte> InvalidLatin1Bytes = SearchValues.Create(
|
||||
0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07,
|
||||
0x08, 0x09, 0x0A, 0x0B, 0x0C, 0x0D, 0x0E, 0x0F,
|
||||
0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17,
|
||||
0x18, 0x19, 0x1A, 0x1B, 0x1C, 0x1D, 0x1E, 0x1F,
|
||||
0x80, 0x81, 0x82, 0x83, 0x84, 0x85, 0x86, 0x87,
|
||||
0x88, 0x89, 0x8A, 0x8B, 0x8C, 0x8D, 0x8E, 0x8F,
|
||||
0x90, 0x91, 0x92, 0x93, 0x94, 0x95, 0x96, 0x97,
|
||||
0x98, 0x99, 0x9A, 0x9B, 0x9C, 0x9D, 0x9E, 0x9F
|
||||
);
|
||||
|
||||
// SearchValues for invalid Latin1 chars (same ranges as bytes)
|
||||
private static readonly SearchValues<char> InvalidLatin1Chars = SearchValues.Create(
|
||||
'\x00', '\x01', '\x02', '\x03', '\x04', '\x05', '\x06', '\x07',
|
||||
'\x08', '\x09', '\x0A', '\x0B', '\x0C', '\x0D', '\x0E', '\x0F',
|
||||
'\x10', '\x11', '\x12', '\x13', '\x14', '\x15', '\x16', '\x17',
|
||||
'\x18', '\x19', '\x1A', '\x1B', '\x1C', '\x1D', '\x1E', '\x1F',
|
||||
'\x80', '\x81', '\x82', '\x83', '\x84', '\x85', '\x86', '\x87',
|
||||
'\x88', '\x89', '\x8A', '\x8B', '\x8C', '\x8D', '\x8E', '\x8F',
|
||||
'\x90', '\x91', '\x92', '\x93', '\x94', '\x95', '\x96', '\x97',
|
||||
'\x98', '\x99', '\x9A', '\x9B', '\x9C', '\x9D', '\x9E', '\x9F'
|
||||
);
|
||||
|
||||
public static Encoding UTF8 => _utf8 ??= new UTF8Encoding(false, false);
|
||||
public static Encoding Unicode => _unicode ??= new UnicodeEncoding(true, false, false);
|
||||
public static Encoding UnicodeLE => _unicodeLE ??= new UnicodeEncoding(false, false, false);
|
||||
public static Encoding Latin1 => Encoding.Latin1;
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static byte[] GetBytesAscii(this string str) => GetBytes(str, Encoding.ASCII);
|
||||
|
|
@ -43,6 +80,9 @@ public static class TextEncoding
|
|||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static byte[] GetBytesAscii(this ReadOnlySpan<char> str) => GetBytes(str, Encoding.ASCII);
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static byte[] GetBytesLatin1(this ReadOnlySpan<char> str) => GetBytes(str, Latin1);
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static byte[] GetBytesBigUni(this ReadOnlySpan<char> str) => GetBytes(str, Unicode);
|
||||
|
||||
|
|
@ -86,6 +126,12 @@ public static class TextEncoding
|
|||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static int GetBytesAscii(this ReadOnlySpan<char> str, Span<byte> buffer) => Encoding.ASCII.GetBytes(str, buffer);
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static int GetBytesLatin1(this ReadOnlySpan<char> str, Span<byte> buffer) => Latin1.GetBytes(str, buffer);
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static int GetBytesLatin1(this string str, Span<byte> buffer) => Latin1.GetBytes(str, buffer);
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static int GetBytesBigUni(this string str, Span<byte> buffer) => Unicode.GetBytes(str, buffer);
|
||||
|
||||
|
|
@ -115,14 +161,111 @@ public static class TextEncoding
|
|||
_ => 1
|
||||
};
|
||||
|
||||
public static string GetString(ReadOnlySpan<byte> span, Encoding encoding, bool safeString = false)
|
||||
/// <summary>
|
||||
/// Decodes an ASCII byte span to a string, filtering C0 control codes and DEL in safe mode.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// For ASCII, bytes >= 0x80 are invalid and become '?' when decoded, which is acceptable for display.
|
||||
/// Only C0 (0x00-0x1F) and DEL (0x7F) need explicit filtering.
|
||||
/// </remarks>
|
||||
public static string GetStringAscii(ReadOnlySpan<byte> bytes, bool safeString = false)
|
||||
{
|
||||
if (!safeString)
|
||||
{
|
||||
return encoding.GetString(span);
|
||||
return Encoding.ASCII.GetString(bytes);
|
||||
}
|
||||
|
||||
var charCount = encoding.GetMaxCharCount(span.Length);
|
||||
// Check bytes directly - filter at byte level, then decode once
|
||||
var index = bytes.IndexOfAny(InvalidAsciiBytes);
|
||||
if (index == -1)
|
||||
{
|
||||
return Encoding.ASCII.GetString(bytes);
|
||||
}
|
||||
|
||||
// Has invalid bytes, filter and decode
|
||||
return FilterInvalidAsciiBytes(bytes, index);
|
||||
}
|
||||
|
||||
private static string FilterInvalidAsciiBytes(ReadOnlySpan<byte> bytes, int firstInvalidIndex)
|
||||
{
|
||||
var maxLength = bytes.Length;
|
||||
|
||||
char[] rentedChars = null;
|
||||
var charBuffer = maxLength <= 256
|
||||
? stackalloc char[maxLength]
|
||||
: rentedChars = STArrayPool<char>.Shared.Rent(maxLength);
|
||||
|
||||
try
|
||||
{
|
||||
using var sb = maxLength <= 256
|
||||
? new ValueStringBuilder(stackalloc char[maxLength])
|
||||
: ValueStringBuilder.Create(maxLength);
|
||||
|
||||
var index = firstInvalidIndex;
|
||||
|
||||
while (index != -1)
|
||||
{
|
||||
if (index > 0)
|
||||
{
|
||||
// Decode segment and append
|
||||
var segment = bytes[..index];
|
||||
var decoded = Encoding.ASCII.GetChars(segment, charBuffer);
|
||||
sb.Append(charBuffer[..decoded]);
|
||||
}
|
||||
|
||||
if (index + 1 < bytes.Length)
|
||||
{
|
||||
bytes = bytes[(index + 1)..];
|
||||
index = bytes.IndexOfAny(InvalidAsciiBytes);
|
||||
}
|
||||
else
|
||||
{
|
||||
bytes = [];
|
||||
index = -1;
|
||||
}
|
||||
}
|
||||
|
||||
if (bytes.Length > 0)
|
||||
{
|
||||
var decoded = Encoding.ASCII.GetChars(bytes, charBuffer);
|
||||
sb.Append(charBuffer[..decoded]);
|
||||
}
|
||||
|
||||
return sb.ToString();
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (rentedChars != null)
|
||||
{
|
||||
STArrayPool<char>.Shared.Return(rentedChars);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Decodes a Latin1 byte span to a string, filtering C0/C1 control codes in safe mode.
|
||||
/// </summary>
|
||||
public static string GetStringLatin1(ReadOnlySpan<byte> bytes, bool safeString = false)
|
||||
{
|
||||
if (!safeString)
|
||||
{
|
||||
return Latin1.GetString(bytes);
|
||||
}
|
||||
|
||||
// Check bytes directly - Latin1 is 1:1 mapping, avoids char buffer for valid strings
|
||||
var index = bytes.IndexOfAny(InvalidLatin1Bytes);
|
||||
if (index == -1)
|
||||
{
|
||||
return Latin1.GetString(bytes);
|
||||
}
|
||||
|
||||
// Has invalid bytes, need to filter - now allocate char buffer
|
||||
return FilterInvalidLatin1Chars(bytes, index);
|
||||
}
|
||||
|
||||
private static string FilterInvalidLatin1Chars(ReadOnlySpan<byte> bytes, int firstInvalidIndex)
|
||||
{
|
||||
var charCount = bytes.Length;
|
||||
|
||||
char[] rentedChars = null;
|
||||
var chars = charCount <= 256
|
||||
|
|
@ -131,19 +274,16 @@ public static class TextEncoding
|
|||
|
||||
try
|
||||
{
|
||||
var length = encoding.GetChars(span, chars);
|
||||
var length = Latin1.GetChars(bytes, chars);
|
||||
chars = chars[..length];
|
||||
|
||||
var index = chars.IndexOfAnyExceptInRange((char)0x20, (char)0xFFFD);
|
||||
if (index == -1)
|
||||
{
|
||||
return new string(chars);
|
||||
}
|
||||
|
||||
using var sb = charCount <= 256
|
||||
? new ValueStringBuilder(stackalloc char[charCount])
|
||||
: ValueStringBuilder.Create(charCount);
|
||||
|
||||
// Start from the first invalid index we already found
|
||||
var index = firstInvalidIndex;
|
||||
|
||||
while (index != -1)
|
||||
{
|
||||
sb.Append(chars[..index]);
|
||||
|
|
@ -151,7 +291,7 @@ public static class TextEncoding
|
|||
if (index + 1 < chars.Length)
|
||||
{
|
||||
chars = chars[(index + 1)..];
|
||||
index = chars.IndexOfAnyExceptInRange((char)0x20, (char)0xFFFD);
|
||||
index = chars.IndexOfAny(InvalidLatin1Chars);
|
||||
}
|
||||
else
|
||||
{
|
||||
|
|
@ -174,4 +314,281 @@ public static class TextEncoding
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Decodes a UTF-8 byte span to a string, filtering control codes and non-characters in safe mode.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Filters: C0 (0x00-0x1F), DEL (0x7F), C1 (0x80-0x9F), and non-characters (0xFFFD-0xFFFF).
|
||||
/// Optimized to check bytes for C0/DEL first (single-byte in UTF-8), then chars for C1/non-chars.
|
||||
/// </remarks>
|
||||
public static string GetStringUtf8(ReadOnlySpan<byte> bytes, bool safeString = false)
|
||||
{
|
||||
if (!safeString)
|
||||
{
|
||||
return UTF8.GetString(bytes);
|
||||
}
|
||||
|
||||
// Quick check: C0 (0x00-0x1F) and DEL (0x7F) are single-byte in UTF-8
|
||||
var hasC0OrDel = bytes.IndexOfAny(InvalidAsciiBytes) >= 0;
|
||||
|
||||
var charCount = UTF8.GetMaxCharCount(bytes.Length);
|
||||
|
||||
char[] rentedChars = null;
|
||||
var chars = charCount <= 256
|
||||
? stackalloc char[charCount]
|
||||
: rentedChars = STArrayPool<char>.Shared.Rent(charCount);
|
||||
|
||||
try
|
||||
{
|
||||
var length = UTF8.GetChars(bytes, chars);
|
||||
chars = chars[..length];
|
||||
|
||||
// If no C0/DEL in bytes, only need to check for C1 and non-chars
|
||||
var index = hasC0OrDel
|
||||
? IndexOfInvalidUnicodeChar(chars)
|
||||
: IndexOfInvalidUnicodeCharNonAscii(chars);
|
||||
|
||||
if (index == -1)
|
||||
{
|
||||
return new string(chars);
|
||||
}
|
||||
|
||||
return FilterInvalidUnicodeChars(chars, index, hasC0OrDel);
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (rentedChars != null)
|
||||
{
|
||||
STArrayPool<char>.Shared.Return(rentedChars);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Decodes a big-endian UTF-16 byte span to a string, filtering control codes and non-characters in safe mode.
|
||||
/// </summary>
|
||||
public static string GetStringBigUni(ReadOnlySpan<byte> bytes, bool safeString = false)
|
||||
{
|
||||
if (!safeString)
|
||||
{
|
||||
return Unicode.GetString(bytes);
|
||||
}
|
||||
|
||||
var charCount = Unicode.GetMaxCharCount(bytes.Length);
|
||||
|
||||
char[] rentedChars = null;
|
||||
var chars = charCount <= 256
|
||||
? stackalloc char[charCount]
|
||||
: rentedChars = STArrayPool<char>.Shared.Rent(charCount);
|
||||
|
||||
try
|
||||
{
|
||||
var length = Unicode.GetChars(bytes, chars);
|
||||
chars = chars[..length];
|
||||
|
||||
var index = IndexOfInvalidUnicodeChar(chars);
|
||||
if (index == -1)
|
||||
{
|
||||
return new string(chars);
|
||||
}
|
||||
|
||||
return FilterInvalidUnicodeChars(chars, index, fullCheck: true);
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (rentedChars != null)
|
||||
{
|
||||
STArrayPool<char>.Shared.Return(rentedChars);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Decodes a little-endian UTF-16 byte span to a string, filtering control codes and non-characters in safe mode.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Optimized for little-endian systems using direct memory cast (no decoding overhead).
|
||||
/// </remarks>
|
||||
public static string GetStringLittleUni(ReadOnlySpan<byte> bytes, bool safeString = false)
|
||||
{
|
||||
// Direct cast - UTF-16 LE bytes map directly to chars on little-endian systems
|
||||
var chars = MemoryMarshal.Cast<byte, char>(bytes);
|
||||
|
||||
if (!safeString)
|
||||
{
|
||||
return new string(chars);
|
||||
}
|
||||
|
||||
var index = IndexOfInvalidUnicodeChar(chars);
|
||||
if (index == -1)
|
||||
{
|
||||
return new string(chars);
|
||||
}
|
||||
|
||||
return FilterInvalidUnicodeChars(chars, index, fullCheck: true);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Generic string decoding with filtering. Prefer encoding-specific methods for better performance.
|
||||
/// </summary>
|
||||
public static string GetString(ReadOnlySpan<byte> span, Encoding encoding, bool safeString = false)
|
||||
{
|
||||
if (!safeString)
|
||||
{
|
||||
return encoding.GetString(span);
|
||||
}
|
||||
|
||||
var charCount = encoding.GetMaxCharCount(span.Length);
|
||||
|
||||
char[] rentedChars = null;
|
||||
var chars = charCount <= 256
|
||||
? stackalloc char[charCount]
|
||||
: rentedChars = STArrayPool<char>.Shared.Rent(charCount);
|
||||
|
||||
try
|
||||
{
|
||||
var length = encoding.GetChars(span, chars);
|
||||
chars = chars[..length];
|
||||
|
||||
var index = IndexOfInvalidUnicodeChar(chars);
|
||||
if (index == -1)
|
||||
{
|
||||
return new string(chars);
|
||||
}
|
||||
|
||||
using var sb = charCount <= 256
|
||||
? new ValueStringBuilder(stackalloc char[charCount])
|
||||
: ValueStringBuilder.Create(charCount);
|
||||
|
||||
while (index != -1)
|
||||
{
|
||||
sb.Append(chars[..index]);
|
||||
|
||||
if (index + 1 < chars.Length)
|
||||
{
|
||||
chars = chars[(index + 1)..];
|
||||
index = IndexOfInvalidUnicodeChar(chars);
|
||||
}
|
||||
else
|
||||
{
|
||||
index = -1;
|
||||
}
|
||||
}
|
||||
|
||||
if (chars.Length > 0)
|
||||
{
|
||||
sb.Append(chars);
|
||||
}
|
||||
|
||||
return sb.ToString();
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (rentedChars != null)
|
||||
{
|
||||
STArrayPool<char>.Shared.Return(rentedChars);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Finds the first invalid Unicode character for display.
|
||||
/// Invalid: C0 (0x00-0x1F), DEL (0x7F), C1 (0x80-0x9F), non-chars (0xFFFE-0xFFFF).
|
||||
/// Note: Surrogate pairs (0xD800-0xDFFF) are not filtered - proper validation requires context checking.
|
||||
/// </summary>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private static int IndexOfInvalidUnicodeChar(ReadOnlySpan<char> chars)
|
||||
{
|
||||
// Check invalid ranges and return the minimum index found
|
||||
var c0Index = chars.IndexOfAnyInRange((char)0x00, (char)0x1F); // C0 control codes
|
||||
var delC1Index = chars.IndexOfAnyInRange((char)0x7F, (char)0x9F); // DEL + C1 control codes
|
||||
var nonCharIndex = chars.IndexOfAnyInRange((char)0xFFFE, (char)0xFFFF); // Non-characters
|
||||
|
||||
// Find minimum non-negative index
|
||||
var minIndex = -1;
|
||||
|
||||
if (c0Index >= 0)
|
||||
{
|
||||
minIndex = c0Index;
|
||||
}
|
||||
|
||||
if (delC1Index >= 0 && (minIndex < 0 || delC1Index < minIndex))
|
||||
{
|
||||
minIndex = delC1Index;
|
||||
}
|
||||
|
||||
if (nonCharIndex >= 0 && (minIndex < 0 || nonCharIndex < minIndex))
|
||||
{
|
||||
minIndex = nonCharIndex;
|
||||
}
|
||||
|
||||
return minIndex;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Finds the first invalid Unicode character, excluding C0/DEL (already checked at byte level).
|
||||
/// Checks: C1 (0x80-0x9F), non-chars (0xFFFE-0xFFFF).
|
||||
/// Note: Surrogate pairs (0xD800-0xDFFF) are not filtered - proper validation requires context checking.
|
||||
/// </summary>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private static int IndexOfInvalidUnicodeCharNonAscii(ReadOnlySpan<char> chars)
|
||||
{
|
||||
// Only check C1 and non-chars (C0/DEL already verified absent at byte level)
|
||||
var c1Index = chars.IndexOfAnyInRange((char)0x80, (char)0x9F); // C1 control codes
|
||||
var nonCharIndex = chars.IndexOfAnyInRange((char)0xFFFE, (char)0xFFFF); // Non-characters
|
||||
|
||||
if (c1Index < 0)
|
||||
{
|
||||
return nonCharIndex;
|
||||
}
|
||||
|
||||
if (nonCharIndex < 0)
|
||||
{
|
||||
return c1Index;
|
||||
}
|
||||
|
||||
return Math.Min(c1Index, nonCharIndex);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Filters invalid Unicode characters from a char span by removing them.
|
||||
/// The UO client renders nothing for invalid characters, so removal is most efficient.
|
||||
/// </summary>
|
||||
private static string FilterInvalidUnicodeChars(ReadOnlySpan<char> chars, int firstInvalidIndex, bool fullCheck)
|
||||
{
|
||||
var maxLength = chars.Length;
|
||||
|
||||
using var sb = maxLength <= 256
|
||||
? new ValueStringBuilder(stackalloc char[maxLength])
|
||||
: ValueStringBuilder.Create(maxLength);
|
||||
|
||||
var index = firstInvalidIndex;
|
||||
|
||||
while (index != -1)
|
||||
{
|
||||
sb.Append(chars[..index]);
|
||||
// Skip the invalid character (don't append replacement - client renders nothing anyway)
|
||||
|
||||
if (index + 1 < chars.Length)
|
||||
{
|
||||
chars = chars[(index + 1)..];
|
||||
index = fullCheck
|
||||
? IndexOfInvalidUnicodeChar(chars)
|
||||
: IndexOfInvalidUnicodeCharNonAscii(chars);
|
||||
}
|
||||
else
|
||||
{
|
||||
chars = [];
|
||||
index = -1;
|
||||
}
|
||||
}
|
||||
|
||||
if (chars.Length > 0)
|
||||
{
|
||||
sb.Append(chars);
|
||||
}
|
||||
|
||||
return sb.ToString();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -23,7 +23,6 @@ public class AccountHandlerTests
|
|||
|
||||
[Theory]
|
||||
[InlineData("", false)] // Empty password
|
||||
[InlineData("Invalid\x01Char", false)] // Contains invalid ASCII character
|
||||
[InlineData("ValidPass123!", true)] // Standard Password
|
||||
[InlineData(" ", true)] // Single space
|
||||
[InlineData("ValidPass!@#", true)] // Valid special characters
|
||||
|
|
|
|||
|
|
@ -247,15 +247,10 @@ public static class AccountHandler
|
|||
username.Length > 0 &&
|
||||
// Usernames must not start with a space, end with a space, or end with a period
|
||||
!username.StartsWith(' ') && !username.EndsWith(' ') && !username.EndsWith('.') &&
|
||||
// Usernames must only contain characters [0x20 -> 0x7E], and not contain any forbidden characters
|
||||
!username.ContainsAnyExceptInRange((char)0x20, (char)0x7E) &&
|
||||
!username.ContainsAny(ForbiddenChars);
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static bool IsValidPassword(ReadOnlySpan<char> password) =>
|
||||
password.Length > 0 &&
|
||||
// Passwords must have characters [0x20 -> 0x7E]
|
||||
!password.ContainsAnyExceptInRange((char)0x20, (char)0x7E);
|
||||
public static bool IsValidPassword(ReadOnlySpan<char> password) => password.Length > 0;
|
||||
|
||||
private static Account CreateAccount(NetState state, string username, string password)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -577,7 +577,7 @@ public ref struct GumpLayoutBuilder
|
|||
{
|
||||
if (!value.IsEmpty)
|
||||
{
|
||||
_bytesWritten += value.GetBytesAscii(_layoutBuffer.AsSpan(_bytesWritten));
|
||||
_bytesWritten += value.GetBytesLatin1(_layoutBuffer.AsSpan(_bytesWritten));
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -122,7 +122,7 @@ public class GumpHtmlLocalized : GumpEntry
|
|||
}
|
||||
case GumpHtmlLocalizedType.Args:
|
||||
{
|
||||
writer.WriteAscii(
|
||||
writer.WriteLatin1(
|
||||
$"{{ xmfhtmltok {X} {Y} {Width} {Height} {(Background ? "1" : "0")} {(Scrollbar ? "1" : "0")} {Color} {Number} @{Args}@ }}"
|
||||
);
|
||||
break;
|
||||
|
|
|
|||
|
|
@ -34,6 +34,13 @@ public class GumpTooltip : GumpEntry
|
|||
|
||||
public override void AppendTo(ref SpanWriter writer, OrderedSet<string> strings, ref int entries, ref int switches)
|
||||
{
|
||||
writer.WriteAscii(string.IsNullOrEmpty(Args) ? $"{{ tooltip {Number} }}" : $"{{ tooltip {Number} @{Args}@ }}");
|
||||
if (string.IsNullOrEmpty(Args))
|
||||
{
|
||||
writer.WriteAscii($"{{ tooltip {Number} }}");
|
||||
}
|
||||
else
|
||||
{
|
||||
writer.WriteLatin1($"{{ tooltip {Number} @{Args}@ }}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -73,9 +73,9 @@ public static class OutgoingGumpPackets
|
|||
writer.Write(serial);
|
||||
writer.Write((short)gumpId);
|
||||
writer.Write((short)(unknown.Length + 1));
|
||||
writer.WriteAsciiNull(unknown);
|
||||
writer.WriteLatin1Null(unknown);
|
||||
writer.Write((short)(caption.Length + 1));
|
||||
writer.WriteAsciiNull(caption);
|
||||
writer.WriteLatin1Null(caption);
|
||||
|
||||
ns.Send(writer.Span);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
/*************************************************************************
|
||||
* ModernUO *
|
||||
* Copyright 2019-2024 - ModernUO Development Team *
|
||||
* Copyright 2019-2026 - ModernUO Development Team *
|
||||
* Email: hi@modernuo.com *
|
||||
* File: RelayInfo.cs *
|
||||
* *
|
||||
|
|
@ -48,8 +48,6 @@ public readonly ref struct RelayInfo
|
|||
public string GetTextEntry(int entryId)
|
||||
{
|
||||
var index = _textIds.IndexOf((ushort)entryId);
|
||||
return index == -1
|
||||
? default
|
||||
: TextEncoding.GetString(_textBlock[_textRanges[index]], TextEncoding.Unicode, true);
|
||||
return index == -1 ? null : TextEncoding.GetStringBigUni(_textBlock[_textRanges[index]], true);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -41,8 +41,8 @@ namespace Server.Items
|
|||
|
||||
reader.Seek(4, SeekOrigin.Current); // Skip flags and page count
|
||||
|
||||
var title = reader.ReadAsciiSafe(60);
|
||||
var author = reader.ReadAsciiSafe(30);
|
||||
var title = reader.ReadLatin1Safe(60);
|
||||
var author = reader.ReadLatin1Safe(30);
|
||||
|
||||
book.Title = title.FixHtml();
|
||||
book.Author = author.FixHtml();
|
||||
|
|
|
|||
|
|
@ -346,7 +346,7 @@ namespace Server.Engines.Mahjong
|
|||
writer.Write((byte)0);
|
||||
|
||||
writer.Write(players.IsPublic(i));
|
||||
writer.WriteAscii(m?.Name ?? "", 30);
|
||||
writer.WriteLatin1(m?.Name ?? "", 30);
|
||||
|
||||
writer.Write(m == null || !players.IsInGamePlayer(i));
|
||||
count++;
|
||||
|
|
|
|||
|
|
@ -63,7 +63,7 @@ public static class IncomingAccountPackets
|
|||
var unk2 = reader.ReadInt32();
|
||||
int unk3 = reader.ReadByte();
|
||||
*/
|
||||
var name = reader.ReadAscii(30);
|
||||
var name = reader.ReadLatin1Safe(30);
|
||||
|
||||
reader.Seek(2, SeekOrigin.Current);
|
||||
var flags = reader.ReadInt32();
|
||||
|
|
@ -371,8 +371,8 @@ public static class IncomingAccountPackets
|
|||
state.Version = ap.Version;
|
||||
state.Seeded = true;
|
||||
|
||||
var username = reader.ReadAscii(30);
|
||||
var password = reader.ReadAscii(30);
|
||||
var username = reader.ReadLatin1Safe(30);
|
||||
var password = reader.ReadLatin1Safe(30);
|
||||
|
||||
var e = new GameServer.GameLoginEventArgs(state, username, password);
|
||||
|
||||
|
|
@ -446,8 +446,8 @@ public static class IncomingAccountPackets
|
|||
|
||||
state.SentFirstPacket = true;
|
||||
|
||||
var username = reader.ReadAscii(30);
|
||||
var password = reader.ReadAscii(30);
|
||||
var username = reader.ReadLatin1Safe(30);
|
||||
var password = reader.ReadLatin1Safe(30);
|
||||
|
||||
var accountLoginEventArgs = new AccountLoginEventArgs(state, username, password);
|
||||
|
||||
|
|
|
|||
|
|
@ -40,7 +40,7 @@ public static class IncomingMessagePackets
|
|||
var type = (MessageType)reader.ReadByte();
|
||||
int hue = reader.ReadInt16();
|
||||
reader.ReadInt16(); // font
|
||||
var text = reader.ReadAsciiSafe().Trim();
|
||||
var text = reader.ReadLatin1Safe().Trim();
|
||||
|
||||
if (text.Length is <= 0 or > 128)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -36,7 +36,7 @@ public static class IncomingMobilePackets
|
|||
|
||||
if (targ != null)
|
||||
{
|
||||
RenameRequests.RenameRequest(from, targ, reader.ReadAsciiSafe());
|
||||
RenameRequests.RenameRequest(from, targ, reader.ReadLatin1Safe());
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -223,7 +223,7 @@ public static class IncomingPlayerPackets
|
|||
var serial = reader.ReadUInt32();
|
||||
var prompt = reader.ReadInt32();
|
||||
var type = reader.ReadInt32();
|
||||
var text = reader.ReadAsciiSafe();
|
||||
var text = reader.ReadLatin1Safe();
|
||||
|
||||
if (text.Length > 128)
|
||||
{
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue