From 775182c08ba8b2b38864e392c3561c2b304570b3 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 4 Oct 2026 06:44:27 +0000 Subject: [PATCH 1/3] Initial plan From 4ae26ee86fcaf48385f91b37812f596377b5c7c9 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 4 Oct 2026 06:46:39 +0000 Subject: [PATCH 2/3] Add escaped surrogate-pair regression tests Co-authored-by: agocke <515774+agocke@users.noreply.github.com> --- test/Serde.Test/JsonDeserializeTests.cs | 9 +++ test/Serde.Test/RoundtripTests.cs | 17 +++++ ...est.RoundtripTests.UnicodeNote.ISerde.g.cs | 67 +++++++++++++++++++ ...pTests.UnicodeNote.ISerdeInfoProvider.g.cs | 18 +++++ ...dtripTests.UnicodeNote.ISerdeProvider.g.cs | 11 +++ 5 files changed, 122 insertions(+) create mode 100644 test/Serde.Test/generated/SerdeGenerator/Serde.SerdeImplRoslynGenerator/Serde.Test.RoundtripTests.UnicodeNote.ISerde.g.cs create mode 100644 test/Serde.Test/generated/SerdeGenerator/Serde.SerdeImplRoslynGenerator/Serde.Test.RoundtripTests.UnicodeNote.ISerdeInfoProvider.g.cs create mode 100644 test/Serde.Test/generated/SerdeGenerator/Serde.SerdeImplRoslynGenerator/Serde.Test.RoundtripTests.UnicodeNote.ISerdeProvider.g.cs diff --git a/test/Serde.Test/JsonDeserializeTests.cs b/test/Serde.Test/JsonDeserializeTests.cs index 0e2ad671..c47a9db6 100644 --- a/test/Serde.Test/JsonDeserializeTests.cs +++ b/test/Serde.Test/JsonDeserializeTests.cs @@ -59,6 +59,15 @@ public void DeserializeObject() ); } + [Fact] + public void DeserializeEscapedSurrogatePairs() + { + Assert.Equal( + new JsonValue.String("a🤖🌍z"), + JsonSerializer.DeserializeJsonValue("\"a\\uD83E\\uDD16\\uD83C\\uDF0Dz\"") + ); + } + [Fact] public void DeserializeInt() { diff --git a/test/Serde.Test/RoundtripTests.cs b/test/Serde.Test/RoundtripTests.cs index ac22a91f..79cc2911 100644 --- a/test/Serde.Test/RoundtripTests.cs +++ b/test/Serde.Test/RoundtripTests.cs @@ -31,6 +31,23 @@ public void TestArray() ); } + [GenerateSerde] + private partial record UnicodeNote + { + public required string Text { get; init; } + } + + [Fact] + public void EscapedSurrogatePairsRoundtrip() + { + var note = new UnicodeNote { Text = "a🤖🌍z" }; + Assert.Equal(note, JsonSerializer.Deserialize(JsonSerializer.Serialize(note))); + Assert.Equal( + note, + JsonSerializer.Deserialize("""{"text":"a\uD83E\uDD16\uD83C\uDF0Dz"}""") + ); + } + [GenerateSerde] [SerdeTypeOptions(MemberFormat = MemberFormat.None)] public partial record Point diff --git a/test/Serde.Test/generated/SerdeGenerator/Serde.SerdeImplRoslynGenerator/Serde.Test.RoundtripTests.UnicodeNote.ISerde.g.cs b/test/Serde.Test/generated/SerdeGenerator/Serde.SerdeImplRoslynGenerator/Serde.Test.RoundtripTests.UnicodeNote.ISerde.g.cs new file mode 100644 index 00000000..a2c70467 --- /dev/null +++ b/test/Serde.Test/generated/SerdeGenerator/Serde.SerdeImplRoslynGenerator/Serde.Test.RoundtripTests.UnicodeNote.ISerde.g.cs @@ -0,0 +1,67 @@ + +#nullable enable + +using System; +using Serde; + +namespace Serde.Test; + +partial class RoundtripTests +{ + partial record UnicodeNote + { + sealed partial class _SerdeObj : global::Serde.ISerde + { + global::Serde.ISerdeInfo global::Serde.ISerdeInfoProvider.SerdeInfo => Serde.Test.RoundtripTests.UnicodeNote.s_serdeInfo; + + void global::Serde.ISerialize.Serialize(Serde.Test.RoundtripTests.UnicodeNote value, global::Serde.ISerializer serializer) + { + var _l_info = global::Serde.SerdeInfoProvider.GetInfo(this); + var _l_type = serializer.WriteType(_l_info, 1); + _l_type.WriteString(_l_info, 0, value.Text); + _l_type.End(_l_info); + } + Serde.Test.RoundtripTests.UnicodeNote Serde.IDeserialize.Deserialize(IDeserializer deserializer) + { + string _l_text = default!; + + byte _r_assignedValid = 0; + + var _l_serdeInfo = global::Serde.SerdeInfoProvider.GetInfo(this); + var typeDeserialize = deserializer.ReadType(_l_serdeInfo); + while (true) + { + var (_l_index_, _) = typeDeserialize.TryReadIndexWithName(_l_serdeInfo); + if (_l_index_ == Serde.ITypeDeserializer.EndOfType) + { + break; + } + + switch (_l_index_) + { + case 0: + Serde.DeserializeException.ThrowIfDuplicate(_r_assignedValid, 0, _l_serdeInfo); + _l_text = typeDeserialize.ReadString(_l_serdeInfo, _l_index_); + _r_assignedValid |= ((byte)1) << 0; + break; + case Serde.ITypeDeserializer.IndexNotFound: + typeDeserialize.SkipValue(_l_serdeInfo, _l_index_); + break; + default: + throw new InvalidOperationException("Unexpected index: " + _l_index_); + } + } + typeDeserialize.End(_l_serdeInfo); + if ((_r_assignedValid & 0b1) != 0b1) + { + throw Serde.DeserializeException.UnassignedMember(_r_assignedValid, 0b1, _l_serdeInfo); + } + var newType = new Serde.Test.RoundtripTests.UnicodeNote() { + Text = _l_text, + }; + + return newType; + } + } + } +} diff --git a/test/Serde.Test/generated/SerdeGenerator/Serde.SerdeImplRoslynGenerator/Serde.Test.RoundtripTests.UnicodeNote.ISerdeInfoProvider.g.cs b/test/Serde.Test/generated/SerdeGenerator/Serde.SerdeImplRoslynGenerator/Serde.Test.RoundtripTests.UnicodeNote.ISerdeInfoProvider.g.cs new file mode 100644 index 00000000..cb58acc6 --- /dev/null +++ b/test/Serde.Test/generated/SerdeGenerator/Serde.SerdeImplRoslynGenerator/Serde.Test.RoundtripTests.UnicodeNote.ISerdeInfoProvider.g.cs @@ -0,0 +1,18 @@ + +#nullable enable + +namespace Serde.Test; + +partial class RoundtripTests +{ + partial record UnicodeNote + { + private static global::Serde.ISerdeInfo s_serdeInfo = Serde.SerdeInfo.MakeCustom( + "UnicodeNote", + typeof(Serde.Test.RoundtripTests.UnicodeNote).GetCustomAttributesData(), + new global::Serde.SerdeInfo.FieldInfo[] { + new("text", global::Serde.SerdeInfoProvider.GetSerializeInfo()) + } + ); + } +} diff --git a/test/Serde.Test/generated/SerdeGenerator/Serde.SerdeImplRoslynGenerator/Serde.Test.RoundtripTests.UnicodeNote.ISerdeProvider.g.cs b/test/Serde.Test/generated/SerdeGenerator/Serde.SerdeImplRoslynGenerator/Serde.Test.RoundtripTests.UnicodeNote.ISerdeProvider.g.cs new file mode 100644 index 00000000..505063a0 --- /dev/null +++ b/test/Serde.Test/generated/SerdeGenerator/Serde.SerdeImplRoslynGenerator/Serde.Test.RoundtripTests.UnicodeNote.ISerdeProvider.g.cs @@ -0,0 +1,11 @@ + +namespace Serde.Test; + +partial class RoundtripTests +{ + partial record UnicodeNote : Serde.ISerdeProvider + { + static Serde.Test.RoundtripTests.UnicodeNote._SerdeObj global::Serde.ISerdeProvider.Instance { get; } + = new Serde.Test.RoundtripTests.UnicodeNote._SerdeObj(); + } +} From a5426a70267b0f50df558bad257d82a4e432a7e5 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 4 Oct 2026 06:48:02 +0000 Subject: [PATCH 3/3] Fix escaped JSON surrogate-pair decoding Co-authored-by: agocke <515774+agocke@users.noreply.github.com> --- src/serde/json/reader/JsonReaderHelper.Unescaping.cs | 8 +++++--- test/Serde.Test/JsonDeserializeTests.cs | 7 ++++++- 2 files changed, 11 insertions(+), 4 deletions(-) diff --git a/src/serde/json/reader/JsonReaderHelper.Unescaping.cs b/src/serde/json/reader/JsonReaderHelper.Unescaping.cs index 91ef7b5d..67c7341d 100644 --- a/src/serde/json/reader/JsonReaderHelper.Unescaping.cs +++ b/src/serde/json/reader/JsonReaderHelper.Unescaping.cs @@ -545,10 +545,12 @@ out int written "invalid escape sequences must have already been caught by Utf8JsonReader.Read()" ); - if (!DecodeUnicodeEscape(source, destination, ref idx, ref written)) + int digitIdx = idx + 1; + if (!DecodeUnicodeEscape(source, destination, ref digitIdx, ref written)) { goto DestinationTooShort; } + idx = digitIdx - 1; break; } @@ -659,7 +661,7 @@ ref int written ); // We must have a low surrogate following a high surrogate. - if (source.Length < idx + 7 || source[idx + 1] != '\\' || source[idx + 2] != 'u') + if (source.Length < idx + 6 || source[idx] != '\\' || source[idx + 1] != 'u') { ThrowHelper.ThrowInvalidOperationException_ReadIncompleteUTF16(); } @@ -667,7 +669,7 @@ ref int written // The source is known to be valid JSON, and hence if we see a \u, it is guaranteed to have 4 hex digits following it // Otherwise, the Utf8JsonReader would have already thrown an exception. result = Utf8Parser.TryParse( - source.Slice(idx + 3, 4), + source.Slice(idx + 2, 4), out int lowSurrogate, out bytesConsumed, 'x' diff --git a/test/Serde.Test/JsonDeserializeTests.cs b/test/Serde.Test/JsonDeserializeTests.cs index c47a9db6..7917c5cb 100644 --- a/test/Serde.Test/JsonDeserializeTests.cs +++ b/test/Serde.Test/JsonDeserializeTests.cs @@ -62,10 +62,15 @@ public void DeserializeObject() [Fact] public void DeserializeEscapedSurrogatePairs() { + var value = new JsonValue.String("a🤖🌍z"); Assert.Equal( - new JsonValue.String("a🤖🌍z"), + value, JsonSerializer.DeserializeJsonValue("\"a\\uD83E\\uDD16\\uD83C\\uDF0Dz\"") ); + Assert.Equal( + value, + JsonSerializer.DeserializeJsonValue(JsonSerializer.Serialize(value)) + ); } [Fact]