Skip to content

Commit bd0db88

Browse files
committed
Fix reading/writing characters with high/low surrogate pairs. Part 2. (#1213)
1 parent adcbe24 commit bd0db88

File tree

5 files changed

+42
-29
lines changed

5 files changed

+42
-29
lines changed

src/FirebirdSql.Data.FirebirdClient.Tests/FbCommandTests.cs

+4-4
Original file line numberDiff line numberDiff line change
@@ -699,8 +699,8 @@ public async Task HighLowSurrogatePassingTest()
699699
{
700700
await using (var cmd = Connection.CreateCommand())
701701
{
702-
const string Value = "😊";
703-
cmd.CommandText = "select cast(@value1 as varchar(1) character set utf8), cast(@value2 as char(1) character set utf8) from rdb$database";
702+
const string Value = "😊!";
703+
cmd.CommandText = "select cast(@value1 as varchar(2) character set utf8), cast(@value2 as char(2) character set utf8) from rdb$database";
704704
cmd.Parameters.Add("value1", Value);
705705
cmd.Parameters.Add("value2", Value);
706706
await using (var reader = await cmd.ExecuteReaderAsync())
@@ -717,8 +717,8 @@ public async Task HighLowSurrogateReadingTest()
717717
{
718718
await using (var cmd = Connection.CreateCommand())
719719
{
720-
const string Value = "😊";
721-
cmd.CommandText = "select cast(x'F09F988A' as varchar(1) character set utf8), cast(x'F09F988A' as char(1) character set utf8) from rdb$database";
720+
const string Value = "😊!";
721+
cmd.CommandText = "select cast(x'F09F988A21' as varchar(2) character set utf8), cast(x'F09F988A21' as char(2) character set utf8) from rdb$database";
722722
await using (var reader = await cmd.ExecuteReaderAsync())
723723
{
724724
await reader.ReadAsync();

src/FirebirdSql.Data.FirebirdClient/Client/Managed/Version10/GdsStatement.cs

+11-8
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
using System.Collections.Generic;
2020
using System.Diagnostics;
2121
using System.IO;
22+
using System.Linq;
2223
using System.Threading;
2324
using System.Threading.Tasks;
2425
using FirebirdSql.Data.Common;
@@ -1246,7 +1247,7 @@ protected void WriteRawParameter(IXdrWriter xdr, DbField field)
12461247
else
12471248
{
12481249
var svalue = field.DbValue.GetString();
1249-
if ((field.Length % field.Charset.BytesPerCharacter) == 0 && svalue.RuneCount() > field.CharCount)
1250+
if ((field.Length % field.Charset.BytesPerCharacter) == 0 && svalue.EnumerateRunesEx().Count() > field.CharCount)
12501251
{
12511252
throw IscException.ForErrorCodes(new[] { IscCodes.isc_arith_except, IscCodes.isc_string_truncation });
12521253
}
@@ -1271,7 +1272,7 @@ protected void WriteRawParameter(IXdrWriter xdr, DbField field)
12711272
else
12721273
{
12731274
var svalue = field.DbValue.GetString();
1274-
if ((field.Length % field.Charset.BytesPerCharacter) == 0 && svalue.RuneCount() > field.CharCount)
1275+
if ((field.Length % field.Charset.BytesPerCharacter) == 0 && svalue.EnumerateRunesEx().Count() > field.CharCount)
12751276
{
12761277
throw IscException.ForErrorCodes(new[] { IscCodes.isc_arith_except, IscCodes.isc_string_truncation });
12771278
}
@@ -1394,7 +1395,7 @@ protected async ValueTask WriteRawParameterAsync(IXdrWriter xdr, DbField field,
13941395
else
13951396
{
13961397
var svalue = await field.DbValue.GetStringAsync(cancellationToken).ConfigureAwait(false);
1397-
if ((field.Length % field.Charset.BytesPerCharacter) == 0 && svalue.RuneCount() > field.CharCount)
1398+
if ((field.Length % field.Charset.BytesPerCharacter) == 0 && svalue.EnumerateRunesEx().Count() > field.CharCount)
13981399
{
13991400
throw IscException.ForErrorCodes(new[] { IscCodes.isc_arith_except, IscCodes.isc_string_truncation });
14001401
}
@@ -1419,7 +1420,7 @@ protected async ValueTask WriteRawParameterAsync(IXdrWriter xdr, DbField field,
14191420
else
14201421
{
14211422
var svalue = await field.DbValue.GetStringAsync(cancellationToken).ConfigureAwait(false);
1422-
if ((field.Length % field.Charset.BytesPerCharacter) == 0 && svalue.RuneCount() > field.CharCount)
1423+
if ((field.Length % field.Charset.BytesPerCharacter) == 0 && svalue.EnumerateRunesEx().Count() > field.CharCount)
14231424
{
14241425
throw IscException.ForErrorCodes(new[] { IscCodes.isc_arith_except, IscCodes.isc_string_truncation });
14251426
}
@@ -1532,10 +1533,11 @@ protected object ReadRawValue(IXdrReader xdr, DbField field)
15321533
else
15331534
{
15341535
var s = xdr.ReadString(innerCharset, field.Length);
1536+
var runes = s.EnumerateRunesEx().ToList();
15351537
if ((field.Length % field.Charset.BytesPerCharacter) == 0 &&
1536-
s.RuneCount() > field.CharCount)
1538+
runes.Count > field.CharCount)
15371539
{
1538-
return s.Substring(0, field.CharCount);
1540+
return new string([.. runes.Take(field.CharCount).SelectMany(x => x)]);
15391541
}
15401542
else
15411543
{
@@ -1629,10 +1631,11 @@ protected async ValueTask<object> ReadRawValueAsync(IXdrReader xdr, DbField fiel
16291631
else
16301632
{
16311633
var s = await xdr.ReadStringAsync(innerCharset, field.Length, cancellationToken).ConfigureAwait(false);
1634+
var runes = s.EnumerateRunesEx().ToList();
16321635
if ((field.Length % field.Charset.BytesPerCharacter) == 0 &&
1633-
s.RuneCount() > field.CharCount)
1636+
runes.Count > field.CharCount)
16341637
{
1635-
return s.Substring(0, field.CharCount);
1638+
return new string([.. runes.Take(field.CharCount).SelectMany(x => x)]);
16361639
}
16371640
else
16381641
{

src/FirebirdSql.Data.FirebirdClient/Common/DbField.cs

+4-2
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
//$Authors = Carlos Guzman Alvarez, Jiri Cincura (jiri@cincura.net)
1717

1818
using System;
19+
using System.Linq;
1920
using System.Numerics;
2021
using FirebirdSql.Data.Types;
2122

@@ -325,10 +326,11 @@ public void SetValue(byte[] buffer)
325326
{
326327
var s = Charset.GetString(buffer, 0, buffer.Length);
327328

329+
var runes = s.EnumerateRunesEx().ToList();
328330
if ((Length % Charset.BytesPerCharacter) == 0 &&
329-
s.RuneCount() > CharCount)
331+
runes.Count > CharCount)
330332
{
331-
s = s.Substring(0, CharCount);
333+
s = new string([.. runes.Take(CharCount).SelectMany(x => x)]);
332334
}
333335

334336
DbValue.SetValue(s);

src/FirebirdSql.Data.FirebirdClient/Common/DbValue.cs

+5-4
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717

1818
using System;
1919
using System.Globalization;
20+
using System.Linq;
2021
using System.Numerics;
2122
using System.Threading;
2223
using System.Threading.Tasks;
@@ -427,7 +428,7 @@ public byte[] GetBytes()
427428
else
428429
{
429430
var svalue = GetString();
430-
if ((Field.Length % Field.Charset.BytesPerCharacter) == 0 && svalue.RuneCount() > Field.CharCount)
431+
if ((Field.Length % Field.Charset.BytesPerCharacter) == 0 && svalue.EnumerateRunesEx().Count() > Field.CharCount)
431432
{
432433
throw IscException.ForErrorCodes(new[] { IscCodes.isc_arith_except, IscCodes.isc_string_truncation });
433434
}
@@ -463,7 +464,7 @@ public byte[] GetBytes()
463464
else
464465
{
465466
var svalue = GetString();
466-
if ((Field.Length % Field.Charset.BytesPerCharacter) == 0 && svalue.RuneCount() > Field.CharCount)
467+
if ((Field.Length % Field.Charset.BytesPerCharacter) == 0 && svalue.EnumerateRunesEx().Count() > Field.CharCount)
467468
{
468469
throw IscException.ForErrorCodes(new[] { IscCodes.isc_arith_except, IscCodes.isc_string_truncation });
469470
}
@@ -642,7 +643,7 @@ public async ValueTask<byte[]> GetBytesAsync(CancellationToken cancellationToken
642643
else
643644
{
644645
var svalue = await GetStringAsync(cancellationToken).ConfigureAwait(false);
645-
if ((Field.Length % Field.Charset.BytesPerCharacter) == 0 && svalue.RuneCount() > Field.CharCount)
646+
if ((Field.Length % Field.Charset.BytesPerCharacter) == 0 && svalue.EnumerateRunesEx().Count() > Field.CharCount)
646647
{
647648
throw IscException.ForErrorCodes(new[] { IscCodes.isc_arith_except, IscCodes.isc_string_truncation });
648649
}
@@ -678,7 +679,7 @@ public async ValueTask<byte[]> GetBytesAsync(CancellationToken cancellationToken
678679
else
679680
{
680681
var svalue = await GetStringAsync(cancellationToken).ConfigureAwait(false);
681-
if ((Field.Length % Field.Charset.BytesPerCharacter) == 0 && svalue.RuneCount() > Field.CharCount)
682+
if ((Field.Length % Field.Charset.BytesPerCharacter) == 0 && svalue.EnumerateRunesEx().Count() > Field.CharCount)
682683
{
683684
throw IscException.ForErrorCodes(new[] { IscCodes.isc_arith_except, IscCodes.isc_string_truncation });
684685
}

src/FirebirdSql.Data.FirebirdClient/Common/Extensions.cs

+18-11
Original file line numberDiff line numberDiff line change
@@ -67,25 +67,32 @@ public static IEnumerable<IEnumerable<T>> Split<T>(this T[] array, int size)
6767
public static HashSet<T> ToHashSet<T>(this IEnumerable<T> source) => new HashSet<T>(source);
6868
#endif
6969

70-
public static int RuneCount(this string s)
70+
public static IEnumerable<char[]> EnumerateRunesEx(this string s)
7171
{
7272
if (s == null)
7373
throw new ArgumentNullException(nameof(s));
7474

7575
#if NETSTANDARD2_0 || NETSTANDARD2_1 || NET48
76-
var cnt = 0;
7776
for (var i = 0; i < s.Length; i++)
78-
{
79-
if (char.IsHighSurrogate(s[i]) && i + 1 < s.Length && char.IsLowSurrogate(s[i + 1]))
80-
{
81-
i++;
82-
}
83-
cnt++;
84-
}
85-
return cnt;
77+
{
78+
if (char.IsHighSurrogate(s[i]) && i + 1 < s.Length && char.IsLowSurrogate(s[i + 1]))
79+
{
80+
yield return new[] { s[i], s[i + 1] };
81+
i++;
82+
}
83+
else
84+
{
85+
yield return new[] { s[i] };
86+
}
87+
}
8688

8789
#else
88-
return s.EnumerateRunes().Count();
90+
return s.EnumerateRunes().Select(r =>
91+
{
92+
var result = new char[r.Utf16SequenceLength];
93+
r.EncodeToUtf16(result);
94+
return result;
95+
});
8996
#endif
9097
}
9198
}

0 commit comments

Comments
 (0)