diff --git a/src/FirebirdSql.Data.FirebirdClient/Client/Managed/Version10/GdsStatement.cs b/src/FirebirdSql.Data.FirebirdClient/Client/Managed/Version10/GdsStatement.cs index 54b5698cc..d8171219b 100644 --- a/src/FirebirdSql.Data.FirebirdClient/Client/Managed/Version10/GdsStatement.cs +++ b/src/FirebirdSql.Data.FirebirdClient/Client/Managed/Version10/GdsStatement.cs @@ -1533,16 +1533,7 @@ protected object ReadRawValue(IXdrReader xdr, DbField field) else { var s = xdr.ReadString(innerCharset, field.Length); - var runes = s.EnumerateRunesToChars().ToList(); - if ((field.Length % field.Charset.BytesPerCharacter) == 0 && - runes.Count > field.CharCount) - { - return new string([.. runes.Take(field.CharCount).SelectMany(x => x)]); - } - else - { - return s; - } + return TruncateStringByRuneCount(s, field); } case DbDataType.VarChar: @@ -1631,16 +1622,7 @@ protected async ValueTask ReadRawValueAsync(IXdrReader xdr, DbField fiel else { var s = await xdr.ReadStringAsync(innerCharset, field.Length, cancellationToken).ConfigureAwait(false); - var runes = s.EnumerateRunesToChars().ToList(); - if ((field.Length % field.Charset.BytesPerCharacter) == 0 && - runes.Count > field.CharCount) - { - return new string([.. runes.Take(field.CharCount).SelectMany(x => x)]); - } - else - { - return s; - } + return TruncateStringByRuneCount(s, field); } case DbDataType.VarChar: @@ -1797,6 +1779,22 @@ protected virtual async ValueTask ReadRowAsync(CancellationToken canc return row; } + private static string TruncateStringByRuneCount(string s, DbField field) + { + if ((field.Length % field.Charset.BytesPerCharacter) != 0) + { + return s; + } + + var runeCount = s.CountRunes(); + if (runeCount <= field.CharCount) + { + return s; + } + + return new string(s.TruncateStringToRuneCount(field.CharCount)); + } + #endregion #region Protected Internal Methods diff --git a/src/FirebirdSql.Data.FirebirdClient/Common/DbField.cs b/src/FirebirdSql.Data.FirebirdClient/Common/DbField.cs index 3be89be74..efc23524a 100644 --- a/src/FirebirdSql.Data.FirebirdClient/Common/DbField.cs +++ b/src/FirebirdSql.Data.FirebirdClient/Common/DbField.cs @@ -325,12 +325,12 @@ public void SetValue(byte[] buffer) else { var s = Charset.GetString(buffer, 0, buffer.Length); - - var runes = s.EnumerateRunesToChars().ToList(); - if ((Length % Charset.BytesPerCharacter) == 0 && - runes.Count > CharCount) - { - s = new string([.. runes.Take(CharCount).SelectMany(x => x)]); + if((Length % Charset.BytesPerCharacter) == 0) + { + var runes = s.CountRunes(); + if(runes > CharCount) { + s = new string(s.TruncateStringToRuneCount(CharCount)); + } } DbValue.SetValue(s); diff --git a/src/FirebirdSql.Data.FirebirdClient/Common/DbValue.cs b/src/FirebirdSql.Data.FirebirdClient/Common/DbValue.cs index 44b79a0f1..f4fe8abee 100644 --- a/src/FirebirdSql.Data.FirebirdClient/Common/DbValue.cs +++ b/src/FirebirdSql.Data.FirebirdClient/Common/DbValue.cs @@ -424,7 +424,7 @@ public byte[] GetBytes() else { var svalue = GetString(); - if ((Field.Length % Field.Charset.BytesPerCharacter) == 0 && svalue.EnumerateRunesToChars().Count() > Field.CharCount) + if ((Field.Length % Field.Charset.BytesPerCharacter) == 0 && svalue.CountRunes() > Field.CharCount) { throw IscException.ForErrorCodes(new[] { IscCodes.isc_arith_except, IscCodes.isc_string_truncation }); } @@ -460,7 +460,7 @@ public byte[] GetBytes() else { var svalue = GetString(); - if ((Field.Length % Field.Charset.BytesPerCharacter) == 0 && svalue.EnumerateRunesToChars().Count() > Field.CharCount) + if ((Field.Length % Field.Charset.BytesPerCharacter) == 0 && svalue.CountRunes() > Field.CharCount) { throw IscException.ForErrorCodes(new[] { IscCodes.isc_arith_except, IscCodes.isc_string_truncation }); } @@ -639,7 +639,7 @@ public async ValueTask GetBytesAsync(CancellationToken cancellationToken else { var svalue = await GetStringAsync(cancellationToken).ConfigureAwait(false); - if ((Field.Length % Field.Charset.BytesPerCharacter) == 0 && svalue.EnumerateRunesToChars().Count() > Field.CharCount) + if ((Field.Length % Field.Charset.BytesPerCharacter) == 0 && svalue.CountRunes() > Field.CharCount) { throw IscException.ForErrorCodes(new[] { IscCodes.isc_arith_except, IscCodes.isc_string_truncation }); } @@ -675,7 +675,7 @@ public async ValueTask GetBytesAsync(CancellationToken cancellationToken else { var svalue = await GetStringAsync(cancellationToken).ConfigureAwait(false); - if ((Field.Length % Field.Charset.BytesPerCharacter) == 0 && svalue.EnumerateRunesToChars().Count() > Field.CharCount) + if ((Field.Length % Field.Charset.BytesPerCharacter) == 0 && svalue.CountRunes() > Field.CharCount) { throw IscException.ForErrorCodes(new[] { IscCodes.isc_arith_except, IscCodes.isc_string_truncation }); } diff --git a/src/FirebirdSql.Data.FirebirdClient/Common/Extensions.cs b/src/FirebirdSql.Data.FirebirdClient/Common/Extensions.cs index 6143cc839..abd71a004 100644 --- a/src/FirebirdSql.Data.FirebirdClient/Common/Extensions.cs +++ b/src/FirebirdSql.Data.FirebirdClient/Common/Extensions.cs @@ -102,4 +102,60 @@ public static Encoding GetANSIEncoding() } } } + + public static int CountRunes(this ReadOnlySpan text) + { + var length = text.Length; + if(length == 0) + return 0; + + var i = text.IndexOfAnyInRange('\uD800', '\uDBFF'); + if(i < 0) + return length; + + var count = i; + while(i < length) + { + if(char.IsHighSurrogate(text[i]) && i + 1 < length && char.IsLowSurrogate(text[i + 1])) + { + i += 2; + } + else + { + i++; + } + count++; + } + return count; + } + + public static ReadOnlySpan TruncateStringToRuneCount(this ReadOnlySpan text, int maxRuneCount) + { + if(maxRuneCount <= 0 || text.IsEmpty) + return ReadOnlySpan.Empty; + + var length = text.Length; + if(maxRuneCount >= length) + return text; + + var prefix = text[..maxRuneCount]; + var i = prefix.IndexOfAnyInRange('\uD800', '\uDBFF'); + if(i < 0) + return prefix; + + var remaining = maxRuneCount - i; + while(i < length && remaining > 0) + { + if(char.IsHighSurrogate(text[i]) && i + 1 < length && char.IsLowSurrogate(text[i + 1])) + { + i += 2; + } + else + { + i++; + } + remaining--; + } + return text[..i]; + } }