This is an automated email from the ASF dual-hosted git repository.
chaokunyang pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/fory.git
The following commit(s) were added to refs/heads/main by this push:
new f5646ab24 chore(csharp): add more csharp and swift tests (#3597)
f5646ab24 is described below
commit f5646ab2446413333c674ad622ef8cca28c2127a
Author: Shawn Yang <[email protected]>
AuthorDate: Tue Apr 21 14:03:30 2026 +0800
chore(csharp): add more csharp and swift tests (#3597)
## Why?
## What does this PR do?
## Related issues
## AI Contribution Checklist
- [ ] Substantial AI assistance was used in this PR: `yes` / `no`
- [ ] If `yes`, I included a completed [AI Contribution
Checklist](https://github.com/apache/fory/blob/main/AI_POLICY.md#9-contributor-checklist-for-ai-assisted-prs)
in this PR description and the required `AI Usage Disclosure`.
- [ ] If `yes`, my PR description includes the required `ai_review`
summary and screenshot evidence of the final clean AI review results
from both fresh reviewers on the current PR diff or current HEAD after
the latest code changes.
## Does this PR introduce any user-facing change?
- [ ] Does this PR introduce any public API change?
- [ ] Does this PR introduce any binary protocol compatibility change?
## Benchmark
---
csharp/src/Fory/TimeSerializers.cs | 7 +-
csharp/tests/Fory.Tests/ByteBufferTests.cs | 252 ++++++++++++++++
csharp/tests/Fory.Tests/ForyRuntimeTests.cs | 233 ++++++++++++++
csharp/tests/Fory.Tests/RuntimeEdgeCaseTests.cs | 335 +++++++++++++++++++++
csharp/tests/Fory.Tests/StringSerializerTests.cs | 127 ++++++++
swift/Sources/Fory/AnySerializer.swift | 32 +-
swift/Sources/Fory/TypeResolver.swift | 50 ++-
swift/Tests/ForyTests/AnyTests.swift | 46 +++
swift/Tests/ForyTests/ByteBufferTests.swift | 230 ++++++++++++++
.../ForyTests/CollectionSerializerTests.swift | 313 +++++++++++++++++++
swift/Tests/ForyTests/CompatibilityTests.swift | 294 ++++++++++++++++++
swift/Tests/ForyTests/ForySwiftTests.swift | 138 +++++++++
swift/Tests/ForyTests/StringSerializerTests.swift | 147 +++++++++
swift/Tests/ForyTests/UnsignedTests.swift | 188 ++++++++++++
14 files changed, 2369 insertions(+), 23 deletions(-)
diff --git a/csharp/src/Fory/TimeSerializers.cs
b/csharp/src/Fory/TimeSerializers.cs
index 24c6825c1..61b216315 100644
--- a/csharp/src/Fory/TimeSerializers.cs
+++ b/csharp/src/Fory/TimeSerializers.cs
@@ -60,15 +60,16 @@ internal static class TimeCodec
{
long seconds = value.Ticks / TimeSpan.TicksPerSecond;
int nanos = checked((int)((value.Ticks % TimeSpan.TicksPerSecond) *
100));
- context.Writer.WriteInt64(seconds);
+ context.Writer.WriteVarInt64(seconds);
context.Writer.WriteInt32(nanos);
}
public static TimeSpan ReadDuration(ReadContext context)
{
- long seconds = context.Reader.ReadInt64();
+ long seconds = context.Reader.ReadVarInt64();
int nanos = context.Reader.ReadInt32();
- return TimeSpan.FromSeconds(seconds) + TimeSpan.FromTicks(nanos / 100);
+ long ticks = checked(seconds * TimeSpan.TicksPerSecond);
+ return TimeSpan.FromTicks(checked(ticks + (nanos / 100)));
}
private static (long Seconds, uint Nanos) ToTimestampParts(DateTimeOffset
value)
diff --git a/csharp/tests/Fory.Tests/ByteBufferTests.cs
b/csharp/tests/Fory.Tests/ByteBufferTests.cs
new file mode 100644
index 000000000..9da9705ac
--- /dev/null
+++ b/csharp/tests/Fory.Tests/ByteBufferTests.cs
@@ -0,0 +1,252 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements. See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership. The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License. You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+using Apache.Fory;
+
+namespace Apache.Fory.Tests;
+
+public sealed class ByteBufferTests
+{
+ public static TheoryData<uint, int> VarUInt32Cases => new()
+ {
+ { 0u, 1 },
+ { 0x7Fu, 1 },
+ { 0x80u, 2 },
+ { 0x3FFFu, 2 },
+ { 0x4000u, 3 },
+ { 0x1F_FFFFu, 3 },
+ { 0x20_0000u, 4 },
+ { 0x0FFF_FFFFu, 4 },
+ { 0x1000_0000u, 5 },
+ { uint.MaxValue, 5 },
+ };
+
+ public static TheoryData<ulong, int> VarUInt64Cases => new()
+ {
+ { 0UL, 1 },
+ { 0x7FUL, 1 },
+ { 0x80UL, 2 },
+ { (1UL << 14) - 1, 2 },
+ { 1UL << 14, 3 },
+ { (1UL << 21) - 1, 3 },
+ { 1UL << 21, 4 },
+ { (1UL << 28) - 1, 4 },
+ { 1UL << 28, 5 },
+ { (1UL << 35) - 1, 5 },
+ { 1UL << 35, 6 },
+ { (1UL << 42) - 1, 6 },
+ { 1UL << 42, 7 },
+ { (1UL << 49) - 1, 7 },
+ { 1UL << 49, 8 },
+ { (1UL << 56) - 1, 8 },
+ { 1UL << 56, 9 },
+ { ulong.MaxValue, 9 },
+ };
+
+ public static TheoryData<int> VarInt32Cases => new()
+ {
+ 0,
+ -1,
+ 1,
+ -63,
+ 63,
+ int.MinValue,
+ int.MaxValue,
+ };
+
+ public static TheoryData<long> VarInt64Cases => new()
+ {
+ 0L,
+ -1L,
+ 1L,
+ -63L,
+ 63L,
+ int.MinValue,
+ int.MaxValue,
+ long.MinValue,
+ long.MaxValue,
+ };
+
+ [Fact]
+ public void PrimitiveReadWriteRoundTrip()
+ {
+ ByteWriter writer = new(1);
+ writer.WriteUInt8(0xAB);
+ writer.WriteInt8(-7);
+ writer.WriteUInt16(0xCAFE);
+ writer.WriteInt16(-12_345);
+ writer.WriteUInt32(0x89ABCDEF);
+ writer.WriteInt32(-123_456_789);
+ writer.WriteUInt64(0xFEDCBA9876543210UL);
+ writer.WriteInt64(-1_234_567_890_123_456_789L);
+ writer.WriteFloat32(123.5f);
+ writer.WriteFloat64(-9876.25);
+ writer.WriteBytes([0x01, 0x02, 0x03, 0xFF]);
+
+ ByteReader reader = new(writer.ToArray());
+ Assert.Equal(0xAB, reader.ReadUInt8());
+ Assert.Equal(-7, reader.ReadInt8());
+ Assert.Equal(0xCAFE, reader.ReadUInt16());
+ Assert.Equal(-12_345, reader.ReadInt16());
+ Assert.Equal(0x89ABCDEFu, reader.ReadUInt32());
+ Assert.Equal(-123_456_789, reader.ReadInt32());
+ Assert.Equal(0xFEDCBA9876543210UL, reader.ReadUInt64());
+ Assert.Equal(-1_234_567_890_123_456_789L, reader.ReadInt64());
+ Assert.Equal(
+ BitConverter.SingleToInt32Bits(123.5f),
+ BitConverter.SingleToInt32Bits(reader.ReadFloat32()));
+ Assert.Equal(
+ BitConverter.DoubleToInt64Bits(-9876.25),
+ BitConverter.DoubleToInt64Bits(reader.ReadFloat64()));
+ Assert.Equal(new byte[] { 0x01, 0x02, 0x03, 0xFF },
reader.ReadBytes(4));
+ Assert.Equal(0, reader.Remaining);
+ }
+
+ [Theory]
+ [MemberData(nameof(VarUInt32Cases))]
+ public void VarUInt32RoundTripAndSize(uint value, int expectedBytes)
+ {
+ ByteWriter writer = new();
+ writer.WriteVarUInt32(value);
+ Assert.Equal(expectedBytes, writer.Count);
+
+ ByteReader reader = new(writer.ToArray());
+ Assert.Equal(value, reader.ReadVarUInt32());
+ Assert.Equal(0, reader.Remaining);
+ }
+
+ [Theory]
+ [MemberData(nameof(VarUInt64Cases))]
+ public void VarUInt64RoundTripAndSize(ulong value, int expectedBytes)
+ {
+ ByteWriter writer = new();
+ writer.WriteVarUInt64(value);
+ Assert.Equal(expectedBytes, writer.Count);
+
+ ByteReader reader = new(writer.ToArray());
+ Assert.Equal(value, reader.ReadVarUInt64());
+ Assert.Equal(0, reader.Remaining);
+ }
+
+ [Theory]
+ [MemberData(nameof(VarInt32Cases))]
+ public void VarInt32RoundTrip(int value)
+ {
+ ByteWriter writer = new();
+ writer.WriteVarInt32(value);
+
+ ByteReader reader = new(writer.ToArray());
+ Assert.Equal(value, reader.ReadVarInt32());
+ Assert.Equal(0, reader.Remaining);
+ }
+
+ [Theory]
+ [MemberData(nameof(VarInt64Cases))]
+ public void VarInt64RoundTrip(long value)
+ {
+ ByteWriter writer = new();
+ writer.WriteVarInt64(value);
+
+ ByteReader reader = new(writer.ToArray());
+ Assert.Equal(value, reader.ReadVarInt64());
+ Assert.Equal(0, reader.Remaining);
+ }
+
+ [Fact]
+ public void VarUInt36SmallBoundariesAndOverflow()
+ {
+ ByteWriter writer = new();
+ foreach (ulong value in new[] { 0UL, 31UL, 32UL, 1UL << 35, (1UL <<
36) - 1 })
+ {
+ writer.Reset();
+ writer.WriteVarUInt36Small(value);
+
+ ByteReader reader = new(writer.ToArray());
+ Assert.Equal(value, reader.ReadVarUInt36Small());
+ Assert.Equal(0, reader.Remaining);
+ }
+
+ Assert.Throws<EncodingException>(() => writer.WriteVarUInt36Small(1UL
<< 36));
+
+ writer.Reset();
+ writer.WriteVarUInt64(1UL << 36);
+ ByteReader overflowReader = new(writer.ToArray());
+ Assert.Throws<EncodingException>(() =>
overflowReader.ReadVarUInt36Small());
+ }
+
+ [Fact]
+ public void TaggedIntegersUseCompactAndWideEncodings()
+ {
+ AssertTaggedInt64(1_073_741_823L, expectedBytes: 4);
+ AssertTaggedInt64(1_073_741_824L, expectedBytes: 9);
+ AssertTaggedInt64(-1_073_741_824L, expectedBytes: 4);
+ AssertTaggedInt64(-1_073_741_825L, expectedBytes: 9);
+
+ AssertTaggedUInt64((ulong)int.MaxValue, expectedBytes: 4);
+ AssertTaggedUInt64((ulong)int.MaxValue + 1UL, expectedBytes: 9);
+ AssertTaggedUInt64(ulong.MaxValue, expectedBytes: 9);
+ }
+
+ [Fact]
+ public void SpanAndPatchOperationsMutateWrittenBytes()
+ {
+ ByteWriter writer = new();
+ Span<byte> span = writer.GetSpan(4);
+ span[0] = 10;
+ span[1] = 20;
+ span[2] = 30;
+ span[3] = 40;
+ writer.Advance(4);
+
+ writer.SetByte(1, 99);
+ writer.SetBytes(2, [7, 8]);
+
+ Assert.Equal(new byte[] { 10, 99, 7, 8 }, writer.ToArray());
+
+ writer.Reset();
+ Assert.Equal(0, writer.Count);
+ }
+
+ [Fact]
+ public void ReaderRejectsTruncatedVarInts()
+ {
+ Assert.Throws<OutOfBoundsException>(() => new
ByteReader([0x80]).ReadVarUInt32());
+ Assert.Throws<OutOfBoundsException>(() => new
ByteReader([0x80]).ReadVarUInt64());
+ }
+
+ private static void AssertTaggedInt64(long value, int expectedBytes)
+ {
+ ByteWriter writer = new();
+ writer.WriteTaggedInt64(value);
+ Assert.Equal(expectedBytes, writer.Count);
+
+ ByteReader reader = new(writer.ToArray());
+ Assert.Equal(value, reader.ReadTaggedInt64());
+ Assert.Equal(0, reader.Remaining);
+ }
+
+ private static void AssertTaggedUInt64(ulong value, int expectedBytes)
+ {
+ ByteWriter writer = new();
+ writer.WriteTaggedUInt64(value);
+ Assert.Equal(expectedBytes, writer.Count);
+
+ ByteReader reader = new(writer.ToArray());
+ Assert.Equal(value, reader.ReadTaggedUInt64());
+ Assert.Equal(0, reader.Remaining);
+ }
+}
diff --git a/csharp/tests/Fory.Tests/ForyRuntimeTests.cs
b/csharp/tests/Fory.Tests/ForyRuntimeTests.cs
index c2b4c07a9..355fcb628 100644
--- a/csharp/tests/Fory.Tests/ForyRuntimeTests.cs
+++ b/csharp/tests/Fory.Tests/ForyRuntimeTests.cs
@@ -115,6 +115,31 @@ public sealed class TwoStringFieldListHolder
public List<TwoStringField?> Items { get; set; } = [];
}
+[ForyObject]
+public sealed class OneStringFieldMapHolder
+{
+ public Dictionary<string, OneStringField?> Items { get; set; } = [];
+}
+
+[ForyObject]
+public sealed class TwoStringFieldMapHolder
+{
+ public Dictionary<string, TwoStringField?> Items { get; set; } = [];
+}
+
+[ForyObject]
+public sealed class UnsignedFields
+{
+ public byte U8 { get; set; }
+ public ushort U16 { get; set; }
+ public uint U32 { get; set; }
+ public ulong U64 { get; set; }
+ public byte? U8Nullable { get; set; }
+ public ushort? U16Nullable { get; set; }
+ public uint? U32Nullable { get; set; }
+ public ulong? U64Nullable { get; set; }
+}
+
[ForyObject]
public sealed class StructWithEnum
{
@@ -162,6 +187,13 @@ public sealed class CollectionContainerHolder
public Stack<int> StackField { get; set; } = new();
}
+[ForyObject]
+public sealed class NestedTypedContainers
+{
+ public List<List<string>> NestedLists { get; set; } = [];
+ public Dictionary<string, List<Address?>> NestedMap { get; set; } = [];
+}
+
public sealed class ForyRuntimeTests
{
private const ulong StringEncodingLatin1 = 0;
@@ -516,6 +548,40 @@ public sealed class ForyRuntimeTests
Assert.Equal(source.StackField.ToArray(),
decoded.StackField.ToArray());
}
+ [Fact]
+ public void GeneratedSerializerSupportsNestedTypedContainers()
+ {
+ ForyRuntime fory = ForyRuntime.Builder().TrackRef(true).Build();
+ fory.Register<Address>(452);
+ fory.Register<NestedTypedContainers>(453);
+
+ Address shared = new() { Street = "Main", Zip = 94107 };
+ NestedTypedContainers source = new()
+ {
+ NestedLists =
+ [
+ ["a", "b"],
+ ["c"],
+ ],
+ NestedMap = new Dictionary<string, List<Address?>>
+ {
+ ["first"] = [shared, null],
+ ["second"] = [shared],
+ },
+ };
+
+ NestedTypedContainers decoded =
fory.Deserialize<NestedTypedContainers>(fory.Serialize(source));
+ Assert.Equal(source.NestedLists.Count, decoded.NestedLists.Count);
+ Assert.Equal(source.NestedLists[0], decoded.NestedLists[0]);
+ Assert.Equal(source.NestedLists[1], decoded.NestedLists[1]);
+
+ Assert.Equal(2, decoded.NestedMap["first"].Count);
+ Assert.Single(decoded.NestedMap["second"]);
+ Assert.NotNull(decoded.NestedMap["first"][0]);
+ Assert.Same(decoded.NestedMap["first"][0],
decoded.NestedMap["second"][0]);
+ Assert.Null(decoded.NestedMap["first"][1]);
+ }
+
[Fact]
public void StreamDeserializeConsumesSingleFrame()
{
@@ -746,6 +812,66 @@ public sealed class ForyRuntimeTests
Assert.Equal(value.U64Tagged, decoded.U64Tagged);
}
+ [Fact]
+ public void TaggedUnsignedFieldUsesCompactBoundary()
+ {
+ ForyRuntime fory = ForyRuntime.Builder().Build();
+ fory.Register<EncodedNumbers>(301);
+
+ EncodedNumbers compact = new()
+ {
+ U32Fixed = 0x11223344u,
+ U64Tagged = (ulong)int.MaxValue,
+ };
+ EncodedNumbers wide = new()
+ {
+ U32Fixed = 0x11223344u,
+ U64Tagged = (ulong)int.MaxValue + 1UL,
+ };
+
+ byte[] compactPayload = fory.Serialize(compact);
+ byte[] widePayload = fory.Serialize(wide);
+
+ Assert.Equal(5, widePayload.Length - compactPayload.Length);
+ Assert.Equal(compact.U64Tagged,
fory.Deserialize<EncodedNumbers>(compactPayload).U64Tagged);
+ Assert.Equal(wide.U64Tagged,
fory.Deserialize<EncodedNumbers>(widePayload).U64Tagged);
+ }
+
+ [Theory]
+ [InlineData(false)]
+ [InlineData(true)]
+ public void UnsignedFieldsRoundTrip(bool compatible)
+ {
+ ForyRuntime fory =
ForyRuntime.Builder().Compatible(compatible).Build();
+ fory.Register<UnsignedFields>(302);
+
+ UnsignedFields highValues = new()
+ {
+ U8 = byte.MaxValue,
+ U16 = ushort.MaxValue,
+ U32 = uint.MaxValue,
+ U64 = ulong.MaxValue,
+ U8Nullable = 128,
+ U16Nullable = 40_000,
+ U32Nullable = 4_000_000_000u,
+ U64Nullable = ulong.MaxValue - 7,
+ };
+ AssertUnsignedEqual(highValues,
fory.Deserialize<UnsignedFields>(fory.Serialize(highValues)));
+
+ UnsignedFields nullablesMissing = new()
+ {
+ U8 = 0,
+ U16 = 1,
+ U32 = 2,
+ U64 = 3,
+ U8Nullable = null,
+ U16Nullable = null,
+ U32Nullable = null,
+ U64Nullable = null,
+ };
+ AssertUnsignedEqual(nullablesMissing,
fory.Deserialize<UnsignedFields>(fory.Serialize(nullablesMissing)));
+ }
+
[Fact]
public void CompatibleSchemaEvolutionRoundTrip()
{
@@ -849,6 +975,50 @@ public sealed class ForyRuntimeTests
Assert.Equal("world", thirdRound.F1);
}
+ [Theory]
+ [InlineData(false)]
+ [InlineData(true)]
+ public void CompatibleSchemaEvolutionRoundTripForMapValues(bool trackRef)
+ {
+ ForyRuntime writer =
ForyRuntime.Builder().Compatible(true).TrackRef(trackRef).Build();
+ writer.Register<OneStringField>(200);
+ writer.Register<OneStringFieldMapHolder>(203);
+
+ ForyRuntime reader =
ForyRuntime.Builder().Compatible(true).TrackRef(trackRef).Build();
+ reader.Register<TwoStringField>(200);
+ reader.Register<TwoStringFieldMapHolder>(203);
+
+ OneStringFieldMapHolder source = new()
+ {
+ Items = new Dictionary<string, OneStringField?>
+ {
+ ["first"] = new OneStringField { F1 = "hello" },
+ ["second"] = null,
+ ["third"] = new OneStringField { F1 = "world" },
+ },
+ };
+
+ TwoStringFieldMapHolder evolved =
reader.Deserialize<TwoStringFieldMapHolder>(writer.Serialize(source));
+ Assert.Equal(3, evolved.Items.Count);
+ TwoStringField first =
Assert.IsType<TwoStringField>(evolved.Items["first"]);
+ Assert.Equal("hello", first.F1);
+ Assert.Equal(string.Empty, first.F2);
+ Assert.Null(evolved.Items["second"]);
+ TwoStringField third =
Assert.IsType<TwoStringField>(evolved.Items["third"]);
+ Assert.Equal("world", third.F1);
+ Assert.Equal(string.Empty, third.F2);
+
+ first.F2 = "extra-first";
+ third.F2 = "extra-third";
+ OneStringFieldMapHolder roundTripped =
writer.Deserialize<OneStringFieldMapHolder>(reader.Serialize(evolved));
+ Assert.Equal(3, roundTripped.Items.Count);
+ OneStringField firstRound =
Assert.IsType<OneStringField>(roundTripped.Items["first"]);
+ Assert.Equal("hello", firstRound.F1);
+ Assert.Null(roundTripped.Items["second"]);
+ OneStringField thirdRound =
Assert.IsType<OneStringField>(roundTripped.Items["third"]);
+ Assert.Equal("world", thirdRound.F1);
+ }
+
[Fact]
public void SchemaVersionMismatchThrows()
{
@@ -1046,6 +1216,57 @@ public sealed class ForyRuntimeTests
Assert.Equal(1, nested[1]);
}
+ [Fact]
+ public void GeneratedSerializerPreservesSharedDynamicReferences()
+ {
+ ForyRuntime fory = ForyRuntime.Builder().TrackRef(true).Build();
+ fory.Register<DynamicAnyHolder>(401);
+
+ List<object?> shared = ["n", 1];
+ DynamicAnyHolder source = new()
+ {
+ AnyValue = shared,
+ AnySet = [shared],
+ AnyMap = new Dictionary<object, object?>
+ {
+ ["first"] = shared,
+ ["second"] = shared,
+ },
+ };
+
+ DynamicAnyHolder decoded =
fory.Deserialize<DynamicAnyHolder>(fory.Serialize(source));
+ List<object?> anyValue =
Assert.IsType<List<object?>>(decoded.AnyValue);
+ List<object?> setItem =
Assert.Single(decoded.AnySet.OfType<List<object?>>());
+ List<object?> first =
Assert.IsType<List<object?>>(decoded.AnyMap["first"]);
+ List<object?> second =
Assert.IsType<List<object?>>(decoded.AnyMap["second"]);
+
+ Assert.Same(anyValue, setItem);
+ Assert.Same(anyValue, first);
+ Assert.Same(anyValue, second);
+ }
+
+ [Fact]
+ public void CollectionRefTrackingPreservesSharedValues()
+ {
+ ForyRuntime fory = ForyRuntime.Builder().TrackRef(true).Build();
+ fory.Register<Address>(402);
+
+ Address shared = new() { Street = "Main", Zip = 94107 };
+
+ List<Address?> list = [shared, shared, null];
+ List<Address?> decodedList =
fory.Deserialize<List<Address?>>(fory.Serialize(list));
+ Assert.Same(decodedList[0], decodedList[1]);
+ Assert.Null(decodedList[2]);
+
+ Dictionary<string, Address> map = new()
+ {
+ ["left"] = shared,
+ ["right"] = shared,
+ };
+ Dictionary<string, Address> decodedMap =
fory.Deserialize<Dictionary<string, Address>>(fory.Serialize(map));
+ Assert.Same(decodedMap["left"], decodedMap["right"]);
+ }
+
[Fact]
public void StringSerializerUsesLatin1WhenAllCharsAreLatin1()
{
@@ -1312,4 +1533,16 @@ public sealed class ForyRuntimeTests
Assert.Equal(0, readContext.Reader.Remaining);
return (encoding, decoded);
}
+
+ private static void AssertUnsignedEqual(UnsignedFields expected,
UnsignedFields actual)
+ {
+ Assert.Equal(expected.U8, actual.U8);
+ Assert.Equal(expected.U16, actual.U16);
+ Assert.Equal(expected.U32, actual.U32);
+ Assert.Equal(expected.U64, actual.U64);
+ Assert.Equal(expected.U8Nullable, actual.U8Nullable);
+ Assert.Equal(expected.U16Nullable, actual.U16Nullable);
+ Assert.Equal(expected.U32Nullable, actual.U32Nullable);
+ Assert.Equal(expected.U64Nullable, actual.U64Nullable);
+ }
}
diff --git a/csharp/tests/Fory.Tests/RuntimeEdgeCaseTests.cs
b/csharp/tests/Fory.Tests/RuntimeEdgeCaseTests.cs
new file mode 100644
index 000000000..f2fc81e3a
--- /dev/null
+++ b/csharp/tests/Fory.Tests/RuntimeEdgeCaseTests.cs
@@ -0,0 +1,335 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements. See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership. The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License. You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+using Apache.Fory;
+using ForyRuntime = Apache.Fory.Fory;
+
+namespace Apache.Fory.Tests;
+
+[ForyObject]
+public sealed class TimeEnvelope
+{
+ public DateOnly Date { get; set; }
+ public DateTime Timestamp { get; set; }
+ public DateTimeOffset OffsetTimestamp { get; set; }
+ public TimeSpan Duration { get; set; }
+ public List<DateOnly> Dates { get; set; } = [];
+ public List<DateTime> Timestamps { get; set; } = [];
+ public List<DateTimeOffset> OffsetTimestamps { get; set; } = [];
+ public List<TimeSpan> Durations { get; set; } = [];
+}
+
+[ForyObject]
+public sealed class NullableEnvelope
+{
+ public int? Int32Value { get; set; }
+ public ulong? UInt64Value { get; set; }
+ public DateTimeOffset? Timestamp { get; set; }
+ public TestColor? Color { get; set; }
+}
+
+[ForyObject]
+public sealed class CustomPayload
+{
+ public int Id { get; set; }
+ public string Marker { get; set; } = string.Empty;
+}
+
+public sealed class CustomPayloadSerializer : Serializer<CustomPayload>
+{
+ public override CustomPayload DefaultValue => null!;
+
+ public override void WriteData(WriteContext context, in CustomPayload
value, bool hasGenerics)
+ {
+ _ = hasGenerics;
+ context.Writer.WriteVarInt32((value ?? new CustomPayload()).Id + 7);
+ }
+
+ public override CustomPayload ReadData(ReadContext context)
+ {
+ return new CustomPayload
+ {
+ Id = context.Reader.ReadVarInt32() - 7,
+ Marker = "custom",
+ };
+ }
+}
+
+public sealed class RuntimeEdgeCaseTests
+{
+ [Fact]
+ public void TimeRoundTripEdgeCases()
+ {
+ ForyRuntime fory = ForyRuntime.Builder().Build();
+
+ DateOnly date = new(1960, 2, 29);
+ Assert.Equal(date, fory.Deserialize<DateOnly>(fory.Serialize(date)));
+
+ DateTimeOffset offset =
DateTimeOffset.FromUnixTimeMilliseconds(-1).AddTicks(45);
+ Assert.Equal(offset,
fory.Deserialize<DateTimeOffset>(fory.Serialize(offset)));
+
+ TimeSpan duration = TimeSpan.FromDays(-3) -
TimeSpan.FromMilliseconds(45) - TimeSpan.FromTicks(67);
+ Assert.Equal(duration,
fory.Deserialize<TimeSpan>(fory.Serialize(duration)));
+
+ DateTime utc = new DateTime(2024, 1, 2, 3, 4, 5, 678,
DateTimeKind.Utc).AddTicks(9);
+ AssertDateTimeEqual(utc,
fory.Deserialize<DateTime>(fory.Serialize(utc)));
+
+ DateTime local = new DateTime(2024, 1, 2, 3, 4, 5, 678,
DateTimeKind.Local).AddTicks(9);
+ AssertDateTimeEqual(local.ToUniversalTime(),
fory.Deserialize<DateTime>(fory.Serialize(local)));
+
+ DateTime unspecified = DateTime.SpecifyKind(new DateTime(2024, 1, 2,
3, 4, 5, 678).AddTicks(9), DateTimeKind.Unspecified);
+ AssertDateTimeEqual(
+ DateTime.SpecifyKind(unspecified, DateTimeKind.Utc),
+ fory.Deserialize<DateTime>(fory.Serialize(unspecified)));
+ }
+
+ [Fact]
+ public void TimeFieldsAndTypedListsRoundTrip()
+ {
+ ForyRuntime fory = ForyRuntime.Builder().Build();
+ fory.Register<TimeEnvelope>(700);
+
+ TimeEnvelope source = new()
+ {
+ Date = new DateOnly(1969, 12, 31),
+ Timestamp = new DateTime(2024, 1, 2, 3, 4, 5, 678,
DateTimeKind.Local).AddTicks(9),
+ OffsetTimestamp = new DateTimeOffset(2024, 1, 2, 3, 4, 5, 678,
TimeSpan.FromHours(5)).AddTicks(9),
+ Duration = TimeSpan.FromTicks(-12_345_678_901),
+ Dates = [new DateOnly(1969, 12, 31), new DateOnly(1970, 1, 1), new
DateOnly(2024, 4, 21)],
+ Timestamps =
+ [
+ new DateTime(2024, 1, 2, 3, 4, 5, 678,
DateTimeKind.Utc).AddTicks(9),
+ new DateTime(2024, 1, 2, 3, 4, 5, 678,
DateTimeKind.Local).AddTicks(10),
+ DateTime.SpecifyKind(new DateTime(2024, 1, 2, 3, 4, 5,
678).AddTicks(11), DateTimeKind.Unspecified),
+ ],
+ OffsetTimestamps =
+ [
+ DateTimeOffset.FromUnixTimeMilliseconds(-1),
+ new DateTimeOffset(2024, 1, 2, 3, 4, 5, 678,
TimeSpan.FromHours(-7)).AddTicks(12),
+ ],
+ Durations =
+ [
+ TimeSpan.Zero,
+ TimeSpan.FromTicks(123_456_789),
+ TimeSpan.FromTicks(-123_456_789),
+ ],
+ };
+
+ TimeEnvelope decoded =
fory.Deserialize<TimeEnvelope>(fory.Serialize(source));
+ Assert.Equal(source.Date, decoded.Date);
+ AssertDateTimeEqual(source.Timestamp.ToUniversalTime(),
decoded.Timestamp);
+ Assert.Equal(source.OffsetTimestamp, decoded.OffsetTimestamp);
+ Assert.Equal(source.Duration, decoded.Duration);
+ Assert.Equal(source.Dates, decoded.Dates);
+ Assert.Equal(source.OffsetTimestamps, decoded.OffsetTimestamps);
+ Assert.Equal(source.Durations, decoded.Durations);
+
+ Assert.Equal(source.Timestamps.Count, decoded.Timestamps.Count);
+ for (int i = 0; i < source.Timestamps.Count; i++)
+ {
+ AssertDateTimeEqual(NormalizeDateTime(source.Timestamps[i]),
decoded.Timestamps[i]);
+ }
+ }
+
+ [Fact]
+ public void TimeSpanUsesVarIntSeconds()
+ {
+ ForyRuntime fory = ForyRuntime.Builder().Build();
+ byte[] payload = fory.Serialize(TimeSpan.FromSeconds(1) +
TimeSpan.FromTicks(3));
+
+ ByteReader reader = new(payload);
+ Assert.False(fory.ReadHead(reader));
+ Assert.Equal((sbyte)RefFlag.NotNullValue, reader.ReadInt8());
+ Assert.Equal((uint)TypeId.Duration, reader.ReadUInt8());
+ Assert.Equal(1L, reader.ReadVarInt64());
+ Assert.Equal(300, reader.ReadInt32());
+ Assert.Equal(0, reader.Remaining);
+ }
+
+ [Fact]
+ public void TimestampNormalizesNegativeFractionalSecond()
+ {
+ ForyRuntime fory = ForyRuntime.Builder().Build();
+ byte[] payload =
fory.Serialize(DateTimeOffset.FromUnixTimeMilliseconds(-1));
+
+ ByteReader reader = new(payload);
+ Assert.False(fory.ReadHead(reader));
+ Assert.Equal((sbyte)RefFlag.NotNullValue, reader.ReadInt8());
+ Assert.Equal((uint)TypeId.Timestamp, reader.ReadUInt8());
+ Assert.Equal(-1L, reader.ReadInt64());
+ Assert.Equal(999_000_000u, reader.ReadUInt32());
+ Assert.Equal(0, reader.Remaining);
+ }
+
+ [Fact]
+ public void NullableValuesRoundTrip()
+ {
+ ForyRuntime fory = ForyRuntime.Builder().Build();
+ fory.Register<TestColor>(704);
+
+ Assert.Null(fory.Deserialize<int?>(fory.Serialize<int?>(null)));
+ Assert.Equal(123, fory.Deserialize<int?>(fory.Serialize<int?>(123)));
+ Assert.Equal(ulong.MaxValue,
fory.Deserialize<ulong?>(fory.Serialize<ulong?>(ulong.MaxValue)));
+
+ DateTimeOffset timestamp =
DateTimeOffset.FromUnixTimeMilliseconds(-1).AddTicks(23);
+ Assert.Equal(timestamp,
fory.Deserialize<DateTimeOffset?>(fory.Serialize<DateTimeOffset?>(timestamp)));
+
+
Assert.Null(fory.Deserialize<TestColor?>(fory.Serialize<TestColor?>(null)));
+ Assert.Equal(TestColor.Red,
fory.Deserialize<TestColor?>(fory.Serialize<TestColor?>(TestColor.Red)));
+
+ List<int?> list = [null, 0, int.MaxValue];
+ Assert.Equal(list, fory.Deserialize<List<int?>>(fory.Serialize(list)));
+ }
+
+ [Fact]
+ public void NullableFieldsRoundTrip()
+ {
+ ForyRuntime fory = ForyRuntime.Builder().Build();
+ fory.Register<TestColor>(705);
+ fory.Register<NullableEnvelope>(701);
+
+ NullableEnvelope populated = new()
+ {
+ Int32Value = int.MinValue,
+ UInt64Value = ulong.MaxValue,
+ Timestamp =
DateTimeOffset.FromUnixTimeMilliseconds(-1).AddTicks(23),
+ Color = (TestColor)12345,
+ };
+ NullableEnvelope decodedPopulated =
fory.Deserialize<NullableEnvelope>(fory.Serialize(populated));
+ Assert.Equal(populated.Int32Value, decodedPopulated.Int32Value);
+ Assert.Equal(populated.UInt64Value, decodedPopulated.UInt64Value);
+ Assert.Equal(populated.Timestamp, decodedPopulated.Timestamp);
+ Assert.Equal(populated.Color, decodedPopulated.Color);
+
+ NullableEnvelope missing = new();
+ NullableEnvelope decodedMissing =
fory.Deserialize<NullableEnvelope>(fory.Serialize(missing));
+ Assert.Null(decodedMissing.Int32Value);
+ Assert.Null(decodedMissing.UInt64Value);
+ Assert.Null(decodedMissing.Timestamp);
+ Assert.Null(decodedMissing.Color);
+ }
+
+ [Fact]
+ public void CustomSerializerRegistrationByIdRoundTrip()
+ {
+ ForyRuntime fory = ForyRuntime.Builder().Build();
+ fory.Register<CustomPayload, CustomPayloadSerializer>(702);
+
+ CustomPayload decoded = fory.Deserialize<CustomPayload>(
+ fory.Serialize(new CustomPayload { Id = 42, Marker = "ignored" }));
+
+ Assert.Equal(42, decoded.Id);
+ Assert.Equal("custom", decoded.Marker);
+ }
+
+ [Fact]
+ public void
ThreadSafeCustomSerializerNamedRegistrationAppliesToInitializedLocal()
+ {
+ using ThreadSafeFory fory = ForyRuntime.Builder().BuildThreadSafe();
+ _ = fory.Serialize(1);
+ fory.Register<CustomPayload, CustomPayloadSerializer>(string.Empty,
"custom_payload");
+
+ CustomPayload decoded = fory.Deserialize<CustomPayload>(
+ fory.Serialize(new CustomPayload { Id = 7, Marker = "ignored" }));
+
+ Assert.Equal(7, decoded.Id);
+ Assert.Equal("custom", decoded.Marker);
+ }
+
+ [Fact]
+ public void DeserializeRejectsTrailingBytes()
+ {
+ ForyRuntime fory = ForyRuntime.Builder().Build();
+ byte[] payload = fory.Serialize(123);
+ byte[] invalidPayload = [.. payload, 0x7F];
+
+ InvalidDataException exception =
Assert.Throws<InvalidDataException>(() =>
fory.Deserialize<int>(invalidPayload));
+ Assert.Contains("unexpected trailing bytes", exception.Message,
StringComparison.Ordinal);
+ }
+
+ [Fact]
+ public void DeserializeRejectsNonXlangBitmap()
+ {
+ ForyRuntime fory = ForyRuntime.Builder().Build();
+ byte[] payload = fory.Serialize(123);
+ payload[0] = 0;
+
+ InvalidDataException exception =
Assert.Throws<InvalidDataException>(() => fory.Deserialize<int>(payload));
+ Assert.Contains("xlang bitmap mismatch", exception.Message,
StringComparison.Ordinal);
+ }
+
+ [Fact]
+ public void DynamicAnyRejectsUnknownUserTypeId()
+ {
+ ForyRuntime writer = ForyRuntime.Builder().Build();
+ writer.Register<CustomPayload, CustomPayloadSerializer>(703);
+ byte[] payload = writer.Serialize<object?>(new CustomPayload { Id = 9,
Marker = "ignored" });
+ byte[] invalidPayload = RewriteRootUserTypeId(payload, TypeId.Ext,
704);
+
+ ForyRuntime reader = ForyRuntime.Builder().Build();
+ TypeNotRegisteredException exception =
+ Assert.Throws<TypeNotRegisteredException>(() =>
reader.Deserialize<object?>(invalidPayload));
+ Assert.Contains("user_type_id=704", exception.Message,
StringComparison.Ordinal);
+ }
+
+ [Fact]
+ public void ThreadSafeForyThrowsAfterDispose()
+ {
+ ThreadSafeFory fory = ForyRuntime.Builder().BuildThreadSafe();
+ byte[] payload = fory.Serialize(123);
+ fory.Dispose();
+
+ Assert.Throws<ObjectDisposedException>(() => fory.Serialize(1));
+ Assert.Throws<ObjectDisposedException>(() =>
fory.Deserialize<int>(payload));
+ Assert.Throws<ObjectDisposedException>(() => fory.Register<Node>(999));
+ }
+
+ private static DateTime NormalizeDateTime(DateTime value)
+ {
+ return value.Kind switch
+ {
+ DateTimeKind.Utc => value,
+ DateTimeKind.Local => value.ToUniversalTime(),
+ _ => DateTime.SpecifyKind(value, DateTimeKind.Utc),
+ };
+ }
+
+ private static void AssertDateTimeEqual(DateTime expected, DateTime actual)
+ {
+ Assert.Equal(expected, actual);
+ Assert.Equal(DateTimeKind.Utc, actual.Kind);
+ }
+
+ private static byte[] RewriteRootUserTypeId(byte[] payload, TypeId
expectedWireTypeId, uint replacementUserTypeId)
+ {
+ ByteReader reader = new(payload);
+ _ = reader.ReadUInt8(); // frame header bitmap
+ _ = reader.ReadInt8(); // root ref flag
+ uint wireTypeId = reader.ReadUInt8();
+ Assert.Equal((uint)expectedWireTypeId, wireTypeId);
+
+ int userTypeIdStart = reader.Cursor;
+ _ = reader.ReadVarUInt32();
+ int userTypeIdEnd = reader.Cursor;
+
+ ByteWriter writer = new(payload.Length + 5);
+ writer.WriteBytes(payload.AsSpan(0, userTypeIdStart));
+ writer.WriteVarUInt32(replacementUserTypeId);
+ writer.WriteBytes(payload.AsSpan(userTypeIdEnd));
+ return writer.ToArray();
+ }
+}
diff --git a/csharp/tests/Fory.Tests/StringSerializerTests.cs
b/csharp/tests/Fory.Tests/StringSerializerTests.cs
new file mode 100644
index 000000000..1b9ca4bf8
--- /dev/null
+++ b/csharp/tests/Fory.Tests/StringSerializerTests.cs
@@ -0,0 +1,127 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements. See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership. The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License. You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+using Apache.Fory;
+
+namespace Apache.Fory.Tests;
+
+public sealed class StringSerializerTests
+{
+ private const ulong Latin1 = 0;
+ private const ulong Utf16 = 1;
+ private const ulong Utf8 = 2;
+
+ [Fact]
+ public void StringRoundTripEdgeCases()
+ {
+ string[] values =
+ [
+ string.Empty,
+ "\0",
+ "\n\r\t",
+ "\u00FF",
+ "\u0100",
+ "café",
+ "你好",
+ "abc你好",
+ "hello café 你好",
+ "😀😁",
+ new string('\u00E9', 64),
+ new string('你', 64),
+ ];
+
+ foreach (string value in values)
+ {
+ (_, string decoded, _, _) = WriteAndReadString(value);
+ Assert.Equal(value, decoded);
+ }
+ }
+
+ [Fact]
+ public void StringSerializerHandlesHeaderSizeBoundaries()
+ {
+ AssertHeader(new string('a', 31), Latin1, expectedHeaderBytes: 1);
+ AssertHeader(new string('a', 32), Latin1, expectedHeaderBytes: 2);
+
+ AssertHeader(new string('你', 15), Utf16, expectedHeaderBytes: 1);
+ AssertHeader(new string('你', 16), Utf16, expectedHeaderBytes: 2);
+
+ AssertHeader(new string('a', 28) + "你", Utf8, expectedHeaderBytes: 1);
+ AssertHeader(new string('a', 29) + "你", Utf8, expectedHeaderBytes: 2);
+
+ AssertHeader(new string('a', 4092) + "你", Utf8, expectedHeaderBytes:
2);
+ AssertHeader(new string('a', 4093) + "你", Utf8, expectedHeaderBytes:
3);
+ }
+
+ [Fact]
+ public void StringSerializerRejectsOddUtf16Payload()
+ {
+ ByteWriter writer = new();
+ writer.WriteVarUInt36Small((3UL << 2) | Utf16);
+ writer.WriteBytes([0x61, 0x00, 0x62]);
+
+ ReadContext context = new(new ByteReader(writer.ToArray()), new
TypeResolver(), trackRef: false, compatible: false);
+ Assert.Throws<EncodingException>(() =>
StringSerializer.ReadString(context));
+ }
+
+ [Fact]
+ public void StringSerializerRejectsUnknownEncoding()
+ {
+ ByteWriter writer = new();
+ writer.WriteVarUInt36Small(3UL);
+
+ ReadContext context = new(new ByteReader(writer.ToArray()), new
TypeResolver(), trackRef: false, compatible: false);
+ Assert.Throws<EncodingException>(() =>
StringSerializer.ReadString(context));
+ }
+
+ private static void AssertHeader(string value, ulong expectedEncoding, int
expectedHeaderBytes)
+ {
+ (ulong encoding, string decoded, int headerBytes, int byteLength) =
WriteAndReadString(value);
+ Assert.Equal(expectedEncoding, encoding);
+ Assert.Equal(expectedHeaderBytes, headerBytes);
+ Assert.Equal(value, decoded);
+ Assert.Equal(byteLength + headerBytes, GetPayloadLength(value));
+ }
+
+ private static int GetPayloadLength(string value)
+ {
+ ByteWriter writer = new();
+ WriteContext context = new(writer, new TypeResolver(), trackRef:
false, compatible: false);
+ StringSerializer.WriteString(context, value);
+ return writer.Count;
+ }
+
+ private static (ulong Encoding, string Decoded, int HeaderBytes, int
ByteLength) WriteAndReadString(string value)
+ {
+ ByteWriter writer = new();
+ TypeResolver resolver = new();
+ WriteContext writeContext = new(writer, resolver, trackRef: false,
compatible: false);
+ StringSerializer.WriteString(writeContext, value);
+
+ byte[] payload = writer.ToArray();
+ ByteReader headerReader = new(payload);
+ ulong header = headerReader.ReadVarUInt36Small();
+ ulong encoding = header & 0x03;
+ int byteLength = checked((int)(header >> 2));
+ Assert.Equal(payload.Length - headerReader.Cursor, byteLength);
+
+ ReadContext readContext = new(new ByteReader(payload), resolver,
trackRef: false, compatible: false);
+ string decoded = StringSerializer.ReadString(readContext);
+ Assert.Equal(0, readContext.Reader.Remaining);
+ return (encoding, decoded, headerReader.Cursor, byteLength);
+ }
+}
diff --git a/swift/Sources/Fory/AnySerializer.swift
b/swift/Sources/Fory/AnySerializer.swift
index cd2fda808..3b64fece8 100644
--- a/swift/Sources/Fory/AnySerializer.swift
+++ b/swift/Sources/Fory/AnySerializer.swift
@@ -215,13 +215,7 @@ struct SerializableAny: Serializer {
context.buffer.writeInt8(RefFlag.null.rawValue)
return
}
- if refMode == .tracking, anyValueIsRefType(value), let object =
value as AnyObject? {
- if context.refWriter.tryWriteRef(buffer: context.buffer,
object: object) {
- return
- }
- } else {
- context.buffer.writeInt8(RefFlag.notNullValue.rawValue)
- }
+ context.buffer.writeInt8(RefFlag.notNullValue.rawValue)
}
if writeTypeInfo {
@@ -273,7 +267,11 @@ struct SerializableAny: Serializer {
let remoteTypeInfo = try requireDynamicTypeInfo()
let value = try foryReadCompatibleData(context,
remoteTypeInfo: remoteTypeInfo)
if let reservedRefID {
- context.refReader.storeRef(value, at: reservedRefID)
+ if let object = value.value as AnyObject? {
+ context.refReader.storeRef(object, at: reservedRefID)
+ } else {
+ context.refReader.storeRef(value, at: reservedRefID)
+ }
}
return value
case .notNullValue:
@@ -296,13 +294,6 @@ private func unwrapOptionalAny(_ value: Any) -> Any? {
return child
}
-private func anyValueIsRefType(_ value: Any) -> Bool {
- guard let serializer = value as? any Serializer else {
- return false
- }
- return type(of: serializer).isRefType
-}
-
private func toAnyHashableKey(_ value: Any) throws -> AnyHashable {
if let anyHashable = value as? AnyHashable {
return anyHashable
@@ -339,7 +330,16 @@ private func writeAnyPayload(_ value: Any, context:
WriteContext, hasGenerics: B
defer { context.leaveDynamicAnyDepth() }
if let serializer = value as? any Serializer {
- try serializer.foryWriteData(context, hasGenerics: hasGenerics)
+ if type(of: serializer).isRefType {
+ try serializer.foryWrite(
+ context,
+ refMode: .tracking,
+ writeTypeInfo: false,
+ hasGenerics: hasGenerics
+ )
+ } else {
+ try serializer.foryWriteData(context, hasGenerics: hasGenerics)
+ }
return
}
if let list = value as? [Any] {
diff --git a/swift/Sources/Fory/TypeResolver.swift
b/swift/Sources/Fory/TypeResolver.swift
index e0f49ab50..57b795ab7 100644
--- a/swift/Sources/Fory/TypeResolver.swift
+++ b/swift/Sources/Fory/TypeResolver.swift
@@ -144,6 +144,48 @@ private func encodedTypeDefHasUserTypeFields(_ fields:
[TypeMeta.FieldInfo]) ->
fields.contains { fieldNeedsTypeInfo($0.fieldType) }
}
+@inline(__always)
+private func readRegisteredValue<T: Serializer>(_ context: ReadContext, as
type: T.Type) throws -> T {
+ try T.foryRead(
+ context,
+ refMode: T.isRefType ? .tracking : .none,
+ readTypeInfo: false
+ )
+}
+
+@inline(__always)
+private func readCompatibleRegisteredValue<T: Serializer>(
+ _ context: ReadContext,
+ as type: T.Type,
+ remoteTypeInfo: TypeInfo
+) throws -> T {
+ guard T.isRefType else {
+ return try T.foryReadCompatibleData(context, remoteTypeInfo:
remoteTypeInfo)
+ }
+
+ let rawFlag = try context.buffer.readInt8()
+ guard let flag = RefFlag(rawValue: rawFlag) else {
+ throw ForyError.refError("invalid ref flag \(rawFlag)")
+ }
+
+ switch flag {
+ case .null:
+ return T.foryDefault()
+ case .ref:
+ let refID = try context.buffer.readVarUInt32()
+ return try context.refReader.readRef(refID, as: T.self)
+ case .refValue:
+ let reservedRefID = context.trackRef ?
context.refReader.reserveRefID() : nil
+ let value = try T.foryReadCompatibleData(context, remoteTypeInfo:
remoteTypeInfo)
+ if let reservedRefID, let object = value as AnyObject? {
+ context.refReader.storeRef(object, at: reservedRefID)
+ }
+ return value
+ case .notNullValue:
+ return try T.foryReadCompatibleData(context, remoteTypeInfo:
remoteTypeInfo)
+ }
+}
+
public final class TypeInfo: @unchecked Sendable {
static let uncached = TypeInfo(typeID: .unknown)
@@ -411,10 +453,10 @@ final class TypeResolver {
typeName: MetaString.empty(specialChar1: "$", specialChar2: "_"),
fields: T.foryFieldsInfo(trackRef: trackRef),
reader: { context in
- try T.foryRead(context, refMode: .none, readTypeInfo: false)
+ try readRegisteredValue(context, as: T.self)
},
compatibleReader: { context, remoteTypeInfo in
- try T.foryReadCompatibleData(context, remoteTypeInfo:
remoteTypeInfo)
+ try readCompatibleRegisteredValue(context, as: T.self,
remoteTypeInfo: remoteTypeInfo)
}
)
@@ -462,10 +504,10 @@ final class TypeResolver {
typeName: typeNameMeta,
fields: T.foryFieldsInfo(trackRef: trackRef),
reader: { context in
- try T.foryRead(context, refMode: .none, readTypeInfo: false)
+ try readRegisteredValue(context, as: T.self)
},
compatibleReader: { context, remoteTypeInfo in
- try T.foryReadCompatibleData(context, remoteTypeInfo:
remoteTypeInfo)
+ try readCompatibleRegisteredValue(context, as: T.self,
remoteTypeInfo: remoteTypeInfo)
}
)
diff --git a/swift/Tests/ForyTests/AnyTests.swift
b/swift/Tests/ForyTests/AnyTests.swift
index 278d2e2d8..c3d0433b6 100644
--- a/swift/Tests/ForyTests/AnyTests.swift
+++ b/swift/Tests/ForyTests/AnyTests.swift
@@ -41,6 +41,19 @@ private final class AnyObjectDynamicNode {
}
}
+@ForyObject
+private final class AnyObjectDynamicGraphNode {
+ var value: Int32 = 0
+ var next: AnyObjectDynamicGraphNode?
+
+ required init() {}
+
+ init(value: Int32, next: AnyObjectDynamicGraphNode? = nil) {
+ self.value = value
+ self.next = next
+ }
+}
+
@ForyObject
private struct AnyHashableMapHolder {
var map: [AnyHashable: Any] = [:]
@@ -409,6 +422,39 @@ func topLevelAnyHomogeneousListAndMapRoundTrip() throws {
#expect(map?["k2"] as? String == "v2")
}
+@Test
+func dynamicAnyListTracksRefs() throws {
+ let fory = Fory(config: .init(xlang: true, trackRef: true))
+ fory.register(AnyObjectDynamicGraphNode.self, id: 503)
+
+ let shared = AnyObjectDynamicGraphNode(value: 17)
+ let payload = try fory.serialize([shared, shared] as [Any])
+ let decoded: Any = try fory.deserialize(payload)
+ let list = decoded as? [Any]
+ let first = list?.first as? AnyObjectDynamicGraphNode
+ let second = list?.dropFirst().first as? AnyObjectDynamicGraphNode
+
+ #expect(list?.count == 2)
+ #expect(first != nil)
+ #expect(first === second)
+}
+
+@Test
+func dynamicAnyObjectTracksCycle() throws {
+ let fory = Fory(config: .init(xlang: true, trackRef: true))
+ fory.register(AnyObjectDynamicGraphNode.self, id: 504)
+
+ let node = AnyObjectDynamicGraphNode(value: 21)
+ node.next = node
+
+ let decoded: AnyObject = try fory.deserialize(try fory.serialize(node as
AnyObject))
+ let graphNode = decoded as? AnyObjectDynamicGraphNode
+
+ #expect(graphNode != nil)
+ #expect(graphNode?.value == 21)
+ #expect(graphNode?.next === graphNode)
+}
+
@Test
func dynamicAnyMaxDepthRejectsDeepNesting() throws {
let value = nestedDynamicAnyList(depth: 3)
diff --git a/swift/Tests/ForyTests/ByteBufferTests.swift
b/swift/Tests/ForyTests/ByteBufferTests.swift
new file mode 100644
index 000000000..fc26f7564
--- /dev/null
+++ b/swift/Tests/ForyTests/ByteBufferTests.swift
@@ -0,0 +1,230 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements. See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership. The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License. You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+import Foundation
+import Testing
+@testable import Fory
+
+@Test
+func byteBufferPrimitiveReadWriteAndCursorOps() throws {
+ let buffer = ByteBuffer(capacity: 8)
+ buffer.writeUInt8(0xFE)
+ buffer.writeInt8(-12)
+ buffer.writeInt16(Int16.max)
+ buffer.writeInt32(Int32.min)
+ buffer.writeInt64(Int64.max)
+ buffer.writeFloat32(3.25)
+ buffer.writeFloat64(-123.5)
+ buffer.writeBytes([0x01, 0x02, 0x03, 0x04])
+
+ #expect(buffer.count == 32)
+ #expect(buffer.getCursor() == 0)
+
+ #expect(try buffer.readUInt8() == 0xFE)
+ #expect(try buffer.readInt8() == -12)
+ #expect(try buffer.readInt16() == Int16.max)
+
+ let int32Offset = buffer.getCursor()
+ #expect(try buffer.readInt32() == Int32.min)
+ buffer.moveBack(4)
+ #expect(buffer.getCursor() == int32Offset)
+ #expect(try buffer.readInt32() == Int32.min)
+
+ #expect(try buffer.readInt64() == Int64.max)
+ #expect(try buffer.readFloat32() == 3.25)
+ #expect(try buffer.readFloat64() == -123.5)
+ #expect(try buffer.readBytes(count: 4) == [0x01, 0x02, 0x03, 0x04])
+ #expect(buffer.remaining == 0)
+}
+
+@Test
+func byteBufferReplaceMutationAndSnapshots() throws {
+ let buffer = ByteBuffer(bytes: [0x01, 0x02, 0x03, 0x04])
+ _ = try buffer.readUInt8()
+ _ = try buffer.readUInt8()
+ #expect(buffer.getCursor() == 2)
+
+ buffer.replace(with: Data([0x10, 0x11, 0x12]))
+ #expect(buffer.getCursor() == 0)
+ #expect(buffer.count == 3)
+ #expect(Array(buffer.toData()) == [0x10, 0x11, 0x12])
+
+ let bridged = Array(buffer.copyToData())
+ buffer.setByte(at: 1, to: 0xFE)
+ buffer.setBytes(at: 0, to: [0xAA, 0xBB])
+
+ #expect(buffer.storage == [0xAA, 0xBB, 0x12])
+ #expect(bridged == [0x10, 0x11, 0x12])
+
+ buffer.setCursor(buffer.count)
+ buffer.flip()
+ #expect(buffer.getCursor() == 0)
+
+ buffer.clear()
+ #expect(buffer.count == 0)
+ #expect(buffer.remaining == 0)
+
+ buffer.writeBytes([0x21, 0x22])
+ buffer.reset()
+ #expect(buffer.count == 0)
+ #expect(buffer.getCursor() == 0)
+}
+
+@Test
+func byteBufferVarUIntBoundariesUseExpectedSizes() throws {
+ let values32: [UInt32] = [
+ 0,
+ 1,
+ 1 << 6,
+ 1 << 7,
+ 1 << 13,
+ 1 << 14,
+ 1 << 20,
+ 1 << 21,
+ 1 << 27,
+ 1 << 28,
+ UInt32.max
+ ]
+ let values64: [UInt64] = [
+ 0,
+ 1,
+ 1 << 6,
+ 1 << 7,
+ 1 << 13,
+ 1 << 14,
+ 1 << 20,
+ 1 << 21,
+ 1 << 27,
+ 1 << 28,
+ 1 << 35,
+ 1 << 42,
+ 1 << 49,
+ 1 << 56,
+ UInt64.max
+ ]
+
+ let buffer = ByteBuffer()
+ for value in values32 {
+ let start = buffer.count
+ buffer.writeVarUInt32(value)
+ #expect(buffer.count - start == UnsafeUtil.varUInt32Size(value))
+ }
+ for value in values64 {
+ let start = buffer.count
+ buffer.writeVarUInt64(value)
+ #expect(buffer.count - start == UnsafeUtil.varUInt64Size(value))
+ }
+
+ for value in values32 {
+ #expect(try buffer.readVarUInt32() == value)
+ }
+ for value in values64 {
+ #expect(try buffer.readVarUInt64() == value)
+ }
+ #expect(buffer.remaining == 0)
+}
+
+@Test
+func byteBufferVarIntAndTaggedIntegerBoundariesRoundTrip() throws {
+ let int32Values: [Int32] = [Int32.min, -1_000_000, -1, 0, 1, 127,
Int32.max]
+ let int64Values: [Int64] = [Int64.min, -1_000_000_000_000, -1, 0, 1, 127,
Int64.max]
+ let taggedIntValues: [(Int64, Int)] = [
+ (-1_073_741_824, 4),
+ (-1_073_741_823, 4),
+ (-1, 4),
+ (0, 4),
+ (1_073_741_823, 4),
+ (1_073_741_824, 9),
+ (Int64.min, 9),
+ (Int64.max, 9)
+ ]
+ let taggedUIntValues: [(UInt64, Int)] = [
+ (0, 4),
+ (1, 4),
+ (UInt64(Int32.max), 4),
+ (UInt64(Int32.max) + 1, 9),
+ (UInt64.max, 9)
+ ]
+
+ let buffer = ByteBuffer()
+ for value in int32Values {
+ buffer.writeVarInt32(value)
+ }
+ for value in int64Values {
+ buffer.writeVarInt64(value)
+ }
+ for (value, encodedWidth) in taggedIntValues {
+ let start = buffer.count
+ buffer.writeTaggedInt64(value)
+ #expect(buffer.count - start == encodedWidth)
+ }
+ for (value, encodedWidth) in taggedUIntValues {
+ let start = buffer.count
+ buffer.writeTaggedUInt64(value)
+ #expect(buffer.count - start == encodedWidth)
+ }
+
+ for value in int32Values {
+ #expect(try buffer.readVarInt32() == value)
+ }
+ for value in int64Values {
+ #expect(try buffer.readVarInt64() == value)
+ }
+ for (value, _) in taggedIntValues {
+ #expect(try buffer.readTaggedInt64() == value)
+ }
+ for (value, _) in taggedUIntValues {
+ #expect(try buffer.readTaggedUInt64() == value)
+ }
+ #expect(buffer.remaining == 0)
+}
+
+@Test
+func byteBufferRejectsInvalidVarintsAndUtf8() throws {
+ let varUInt32Overflow = ByteBuffer(bytes: [0x80, 0x80, 0x80, 0x80, 0x80])
+ do {
+ _ = try varUInt32Overflow.readVarUInt32()
+ #expect(Bool(false))
+ } catch {
+ #expect("\(error)".contains("varuint32 overflow"))
+ }
+
+ let truncatedVarUInt64 = ByteBuffer(bytes: [0x80, 0x80, 0x80, 0x80, 0x80,
0x80, 0x80, 0x80])
+ do {
+ _ = try truncatedVarUInt64.readVarUInt64()
+ #expect(Bool(false))
+ } catch {
+ #expect("\(error)".contains("out of bounds"))
+ }
+
+ let varUInt36Overflow = ByteBuffer()
+ varUInt36Overflow.writeVarUInt64(1 << 36)
+ do {
+ _ = try varUInt36Overflow.readVarUInt36Small()
+ #expect(Bool(false))
+ } catch {
+ #expect("\(error)".contains("varuint36small overflow"))
+ }
+
+ let invalidUTF8 = ByteBuffer(bytes: [0xC3, 0x28])
+ do {
+ _ = try invalidUTF8.readUTF8String(count: 2)
+ #expect(Bool(false))
+ } catch {
+ #expect("\(error)".contains("invalid UTF-8"))
+ }
+}
diff --git a/swift/Tests/ForyTests/CollectionSerializerTests.swift
b/swift/Tests/ForyTests/CollectionSerializerTests.swift
new file mode 100644
index 000000000..3345fdf19
--- /dev/null
+++ b/swift/Tests/ForyTests/CollectionSerializerTests.swift
@@ -0,0 +1,313 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements. See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership. The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License. You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+import Foundation
+import Testing
+@testable import Fory
+
+@ForyObject
+private final class RefKeyNode: Hashable {
+ var id: Int32 = 0
+
+ required init() {}
+
+ init(id: Int32) {
+ self.id = id
+ }
+
+ static func == (lhs: RefKeyNode, rhs: RefKeyNode) -> Bool {
+ lhs === rhs
+ }
+
+ func hash(into hasher: inout Hasher) {
+ hasher.combine(ObjectIdentifier(self))
+ }
+}
+
+@ForyObject
+private struct RefKeyHolder {
+ var key: RefKeyNode = .init()
+ var map: [RefKeyNode: Int32] = [:]
+}
+
+@ForyObject
+private struct RefKeyValueHolder {
+ var map: [RefKeyNode: RefKeyNode] = [:]
+}
+
+@ForyObject
+private struct RefKeyChunkHolder {
+ var keys: [RefKeyNode] = []
+ var map: [RefKeyNode: Int32] = [:]
+}
+
+@Test
+func primitiveArrayTypeIDsAndRoundTripsCoverSwiftCollectionSurface() throws {
+ #expect([Bool].staticTypeId == .boolArray)
+ #expect([Int8].staticTypeId == .int8Array)
+ #expect([Int16].staticTypeId == .int16Array)
+ #expect([Int32].staticTypeId == .int32Array)
+ #expect([Int64].staticTypeId == .int64Array)
+ #expect([UInt8].staticTypeId == .uint8Array)
+ #expect([UInt16].staticTypeId == .uint16Array)
+ #expect([UInt32].staticTypeId == .uint32Array)
+ #expect([UInt64].staticTypeId == .uint64Array)
+ #expect([Float16].staticTypeId == .float16Array)
+ #expect([BFloat16].staticTypeId == .bfloat16Array)
+ #expect([Float].staticTypeId == .float32Array)
+ #expect([Double].staticTypeId == .float64Array)
+
+ let fory = Fory()
+
+ let bools = [true, false, true]
+ let int8s: [Int8] = [-1, 0, 127]
+ let int16s: [Int16] = [Int16.min, -1, Int16.max]
+ let int32s: [Int32] = [Int32.min, 0, Int32.max]
+ let int64s: [Int64] = [Int64.min, 0, Int64.max]
+ let uint8s: [UInt8] = [0, 1, 255]
+ let uint16s: [UInt16] = [0, 1, UInt16.max]
+ let uint32s: [UInt32] = [0, 1, UInt32.max]
+ let uint64s: [UInt64] = [0, 1, UInt64.max]
+ let float16s: [Float16] = [Float16(0), Float16(-2.5), Float16(7.25)]
+ let bfloat16s: [BFloat16] = [.init(rawValue: 0x0000), .init(rawValue:
0x3F80), .init(rawValue: 0xBF80)]
+ let floats: [Float] = [-1.5, 0, 3.25]
+ let doubles: [Double] = [-10.5, 0, 9.75]
+
+ let decodedBools: [Bool] = try fory.deserialize(try fory.serialize(bools))
+ let decodedInt8s: [Int8] = try fory.deserialize(try fory.serialize(int8s))
+ let decodedInt16s: [Int16] = try fory.deserialize(try
fory.serialize(int16s))
+ let decodedInt32s: [Int32] = try fory.deserialize(try
fory.serialize(int32s))
+ let decodedInt64s: [Int64] = try fory.deserialize(try
fory.serialize(int64s))
+ let decodedUInt8s: [UInt8] = try fory.deserialize(try
fory.serialize(uint8s))
+ let decodedUInt16s: [UInt16] = try fory.deserialize(try
fory.serialize(uint16s))
+ let decodedUInt32s: [UInt32] = try fory.deserialize(try
fory.serialize(uint32s))
+ let decodedUInt64s: [UInt64] = try fory.deserialize(try
fory.serialize(uint64s))
+ let decodedFloat16s: [Float16] = try fory.deserialize(try
fory.serialize(float16s))
+ let decodedBFloat16s: [BFloat16] = try fory.deserialize(try
fory.serialize(bfloat16s))
+ let decodedFloats: [Float] = try fory.deserialize(try
fory.serialize(floats))
+ let decodedDoubles: [Double] = try fory.deserialize(try
fory.serialize(doubles))
+
+ #expect(decodedBools == bools)
+ #expect(decodedInt8s == int8s)
+ #expect(decodedInt16s == int16s)
+ #expect(decodedInt32s == int32s)
+ #expect(decodedInt64s == int64s)
+ #expect(decodedUInt8s == uint8s)
+ #expect(decodedUInt16s == uint16s)
+ #expect(decodedUInt32s == uint32s)
+ #expect(decodedUInt64s == uint64s)
+ #expect(decodedFloat16s.map(\.bitPattern) == float16s.map(\.bitPattern))
+ #expect(decodedBFloat16s == bfloat16s)
+ #expect(decodedFloats == floats)
+ #expect(decodedDoubles == doubles)
+}
+
+@Test
+func floatingPointArraysPreserveBits() throws {
+ let fory = Fory()
+
+ let float16s: [Float16] = [
+ .init(bitPattern: 0x0000),
+ .init(bitPattern: 0x8000),
+ .init(bitPattern: 0x7C00),
+ .init(bitPattern: 0xFC00),
+ .init(bitPattern: 0x0001),
+ .init(bitPattern: 0x7E21)
+ ]
+ let bfloat16s: [BFloat16] = [
+ .init(rawValue: 0x0000),
+ .init(rawValue: 0x8000),
+ .init(rawValue: 0x7F80),
+ .init(rawValue: 0xFF80),
+ .init(rawValue: 0x0001),
+ .init(rawValue: 0x7FC1)
+ ]
+ let floats: [Float] = [
+ 0.0,
+ -0.0,
+ .infinity,
+ -.infinity,
+ .leastNonzeroMagnitude,
+ .greatestFiniteMagnitude,
+ Float(bitPattern: 0x7FC0_1234)
+ ]
+ let doubles: [Double] = [
+ 0.0,
+ -0.0,
+ .infinity,
+ -.infinity,
+ .leastNonzeroMagnitude,
+ .greatestFiniteMagnitude,
+ Double(bitPattern: 0x7FF8_0000_0000_1234)
+ ]
+
+ let decodedFloat16s: [Float16] = try fory.deserialize(try
fory.serialize(float16s))
+ let decodedBFloat16s: [BFloat16] = try fory.deserialize(try
fory.serialize(bfloat16s))
+ let decodedFloats: [Float] = try fory.deserialize(try
fory.serialize(floats))
+ let decodedDoubles: [Double] = try fory.deserialize(try
fory.serialize(doubles))
+
+ #expect(decodedFloat16s.map(\.bitPattern) == float16s.map(\.bitPattern))
+ #expect(decodedBFloat16s.map(\.rawValue) == bfloat16s.map(\.rawValue))
+ #expect(decodedFloats.map(\.bitPattern) == floats.map(\.bitPattern))
+ #expect(decodedDoubles.map(\.bitPattern) == doubles.map(\.bitPattern))
+}
+
+@Test
+func uint8ArrayAndDataInteroperateAcrossWireTypes() throws {
+ let payload: [UInt8] = [0x00, 0x01, 0x7F, 0xFF]
+
+ let schemaConsistent = Fory(config: .init(xlang: true, trackRef: false,
compatible: false))
+ let schemaBytes = try schemaConsistent.serialize(payload)
+ #expect(Array(schemaBytes)[2] == UInt8(TypeId.uint8Array.rawValue))
+
+ let compatible = Fory(config: .init(xlang: true, trackRef: false,
compatible: true))
+ let compatibleBytes = try compatible.serialize(payload)
+ #expect(Array(compatibleBytes)[2] == UInt8(TypeId.binary.rawValue))
+
+ let decodedBinary: Data = try compatible.deserialize(compatibleBytes)
+ #expect(Array(decodedBinary) == payload)
+
+ let binary = Data(payload)
+ let binaryBytes = try compatible.serialize(binary)
+ let decodedArray: [UInt8] = try compatible.deserialize(binaryBytes)
+ #expect(decodedArray == payload)
+}
+
+@Test
+func nestedCollectionsAndNullabilityRoundTrip() throws {
+ let fory = Fory(config: .init(xlang: true, trackRef: true, compatible:
true))
+
+ let nested: [[String?]] = [
+ ["alpha", nil],
+ [],
+ ["beta", "gamma"]
+ ]
+ let decodedNested: [[String?]] = try fory.deserialize(try
fory.serialize(nested))
+ #expect(decodedNested == nested)
+
+ let set: Set<UInt16> = [0, 42, UInt16.max]
+ let decodedSet: Set<UInt16> = try fory.deserialize(try fory.serialize(set))
+ #expect(decodedSet == set)
+
+ let map: [Int32?: [String?]?] = [
+ 1: ["x", nil, "z"],
+ nil: nil,
+ 3: []
+ ]
+ let decodedMap: [Int32?: [String?]?] = try fory.deserialize(try
fory.serialize(map))
+ #expect(decodedMap == map)
+}
+
+@Test
+func mapRefKeysTrackIdentity() throws {
+ let fory = Fory(config: .init(xlang: true, trackRef: true, compatible:
true))
+ fory.register(RefKeyNode.self, id: 9501)
+ fory.register(RefKeyHolder.self, id: 9502)
+
+ let sharedKey = RefKeyNode(id: 7)
+ let value = RefKeyHolder(key: sharedKey, map: [sharedKey: 99])
+
+ let decoded: RefKeyHolder = try fory.deserialize(try fory.serialize(value))
+ let pair = decoded.map.first
+
+ #expect(pair != nil)
+ if let pair {
+ #expect(decoded.key === pair.key)
+ #expect(pair.value == 99)
+ }
+}
+
+@Test
+func mapRefKeyAndValueShareIdentity() throws {
+ let fory = Fory(config: .init(xlang: true, trackRef: true, compatible:
true))
+ fory.register(RefKeyNode.self, id: 9501)
+ fory.register(RefKeyValueHolder.self, id: 9503)
+
+ let shared = RefKeyNode(id: 11)
+ let value = RefKeyValueHolder(map: [shared: shared])
+
+ let decoded: RefKeyValueHolder = try fory.deserialize(try
fory.serialize(value))
+ let pair = decoded.map.first
+
+ #expect(pair != nil)
+ if let pair {
+ #expect(pair.key === pair.value)
+ }
+}
+
+@Test
+func mapRefKeysChunkAcross255Entries() throws {
+ let fory = Fory(config: .init(xlang: true, trackRef: true, compatible:
true))
+ fory.register(RefKeyNode.self, id: 9501)
+ fory.register(RefKeyChunkHolder.self, id: 9504)
+
+ var keys: [RefKeyNode] = []
+ var map: [RefKeyNode: Int32] = [:]
+ for index in 0..<300 {
+ let key = RefKeyNode(id: Int32(index))
+ keys.append(key)
+ map[key] = Int32(index * 2)
+ }
+ let value = RefKeyChunkHolder(keys: keys, map: map)
+
+ let decoded: RefKeyChunkHolder = try fory.deserialize(try
fory.serialize(value))
+ #expect(decoded.keys.count == 300)
+ #expect(decoded.map.count == 300)
+
+ var decodedMapKeysByID: [Int32: RefKeyNode] = [:]
+ for key in decoded.map.keys {
+ decodedMapKeysByID[key.id] = key
+ }
+
+ for key in decoded.keys {
+ #expect(decodedMapKeysByID[key.id] === key)
+ #expect(decoded.map[decodedMapKeysByID[key.id]!] == key.id * 2)
+ }
+}
+
+@Test
+func collectionSerializersRejectMalformedPrimitivePayloads() throws {
+ let int16Buffer = ByteBuffer()
+ int16Buffer.writeVarUInt32(3)
+ int16Buffer.writeBytes([0x01, 0x02, 0x03])
+ let int16Context = ReadContext(
+ buffer: int16Buffer,
+ typeResolver: TypeResolver(trackRef: false),
+ trackRef: false
+ )
+ do {
+ let _: [Int16] = try [Int16].foryReadData(int16Context)
+ #expect(Bool(false))
+ } catch {
+ #expect("\(error)".contains("payload size mismatch"))
+ }
+
+ let float64Buffer = ByteBuffer()
+ float64Buffer.writeVarUInt32(4)
+ float64Buffer.writeBytes([0x01, 0x02, 0x03, 0x04])
+ let float64Context = ReadContext(
+ buffer: float64Buffer,
+ typeResolver: TypeResolver(trackRef: false),
+ trackRef: false
+ )
+ do {
+ let _: [Double] = try [Double].foryReadData(float64Context)
+ #expect(Bool(false))
+ } catch {
+ #expect("\(error)".contains("payload size mismatch"))
+ }
+}
diff --git a/swift/Tests/ForyTests/CompatibilityTests.swift
b/swift/Tests/ForyTests/CompatibilityTests.swift
new file mode 100644
index 000000000..49bf7b996
--- /dev/null
+++ b/swift/Tests/ForyTests/CompatibilityTests.swift
@@ -0,0 +1,294 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements. See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership. The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License. You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+import Foundation
+import Testing
+@testable import Fory
+
+@ForyObject
+private struct CompatibleProfileV1: Equatable {
+ var id: Int32 = 0
+ var name: String = ""
+}
+
+@ForyObject
+private struct CompatibleProfileV2: Equatable {
+ var id: Int32 = 0
+ var name: String = ""
+ var nickname: String = "guest"
+ var scores: [Int32] = []
+}
+
+@ForyObject
+private struct CompatibleNestedProfileV1: Equatable {
+ var id: Int32 = 0
+ var name: String = ""
+}
+
+@ForyObject
+private struct CompatibleNestedProfileV2: Equatable {
+ var id: Int32 = 0
+ var name: String = ""
+ var alias: String = ""
+ var scores: [Int32] = []
+}
+
+@ForyObject
+private struct CompatibleNestedArrayV1: Equatable {
+ var items: [CompatibleNestedProfileV1] = []
+}
+
+@ForyObject
+private struct CompatibleNestedArrayV2: Equatable {
+ var items: [CompatibleNestedProfileV2] = []
+}
+
+@ForyObject
+private struct CompatibleNestedMapV1: Equatable {
+ var items: [Int32: CompatibleNestedProfileV1] = [:]
+}
+
+@ForyObject
+private struct CompatibleNestedMapV2: Equatable {
+ var items: [Int32: CompatibleNestedProfileV2] = [:]
+}
+
+@ForyObject
+private struct SchemaVersionV1: Equatable {
+ var id: Int32 = 0
+ var name: String = ""
+}
+
+@ForyObject
+private struct SchemaVersionV2: Equatable {
+ var id: Int32 = 0
+ var alias: String = ""
+ var count: Int32 = 0
+}
+
+@ForyObject
+private final class CompatibleGraphNode {
+ var value: Int32 = 0
+ var next: CompatibleGraphNode?
+
+ required init() {}
+
+ init(value: Int32, next: CompatibleGraphNode? = nil) {
+ self.value = value
+ self.next = next
+ }
+}
+
+@ForyObject
+private final class CompatibleGraphContainer {
+ var first: CompatibleGraphNode?
+ var second: CompatibleGraphNode?
+ var items: [CompatibleGraphNode] = []
+ var byName: [String: CompatibleGraphNode] = [:]
+
+ required init() {}
+
+ init(
+ first: CompatibleGraphNode?,
+ second: CompatibleGraphNode?,
+ items: [CompatibleGraphNode],
+ byName: [String: CompatibleGraphNode]
+ ) {
+ self.first = first
+ self.second = second
+ self.items = items
+ self.byName = byName
+ }
+}
+
+@Test
+func compatibleModeSupportsAddedAndRemovedFields() throws {
+ let writerV1 = Fory(config: .init(xlang: true, trackRef: false,
compatible: true))
+ writerV1.register(CompatibleProfileV1.self, id: 9901)
+
+ let readerV2 = Fory(config: .init(xlang: true, trackRef: false,
compatible: true))
+ readerV2.register(CompatibleProfileV2.self, id: 9901)
+
+ let sourceV1 = CompatibleProfileV1(id: 7, name: "swift")
+ let bytesFromV1 = try writerV1.serialize(sourceV1)
+ let decodedAsV2: CompatibleProfileV2 = try
readerV2.deserialize(bytesFromV1)
+ #expect(decodedAsV2.id == sourceV1.id)
+ #expect(decodedAsV2.name == sourceV1.name)
+ #expect(decodedAsV2.nickname == "")
+ #expect(decodedAsV2.scores.isEmpty)
+
+ let writerV2 = Fory(config: .init(xlang: true, trackRef: false,
compatible: true))
+ writerV2.register(CompatibleProfileV2.self, id: 9901)
+
+ let readerV1 = Fory(config: .init(xlang: true, trackRef: false,
compatible: true))
+ readerV1.register(CompatibleProfileV1.self, id: 9901)
+
+ let sourceV2 = CompatibleProfileV2(id: 9, name: "fory", nickname: "macro",
scores: [1, 2, 3])
+ let bytesFromV2 = try writerV2.serialize(sourceV2)
+ let decodedAsV1: CompatibleProfileV1 = try
readerV1.deserialize(bytesFromV2)
+ #expect(decodedAsV1 == CompatibleProfileV1(id: sourceV2.id, name:
sourceV2.name))
+}
+
+@Test
+func schemaConsistentModeRejectsVersionHashMismatch() throws {
+ let writer = Fory(config: .init(xlang: true, trackRef: false, compatible:
false, checkClassVersion: true))
+ writer.register(SchemaVersionV1.self, id: 9902)
+
+ let reader = Fory(config: .init(xlang: true, trackRef: false, compatible:
false, checkClassVersion: true))
+ reader.register(SchemaVersionV2.self, id: 9902)
+
+ let bytes = try writer.serialize(SchemaVersionV1(id: 1, name: "shape"))
+ do {
+ let _: SchemaVersionV2 = try reader.deserialize(bytes)
+ #expect(Bool(false))
+ } catch {
+ #expect("\(error)".contains("class version hash mismatch"))
+ }
+}
+
+@Test
+func compatibleModePreservesSharedAndCircularReferencesForMacroObjects()
throws {
+ let fory = Fory(config: .init(xlang: true, trackRef: true, compatible:
true))
+ fory.register(CompatibleGraphNode.self, id: 9903)
+ fory.register(CompatibleGraphContainer.self, id: 9904)
+
+ let shared = CompatibleGraphNode(value: 11)
+ shared.next = shared
+ let value = CompatibleGraphContainer(
+ first: shared,
+ second: shared,
+ items: [shared, shared],
+ byName: [
+ "left": shared,
+ "right": shared
+ ]
+ )
+
+ let decoded: CompatibleGraphContainer = try fory.deserialize(try
fory.serialize(value))
+
+ #expect(decoded.first != nil)
+ #expect(decoded.first === decoded.second)
+ #expect(decoded.first === decoded.items[0])
+ #expect(decoded.items[0] === decoded.items[1])
+ #expect(decoded.byName["left"] === decoded.byName["right"])
+ #expect(decoded.byName["left"] === decoded.first)
+ #expect(decoded.first?.next === decoded.first)
+}
+
+@Test
+func compatibleNestedArrayEvolves() throws {
+ let writerV1 = Fory(config: .init(xlang: true, trackRef: false,
compatible: true))
+ writerV1.register(CompatibleNestedProfileV1.self, id: 9910)
+ writerV1.register(CompatibleNestedArrayV1.self, id: 9911)
+
+ let readerV2 = Fory(config: .init(xlang: true, trackRef: false,
compatible: true))
+ readerV2.register(CompatibleNestedProfileV2.self, id: 9910)
+ readerV2.register(CompatibleNestedArrayV2.self, id: 9911)
+
+ let sourceV1 = CompatibleNestedArrayV1(
+ items: [
+ CompatibleNestedProfileV1(id: 1, name: "alpha"),
+ CompatibleNestedProfileV1(id: 2, name: "beta")
+ ]
+ )
+ let decodedAsV2: CompatibleNestedArrayV2 = try readerV2.deserialize(try
writerV1.serialize(sourceV1))
+ #expect(decodedAsV2.items.map(\.id) == [1, 2])
+ #expect(decodedAsV2.items.map(\.name) == ["alpha", "beta"])
+ #expect(decodedAsV2.items.allSatisfy { $0.alias.isEmpty })
+ #expect(decodedAsV2.items.allSatisfy { $0.scores.isEmpty })
+
+ let writerV2 = Fory(config: .init(xlang: true, trackRef: false,
compatible: true))
+ writerV2.register(CompatibleNestedProfileV2.self, id: 9910)
+ writerV2.register(CompatibleNestedArrayV2.self, id: 9911)
+
+ let readerV1 = Fory(config: .init(xlang: true, trackRef: false,
compatible: true))
+ readerV1.register(CompatibleNestedProfileV1.self, id: 9910)
+ readerV1.register(CompatibleNestedArrayV1.self, id: 9911)
+
+ let sourceV2 = CompatibleNestedArrayV2(
+ items: [
+ CompatibleNestedProfileV2(id: 3, name: "gamma", alias: "g",
scores: [3, 4]),
+ CompatibleNestedProfileV2(id: 4, name: "delta", alias: "d",
scores: [])
+ ]
+ )
+ let decodedAsV1: CompatibleNestedArrayV1 = try readerV1.deserialize(try
writerV2.serialize(sourceV2))
+ #expect(decodedAsV1.items == [
+ CompatibleNestedProfileV1(id: 3, name: "gamma"),
+ CompatibleNestedProfileV1(id: 4, name: "delta")
+ ])
+}
+
+@Test
+func compatibleNestedMapEvolves() throws {
+ let writerV1 = Fory(config: .init(xlang: true, trackRef: false,
compatible: true))
+ writerV1.register(CompatibleNestedProfileV1.self, id: 9910)
+ writerV1.register(CompatibleNestedMapV1.self, id: 9912)
+
+ let readerV2 = Fory(config: .init(xlang: true, trackRef: false,
compatible: true))
+ readerV2.register(CompatibleNestedProfileV2.self, id: 9910)
+ readerV2.register(CompatibleNestedMapV2.self, id: 9912)
+
+ let sourceV1 = CompatibleNestedMapV1(
+ items: [
+ 1: CompatibleNestedProfileV1(id: 10, name: "first"),
+ 2: CompatibleNestedProfileV1(id: 20, name: "second")
+ ]
+ )
+ let decodedAsV2: CompatibleNestedMapV2 = try readerV2.deserialize(try
writerV1.serialize(sourceV1))
+ #expect(decodedAsV2.items[1]?.id == 10)
+ #expect(decodedAsV2.items[1]?.name == "first")
+ #expect(decodedAsV2.items[1]?.alias == "")
+ #expect(decodedAsV2.items[1]?.scores.isEmpty == true)
+ #expect(decodedAsV2.items[2]?.id == 20)
+ #expect(decodedAsV2.items[2]?.name == "second")
+ #expect(decodedAsV2.items[2]?.alias == "")
+ #expect(decodedAsV2.items[2]?.scores.isEmpty == true)
+}
+
+@Test
+func compatibleNestedReadsReuseTypeMeta() throws {
+ let writerV1 = Fory(config: .init(xlang: true, trackRef: false,
compatible: true))
+ writerV1.register(CompatibleNestedProfileV1.self, id: 9910)
+ writerV1.register(CompatibleNestedArrayV1.self, id: 9911)
+
+ let readerV2 = Fory(config: .init(xlang: true, trackRef: false,
compatible: true))
+ readerV2.register(CompatibleNestedProfileV2.self, id: 9910)
+ readerV2.register(CompatibleNestedArrayV2.self, id: 9911)
+
+ let first = CompatibleNestedArrayV1(
+ items: [
+ CompatibleNestedProfileV1(id: 1, name: "alpha"),
+ CompatibleNestedProfileV1(id: 2, name: "beta")
+ ]
+ )
+ let second = CompatibleNestedArrayV1(
+ items: [
+ CompatibleNestedProfileV1(id: 3, name: "gamma"),
+ CompatibleNestedProfileV1(id: 4, name: "delta"),
+ CompatibleNestedProfileV1(id: 5, name: "epsilon")
+ ]
+ )
+
+ let decodedFirst: CompatibleNestedArrayV2 = try readerV2.deserialize(try
writerV1.serialize(first))
+ let decodedSecond: CompatibleNestedArrayV2 = try readerV2.deserialize(try
writerV1.serialize(second))
+
+ #expect(decodedFirst.items.map(\.id) == [1, 2])
+ #expect(decodedSecond.items.map(\.id) == [3, 4, 5])
+ #expect(decodedSecond.items.map(\.name) == ["gamma", "delta", "epsilon"])
+ #expect(decodedSecond.items.allSatisfy { $0.alias.isEmpty })
+ #expect(decodedSecond.items.allSatisfy { $0.scores.isEmpty })
+}
diff --git a/swift/Tests/ForyTests/ForySwiftTests.swift
b/swift/Tests/ForyTests/ForySwiftTests.swift
index 146138d86..60cf23164 100644
--- a/swift/Tests/ForyTests/ForySwiftTests.swift
+++ b/swift/Tests/ForyTests/ForySwiftTests.swift
@@ -251,6 +251,66 @@ func extendedWireTypesRoundTrip() throws {
#expect(float16ArrayDecoded.map(\.bitPattern) ==
float16Array.map(\.bitPattern))
}
+@Test
+func floatingSpecialsRoundTrip() throws {
+ let fory = Fory()
+
+ let floatValues: [Float] = [
+ 0.0,
+ -0.0,
+ .infinity,
+ -.infinity,
+ .leastNonzeroMagnitude,
+ .greatestFiniteMagnitude,
+ Float(bitPattern: 0x7FC0_1234)
+ ]
+ for value in floatValues {
+ let decoded: Float = try fory.deserialize(try fory.serialize(value))
+ #expect(decoded.bitPattern == value.bitPattern)
+ }
+
+ let doubleValues: [Double] = [
+ 0.0,
+ -0.0,
+ .infinity,
+ -.infinity,
+ .leastNonzeroMagnitude,
+ .greatestFiniteMagnitude,
+ Double(bitPattern: 0x7FF8_0000_0000_1234)
+ ]
+ for value in doubleValues {
+ let decoded: Double = try fory.deserialize(try fory.serialize(value))
+ #expect(decoded.bitPattern == value.bitPattern)
+ }
+
+ let float16Values: [Float16] = [
+ .init(bitPattern: 0x0000),
+ .init(bitPattern: 0x8000),
+ .init(bitPattern: 0x7C00),
+ .init(bitPattern: 0xFC00),
+ .init(bitPattern: 0x0001),
+ .init(bitPattern: 0x7BFF),
+ .init(bitPattern: 0x7E11)
+ ]
+ for value in float16Values {
+ let decoded: Float16 = try fory.deserialize(try fory.serialize(value))
+ #expect(decoded.bitPattern == value.bitPattern)
+ }
+
+ let bfloat16Values: [BFloat16] = [
+ .init(rawValue: 0x0000),
+ .init(rawValue: 0x8000),
+ .init(rawValue: 0x7F80),
+ .init(rawValue: 0xFF80),
+ .init(rawValue: 0x0001),
+ .init(rawValue: 0x7FC1)
+ ]
+ for value in bfloat16Values {
+ let decoded: BFloat16 = try fory.deserialize(try fory.serialize(value))
+ #expect(decoded.rawValue == value.rawValue)
+ }
+}
+
@Test
func namedInitializerBuildsConfig() {
let defaultConfig = Fory()
@@ -518,6 +578,59 @@ func registrationIsRejectedAfterFirstTopLevelUse() throws {
}
}
+@Test
+func serializeToAppendsRoots() throws {
+ let fory = Fory()
+ let first = Int32(7)
+ let second = "swift-buffer"
+ let third: String? = nil
+
+ let firstData = try fory.serialize(first)
+ let secondData = try fory.serialize(second)
+ let thirdData = try fory.serialize(third)
+
+ var stream = Data()
+ try fory.serialize(first, to: &stream)
+ try fory.serialize(second, to: &stream)
+ try fory.serialize(third, to: &stream)
+
+ var expected = Data()
+ expected.append(firstData)
+ expected.append(secondData)
+ expected.append(thirdData)
+ #expect(stream == expected)
+
+ let buffer = ByteBuffer(data: stream)
+ let decodedFirst: Int32 = try fory.deserialize(from: buffer)
+ #expect(decodedFirst == first)
+ #expect(buffer.getCursor() == firstData.count)
+
+ let decodedSecond: String = try fory.deserialize(from: buffer)
+ #expect(decodedSecond == second)
+ #expect(buffer.getCursor() == firstData.count + secondData.count)
+
+ let decodedThird: String? = try fory.deserialize(from: buffer)
+ #expect(decodedThird == nil)
+ #expect(buffer.remaining == 0)
+}
+
+@Test
+func rootBufferHonorsCursor() throws {
+ let fory = Fory()
+ let prefix: [UInt8] = [0xAA, 0xBB, 0xCC]
+ let payload = try fory.serialize("offset")
+
+ let buffer = ByteBuffer()
+ buffer.writeBytes(prefix)
+ buffer.writeBytes(Array(payload))
+ buffer.setCursor(prefix.count)
+
+ let decoded: String = try fory.deserialize(from: buffer)
+ #expect(decoded == "offset")
+ #expect(buffer.getCursor() == buffer.count)
+ #expect(Array(buffer.storage.prefix(prefix.count)) == prefix)
+}
+
@Test
func topLevelAnyObjectRoundTrip() throws {
let fory = Fory(config: .init(xlang: true, trackRef: true))
@@ -596,6 +709,31 @@ func
macroDynamicAnyObjectAndAnySerializerFieldsRoundTrip() throws {
#expect(serializerDecoded.map["address"] as? Address == Address(street:
"Mapped", zip: 10003))
}
+@Test
+func dynamicAnySerializerTracksRefs() throws {
+ let fory = Fory(config: .init(xlang: true, trackRef: true))
+ fory.register(Node.self, id: 226)
+ fory.register(AnySerializerHolder.self, id: 227)
+
+ let shared = Node(value: 88)
+ shared.next = shared
+ let value = AnySerializerHolder(
+ value: shared,
+ items: [shared],
+ map: ["shared": shared]
+ )
+
+ let decoded: AnySerializerHolder = try fory.deserialize(try
fory.serialize(value))
+ let root = decoded.value as? Node
+ let item = decoded.items.first as? Node
+ let mapped = decoded.map["shared"] as? Node
+
+ #expect(root != nil)
+ #expect(root === item)
+ #expect(item === mapped)
+ #expect(root?.next === root)
+}
+
@Test
func macroAnyFieldsRoundTrip() throws {
let fory = Fory()
diff --git a/swift/Tests/ForyTests/StringSerializerTests.swift
b/swift/Tests/ForyTests/StringSerializerTests.swift
new file mode 100644
index 000000000..a0e0cef0d
--- /dev/null
+++ b/swift/Tests/ForyTests/StringSerializerTests.swift
@@ -0,0 +1,147 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements. See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership. The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License. You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+import Foundation
+import Testing
+@testable import Fory
+
+private enum ManualStringEncoding: UInt64 {
+ case latin1 = 0
+ case utf16 = 1
+ case utf8 = 2
+}
+
+private func utf16LittleEndianBytes(_ value: String) -> [UInt8] {
+ value.utf16.flatMap { unit in
+ [
+ UInt8(truncatingIfNeeded: unit),
+ UInt8(truncatingIfNeeded: unit >> 8)
+ ]
+ }
+}
+
+private func makeStringReadContext(payload: [UInt8], encoding:
ManualStringEncoding) -> ReadContext {
+ let buffer = ByteBuffer()
+ buffer.writeVarUInt36Small((UInt64(payload.count) << 2) |
encoding.rawValue)
+ buffer.writeBytes(payload)
+ return ReadContext(
+ buffer: buffer,
+ typeResolver: TypeResolver(trackRef: false),
+ trackRef: false
+ )
+}
+
+private func stringPayloadBytes(for value: String) throws -> [UInt8] {
+ let context = WriteContext(
+ buffer: ByteBuffer(),
+ typeResolver: TypeResolver(trackRef: false),
+ trackRef: false
+ )
+ try value.foryWriteData(context, hasGenerics: false)
+ return Array(context.buffer.storage.prefix(context.buffer.count))
+}
+
+@Test
+func stringSerializerRoundTripsUnicodeAndLengthBoundaries() throws {
+ let fory = Fory(config: .init(xlang: true, trackRef: false, compatible:
true))
+ let values = [
+ "",
+ "ascii",
+ String(repeating: "a", count: 200),
+ "café",
+ "你好,Swift",
+ "emoji 👩🏽💻🚀",
+ "e\u{301}",
+ "null\u{0000}byte",
+ "𐍈 Gothic"
+ ]
+
+ for value in values {
+ let data = try fory.serialize(value)
+ let decoded: String = try fory.deserialize(data)
+ #expect(decoded == value)
+
+ let payload = try stringPayloadBytes(for: value)
+ let headerReader = ByteBuffer(bytes: payload)
+ let header = try headerReader.readVarUInt36Small()
+ #expect((header & 0x03) == ManualStringEncoding.utf8.rawValue)
+ #expect(Int(header >> 2) == value.utf8.count)
+
+ let context = ReadContext(
+ buffer: ByteBuffer(bytes: payload),
+ typeResolver: TypeResolver(trackRef: false),
+ trackRef: false
+ )
+ #expect(try String.foryReadData(context) == value)
+ }
+}
+
+@Test
+func stringSerializerReadsUtf8Latin1AndUtf16Payloads() throws {
+ let latin1Context = makeStringReadContext(
+ payload: [0x63, 0x61, 0x66, 0xE9],
+ encoding: .latin1
+ )
+ #expect(try String.foryReadData(latin1Context) == "café")
+
+ let utf16Value = "你好😀"
+ let utf16Context = makeStringReadContext(
+ payload: utf16LittleEndianBytes(utf16Value),
+ encoding: .utf16
+ )
+ #expect(try String.foryReadData(utf16Context) == utf16Value)
+
+ let utf8Value = "emoji 👩🏽💻"
+ let utf8Context = makeStringReadContext(
+ payload: Array(utf8Value.utf8),
+ encoding: .utf8
+ )
+ #expect(try String.foryReadData(utf8Context) == utf8Value)
+}
+
+@Test
+func stringSerializerRejectsInvalidPayloads() throws {
+ let oddUTF16 = makeStringReadContext(payload: [0x41], encoding: .utf16)
+ do {
+ _ = try String.foryReadData(oddUTF16)
+ #expect(Bool(false))
+ } catch {
+ #expect("\(error)".contains("utf16 byte length is not even"))
+ }
+
+ let invalidUTF8 = makeStringReadContext(payload: [0xC3, 0x28], encoding:
.utf8)
+ do {
+ _ = try String.foryReadData(invalidUTF8)
+ #expect(Bool(false))
+ } catch {
+ #expect("\(error)".contains("invalid UTF-8"))
+ }
+
+ let unsupportedEncodingBuffer = ByteBuffer()
+ unsupportedEncodingBuffer.writeVarUInt36Small(3)
+ let unsupportedEncoding = ReadContext(
+ buffer: unsupportedEncodingBuffer,
+ typeResolver: TypeResolver(trackRef: false),
+ trackRef: false
+ )
+ do {
+ _ = try String.foryReadData(unsupportedEncoding)
+ #expect(Bool(false))
+ } catch {
+ #expect("\(error)".contains("unsupported string encoding"))
+ }
+}
diff --git a/swift/Tests/ForyTests/UnsignedTests.swift
b/swift/Tests/ForyTests/UnsignedTests.swift
new file mode 100644
index 000000000..f90020579
--- /dev/null
+++ b/swift/Tests/ForyTests/UnsignedTests.swift
@@ -0,0 +1,188 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements. See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership. The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License. You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+import Foundation
+import Testing
+@testable import Fory
+
+@ForyObject
+private struct UnsignedFieldBundle: Equatable {
+ var u8: UInt8 = 0
+ var u16: UInt16 = 0
+ var u32Var: UInt32 = 0
+ var u32Fixed: ForyUInt32Fixed = .init()
+ var u64Var: UInt64 = 0
+ var u64Fixed: ForyUInt64Fixed = .init()
+ var u64Tagged: ForyUInt64Tagged = .init()
+
+ var u8Nullable: UInt8?
+ var u16Nullable: UInt16?
+ var u32VarNullable: UInt32?
+ var u32FixedNullable: ForyUInt32Fixed?
+ var u64VarNullable: UInt64?
+ var u64FixedNullable: ForyUInt64Fixed?
+ var u64TaggedNullable: ForyUInt64Tagged?
+}
+
+@Test
+func unsignedPrimitiveRoundTripsCoverZeroMidpointAndMax() throws {
+ let fory = Fory()
+
+ let uint8Values: [UInt8] = [0, 127, 128, UInt8.max]
+ let uint16Values: [UInt16] = [0, 32_767, 32_768, UInt16.max]
+ let uint32Values: [UInt32] = [0, UInt32(Int32.max), UInt32(Int32.max) + 1,
UInt32.max]
+ let uint64Values: [UInt64] = [0, UInt64(Int64.max), UInt64(Int64.max) + 1,
UInt64.max]
+
+ for value in uint8Values {
+ let decoded: UInt8 = try fory.deserialize(try fory.serialize(value))
+ #expect(decoded == value)
+ }
+ for value in uint16Values {
+ let decoded: UInt16 = try fory.deserialize(try fory.serialize(value))
+ #expect(decoded == value)
+ }
+ for value in uint32Values {
+ let decoded: UInt32 = try fory.deserialize(try fory.serialize(value))
+ #expect(decoded == value)
+ }
+ for value in uint64Values {
+ let decoded: UInt64 = try fory.deserialize(try fory.serialize(value))
+ #expect(decoded == value)
+ }
+}
+
+@Test
+func unsignedEncodingWrappersPreserveExpectedWireWidths() throws {
+ let fixed32 = ForyUInt32Fixed(rawValue: UInt32.max)
+ let fixed32Context = WriteContext(
+ buffer: ByteBuffer(),
+ typeResolver: TypeResolver(trackRef: false),
+ trackRef: false
+ )
+ try fixed32.foryWriteData(fixed32Context, hasGenerics: false)
+ #expect(fixed32Context.buffer.count == 4)
+ let fixed32Decoded = try ForyUInt32Fixed.foryReadData(
+ ReadContext(buffer: fixed32Context.buffer, typeResolver:
TypeResolver(trackRef: false), trackRef: false)
+ )
+ #expect(fixed32Decoded == fixed32)
+
+ let fixed64 = ForyUInt64Fixed(rawValue: UInt64.max)
+ let fixed64Context = WriteContext(
+ buffer: ByteBuffer(),
+ typeResolver: TypeResolver(trackRef: false),
+ trackRef: false
+ )
+ try fixed64.foryWriteData(fixed64Context, hasGenerics: false)
+ #expect(fixed64Context.buffer.count == 8)
+ let fixed64Decoded = try ForyUInt64Fixed.foryReadData(
+ ReadContext(buffer: fixed64Context.buffer, typeResolver:
TypeResolver(trackRef: false), trackRef: false)
+ )
+ #expect(fixed64Decoded == fixed64)
+
+ let compactTagged = ForyUInt64Tagged(rawValue: UInt64(Int32.max))
+ let compactContext = WriteContext(
+ buffer: ByteBuffer(),
+ typeResolver: TypeResolver(trackRef: false),
+ trackRef: false
+ )
+ try compactTagged.foryWriteData(compactContext, hasGenerics: false)
+ #expect(compactContext.buffer.count == 4)
+ let compactDecoded = try ForyUInt64Tagged.foryReadData(
+ ReadContext(buffer: compactContext.buffer, typeResolver:
TypeResolver(trackRef: false), trackRef: false)
+ )
+ #expect(compactDecoded == compactTagged)
+
+ let wideTagged = ForyUInt64Tagged(rawValue: UInt64(Int32.max) + 1)
+ let wideContext = WriteContext(
+ buffer: ByteBuffer(),
+ typeResolver: TypeResolver(trackRef: false),
+ trackRef: false
+ )
+ try wideTagged.foryWriteData(wideContext, hasGenerics: false)
+ #expect(wideContext.buffer.count == 9)
+ let wideDecoded = try ForyUInt64Tagged.foryReadData(
+ ReadContext(buffer: wideContext.buffer, typeResolver:
TypeResolver(trackRef: false), trackRef: false)
+ )
+ #expect(wideDecoded == wideTagged)
+}
+
+@Test
+func unsignedMacroFieldsRoundTripAcrossSchemaModes() throws {
+ let cases = [
+ UnsignedFieldBundle(
+ u8: 0,
+ u16: 0,
+ u32Var: 0,
+ u32Fixed: .init(rawValue: 0),
+ u64Var: 0,
+ u64Fixed: .init(rawValue: 0),
+ u64Tagged: .init(rawValue: 0),
+ u8Nullable: nil,
+ u16Nullable: nil,
+ u32VarNullable: nil,
+ u32FixedNullable: nil,
+ u64VarNullable: nil,
+ u64FixedNullable: nil,
+ u64TaggedNullable: nil
+ ),
+ UnsignedFieldBundle(
+ u8: 128,
+ u16: 32_768,
+ u32Var: UInt32(Int32.max) + 1,
+ u32Fixed: .init(rawValue: UInt32(Int32.max) + 1),
+ u64Var: UInt64(Int64.max) + 1,
+ u64Fixed: .init(rawValue: UInt64(Int64.max) + 1),
+ u64Tagged: .init(rawValue: UInt64(Int32.max) + 1),
+ u8Nullable: 128,
+ u16Nullable: 32_768,
+ u32VarNullable: UInt32(Int32.max) + 1,
+ u32FixedNullable: .init(rawValue: UInt32(Int32.max) + 1),
+ u64VarNullable: UInt64(Int64.max) + 1,
+ u64FixedNullable: .init(rawValue: UInt64(Int64.max) + 1),
+ u64TaggedNullable: .init(rawValue: UInt64(Int32.max) + 1)
+ ),
+ UnsignedFieldBundle(
+ u8: UInt8.max,
+ u16: UInt16.max,
+ u32Var: UInt32.max,
+ u32Fixed: .init(rawValue: UInt32.max),
+ u64Var: UInt64.max,
+ u64Fixed: .init(rawValue: UInt64.max),
+ u64Tagged: .init(rawValue: UInt64.max),
+ u8Nullable: UInt8.max,
+ u16Nullable: UInt16.max,
+ u32VarNullable: UInt32.max,
+ u32FixedNullable: .init(rawValue: UInt32.max),
+ u64VarNullable: UInt64.max,
+ u64FixedNullable: .init(rawValue: UInt64.max),
+ u64TaggedNullable: .init(rawValue: UInt64.max)
+ )
+ ]
+
+ let schemaConsistent = Fory(config: .init(xlang: true, trackRef: false,
compatible: false))
+ schemaConsistent.register(UnsignedFieldBundle.self, id: 9801)
+
+ let compatible = Fory(config: .init(xlang: true, trackRef: false,
compatible: true))
+ compatible.register(UnsignedFieldBundle.self, id: 9801)
+
+ for value in cases {
+ let decodedSchema: UnsignedFieldBundle = try
schemaConsistent.deserialize(try schemaConsistent.serialize(value))
+ let decodedCompatible: UnsignedFieldBundle = try
compatible.deserialize(try compatible.serialize(value))
+ #expect(decodedSchema == value)
+ #expect(decodedCompatible == value)
+ }
+}
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]