Skip to content

Commit adcbe24

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

File tree

5 files changed

+67
-11
lines changed

5 files changed

+67
-11
lines changed

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

+34
Original file line numberDiff line numberDiff line change
@@ -694,6 +694,40 @@ public async Task PassesDateTimeWithProperPrecisionTest()
694694
}
695695
}
696696

697+
[Test]
698+
public async Task HighLowSurrogatePassingTest()
699+
{
700+
await using (var cmd = Connection.CreateCommand())
701+
{
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";
704+
cmd.Parameters.Add("value1", Value);
705+
cmd.Parameters.Add("value2", Value);
706+
await using (var reader = await cmd.ExecuteReaderAsync())
707+
{
708+
await reader.ReadAsync();
709+
Assert.AreEqual(Value, reader[0]);
710+
Assert.AreEqual(Value, reader[1]);
711+
}
712+
}
713+
}
714+
715+
[Test]
716+
public async Task HighLowSurrogateReadingTest()
717+
{
718+
await using (var cmd = Connection.CreateCommand())
719+
{
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";
722+
await using (var reader = await cmd.ExecuteReaderAsync())
723+
{
724+
await reader.ReadAsync();
725+
Assert.AreEqual(Value, reader[0]);
726+
Assert.AreEqual(Value, reader[1]);
727+
}
728+
}
729+
}
730+
697731
[Test]
698732
public async Task ExecuteNonQueryReturnsMinusOneOnNonInsertUpdateDeleteTest()
699733
{

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

+6-6
Original file line numberDiff line numberDiff line change
@@ -1246,7 +1246,7 @@ protected void WriteRawParameter(IXdrWriter xdr, DbField field)
12461246
else
12471247
{
12481248
var svalue = field.DbValue.GetString();
1249-
if ((field.Length % field.Charset.BytesPerCharacter) == 0 && svalue.Length > field.CharCount)
1249+
if ((field.Length % field.Charset.BytesPerCharacter) == 0 && svalue.RuneCount() > field.CharCount)
12501250
{
12511251
throw IscException.ForErrorCodes(new[] { IscCodes.isc_arith_except, IscCodes.isc_string_truncation });
12521252
}
@@ -1271,7 +1271,7 @@ protected void WriteRawParameter(IXdrWriter xdr, DbField field)
12711271
else
12721272
{
12731273
var svalue = field.DbValue.GetString();
1274-
if ((field.Length % field.Charset.BytesPerCharacter) == 0 && svalue.Length > field.CharCount)
1274+
if ((field.Length % field.Charset.BytesPerCharacter) == 0 && svalue.RuneCount() > field.CharCount)
12751275
{
12761276
throw IscException.ForErrorCodes(new[] { IscCodes.isc_arith_except, IscCodes.isc_string_truncation });
12771277
}
@@ -1394,7 +1394,7 @@ protected async ValueTask WriteRawParameterAsync(IXdrWriter xdr, DbField field,
13941394
else
13951395
{
13961396
var svalue = await field.DbValue.GetStringAsync(cancellationToken).ConfigureAwait(false);
1397-
if ((field.Length % field.Charset.BytesPerCharacter) == 0 && svalue.Length > field.CharCount)
1397+
if ((field.Length % field.Charset.BytesPerCharacter) == 0 && svalue.RuneCount() > field.CharCount)
13981398
{
13991399
throw IscException.ForErrorCodes(new[] { IscCodes.isc_arith_except, IscCodes.isc_string_truncation });
14001400
}
@@ -1419,7 +1419,7 @@ protected async ValueTask WriteRawParameterAsync(IXdrWriter xdr, DbField field,
14191419
else
14201420
{
14211421
var svalue = await field.DbValue.GetStringAsync(cancellationToken).ConfigureAwait(false);
1422-
if ((field.Length % field.Charset.BytesPerCharacter) == 0 && svalue.Length > field.CharCount)
1422+
if ((field.Length % field.Charset.BytesPerCharacter) == 0 && svalue.RuneCount() > field.CharCount)
14231423
{
14241424
throw IscException.ForErrorCodes(new[] { IscCodes.isc_arith_except, IscCodes.isc_string_truncation });
14251425
}
@@ -1533,7 +1533,7 @@ protected object ReadRawValue(IXdrReader xdr, DbField field)
15331533
{
15341534
var s = xdr.ReadString(innerCharset, field.Length);
15351535
if ((field.Length % field.Charset.BytesPerCharacter) == 0 &&
1536-
s.Length > field.CharCount)
1536+
s.RuneCount() > field.CharCount)
15371537
{
15381538
return s.Substring(0, field.CharCount);
15391539
}
@@ -1630,7 +1630,7 @@ protected async ValueTask<object> ReadRawValueAsync(IXdrReader xdr, DbField fiel
16301630
{
16311631
var s = await xdr.ReadStringAsync(innerCharset, field.Length, cancellationToken).ConfigureAwait(false);
16321632
if ((field.Length % field.Charset.BytesPerCharacter) == 0 &&
1633-
s.Length > field.CharCount)
1633+
s.RuneCount() > field.CharCount)
16341634
{
16351635
return s.Substring(0, field.CharCount);
16361636
}

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

+1-1
Original file line numberDiff line numberDiff line change
@@ -326,7 +326,7 @@ public void SetValue(byte[] buffer)
326326
var s = Charset.GetString(buffer, 0, buffer.Length);
327327

328328
if ((Length % Charset.BytesPerCharacter) == 0 &&
329-
s.Length > CharCount)
329+
s.RuneCount() > CharCount)
330330
{
331331
s = s.Substring(0, CharCount);
332332
}

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

+4-4
Original file line numberDiff line numberDiff line change
@@ -427,7 +427,7 @@ public byte[] GetBytes()
427427
else
428428
{
429429
var svalue = GetString();
430-
if ((Field.Length % Field.Charset.BytesPerCharacter) == 0 && svalue.Length > Field.CharCount)
430+
if ((Field.Length % Field.Charset.BytesPerCharacter) == 0 && svalue.RuneCount() > Field.CharCount)
431431
{
432432
throw IscException.ForErrorCodes(new[] { IscCodes.isc_arith_except, IscCodes.isc_string_truncation });
433433
}
@@ -463,7 +463,7 @@ public byte[] GetBytes()
463463
else
464464
{
465465
var svalue = GetString();
466-
if ((Field.Length % Field.Charset.BytesPerCharacter) == 0 && svalue.Length > Field.CharCount)
466+
if ((Field.Length % Field.Charset.BytesPerCharacter) == 0 && svalue.RuneCount() > Field.CharCount)
467467
{
468468
throw IscException.ForErrorCodes(new[] { IscCodes.isc_arith_except, IscCodes.isc_string_truncation });
469469
}
@@ -642,7 +642,7 @@ public async ValueTask<byte[]> GetBytesAsync(CancellationToken cancellationToken
642642
else
643643
{
644644
var svalue = await GetStringAsync(cancellationToken).ConfigureAwait(false);
645-
if ((Field.Length % Field.Charset.BytesPerCharacter) == 0 && svalue.Length > Field.CharCount)
645+
if ((Field.Length % Field.Charset.BytesPerCharacter) == 0 && svalue.RuneCount() > Field.CharCount)
646646
{
647647
throw IscException.ForErrorCodes(new[] { IscCodes.isc_arith_except, IscCodes.isc_string_truncation });
648648
}
@@ -678,7 +678,7 @@ public async ValueTask<byte[]> GetBytesAsync(CancellationToken cancellationToken
678678
else
679679
{
680680
var svalue = await GetStringAsync(cancellationToken).ConfigureAwait(false);
681-
if ((Field.Length % Field.Charset.BytesPerCharacter) == 0 && svalue.Length > Field.CharCount)
681+
if ((Field.Length % Field.Charset.BytesPerCharacter) == 0 && svalue.RuneCount() > Field.CharCount)
682682
{
683683
throw IscException.ForErrorCodes(new[] { IscCodes.isc_arith_except, IscCodes.isc_string_truncation });
684684
}

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

+22
Original file line numberDiff line numberDiff line change
@@ -66,4 +66,26 @@ public static IEnumerable<IEnumerable<T>> Split<T>(this T[] array, int size)
6666
#if NETSTANDARD2_0
6767
public static HashSet<T> ToHashSet<T>(this IEnumerable<T> source) => new HashSet<T>(source);
6868
#endif
69+
70+
public static int RuneCount(this string s)
71+
{
72+
if (s == null)
73+
throw new ArgumentNullException(nameof(s));
74+
75+
#if NETSTANDARD2_0 || NETSTANDARD2_1 || NET48
76+
var cnt = 0;
77+
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;
86+
87+
#else
88+
return s.EnumerateRunes().Count();
89+
#endif
90+
}
6991
}

0 commit comments

Comments
 (0)