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 dd0777bec feat(swift): harden decode paths and add missing wire type
support (#3427)
dd0777bec is described below
commit dd0777bec46d5ffa090655692b0b25bfc3e7788b
Author: Shawn Yang <[email protected]>
AuthorDate: Thu Feb 26 18:30:03 2026 +0800
feat(swift): harden decode paths and add missing wire type support (#3427)
## Why?
## What does this PR do?
## Related issues
## 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
---
.github/workflows/ci.yml | 7 +
AGENTS.md | 35 ++
ci/format.sh | 21 ++
docs/compiler/compiler-guide.md | 30 +-
swift/.swiftlint.yml | 50 +++
swift/Package.swift | 8 +-
swift/Sources/Fory/AnySerializer.swift | 20 +-
swift/Sources/Fory/ByteBuffer.swift | 11 +-
swift/Sources/Fory/CollectionSerializers.swift | 74 +++-
swift/Sources/Fory/Context.swift | 99 ++++++
swift/Sources/Fory/DateTimeSerializers.swift | 28 ++
swift/Sources/Fory/FieldSkipper.swift | 447 ++++++++++++++++++++-----
swift/Sources/Fory/Fory.swift | 58 ++--
swift/Sources/Fory/MetaString.swift | 61 ++--
swift/Sources/Fory/MurmurHash3.swift | 22 +-
swift/Sources/Fory/OptionalSerializer.swift | 6 +-
swift/Sources/Fory/PrimitiveSerializers.swift | 40 ++-
swift/Sources/Fory/TypeMeta.swift | 11 +-
swift/Sources/Fory/TypeResolver.swift | 106 +++++-
swift/Sources/ForyMacro/ForyObjectMacro.swift | 125 ++++++-
swift/Tests/ForyTests/AnyTests.swift | 98 +++++-
swift/Tests/ForyTests/DateTimeTests.swift | 6 +
swift/Tests/ForyTests/EnumTests.swift | 4 +-
swift/Tests/ForyTests/ForySwiftTests.swift | 133 +++++++-
swift/Tests/ForyXlangTests/main.swift | 64 ++--
25 files changed, 1295 insertions(+), 269 deletions(-)
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index a78931c8e..b0db1328c 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -217,6 +217,13 @@ jobs:
run: |
cd swift
swift test
+ - name: Run SwiftLint check
+ run: |
+ if ! command -v swiftlint >/dev/null; then
+ brew install swiftlint
+ fi
+ cd swift
+ swiftlint lint --config .swiftlint.yml
swift_xlang:
name: Swift Xlang Test
diff --git a/AGENTS.md b/AGENTS.md
index 56a3fc319..db711837a 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -235,6 +235,41 @@ cd fory-core
RUST_BACKTRACE=1 FORY_PANIC_ON_ERROR=1 FORY_RUST_JAVA_CI=1
ENABLE_FORY_DEBUG_OUTPUT=1 mvn test -Dtest=org.apache.fory.xlang.RustXlangTest
```
+### Swift Development
+
+- All commands must be executed within the `swift` directory.
+- All changes to `swift` must pass lint and tests.
+- Swift lint uses `swift/.swiftlint.yml`.
+- Use `ENABLE_FORY_DEBUG_OUTPUT=1` when debugging Swift tests.
+
+```bash
+# Build package
+swift build
+
+# Run tests
+swift test
+
+# Run tests with debug output
+ENABLE_FORY_DEBUG_OUTPUT=1 swift test
+
+# Lint check
+swiftlint lint --config .swiftlint.yml
+
+# Auto-fix lint issues where supported
+swiftlint --fix --config .swiftlint.yml
+```
+
+Run Swift xlang tests:
+
+```bash
+cd swift
+swift build -c release --disable-automatic-resolution --product ForyXlangTests
+cd ../java
+mvn -T16 install -DskipTests
+cd fory-core
+FORY_SWIFT_JAVA_CI=1 ENABLE_FORY_DEBUG_OUTPUT=1 mvn -T16 test
-Dtest=org.apache.fory.xlang.SwiftXlangTest
+```
+
### JavaScript/TypeScript Development
- All commands must be executed within the `javascript` directory.
diff --git a/ci/format.sh b/ci/format.sh
index 5501dc481..0545ea231 100755
--- a/ci/format.sh
+++ b/ci/format.sh
@@ -213,6 +213,18 @@ format_go() {
fi
}
+format_swift() {
+ echo "$(date)" "SwiftLint check Swift files...."
+ if command -v swiftlint >/dev/null; then
+ pushd "$ROOT/swift"
+ swiftlint lint --config .swiftlint.yml
+ popd
+ echo "$(date)" "SwiftLint done!"
+ else
+ echo "WARNING: swiftlint is not installed, skip swift lint check"
+ fi
+}
+
# Format all files, and print the diff to stdout for travis.
format_all() {
format_all_scripts "${@}"
@@ -239,6 +251,9 @@ format_all() {
git ls-files -- '*.go' "${GIT_LS_EXCLUDES[@]}" | xargs -P 5 gofmt -w
fi
+ echo "$(date)" "lint swift...."
+ format_swift
+
echo "$(date)" "done!"
}
@@ -292,6 +307,10 @@ format_changed() {
prettier --write "**/*.md"
popd
fi
+
+ if ! git diff --diff-filter=ACRM --quiet --exit-code "$MERGEBASE" --
'swift' &>/dev/null; then
+ format_swift
+ fi
}
@@ -316,6 +335,8 @@ elif [ "${1-}" == '--python' ]; then
format_python
elif [ "${1-}" == '--go' ]; then
format_go
+elif [ "${1-}" == '--swift' ]; then
+ format_swift
else
# Add the origin remote if it doesn't exist
if ! git remote -v | grep -q origin; then
diff --git a/docs/compiler/compiler-guide.md b/docs/compiler/compiler-guide.md
index 4a513f290..0457751d2 100644
--- a/docs/compiler/compiler-guide.md
+++ b/docs/compiler/compiler-guide.md
@@ -52,21 +52,21 @@ foryc --scan-generated [OPTIONS]
Compile options:
-| Option | Description
| Default |
-| ------------------------------------- |
----------------------------------------------------- | ------------------- |
-| `--lang` | Comma-separated target languages
| `all` |
-| `--output`, `-o` | Output directory
| `./generated` |
-| `--package` | Override package name from Fory IDL
file | (from file) |
-| `-I`, `--proto_path`, `--import_path` | Add directory to import search path
(can be repeated) | (none) |
-| `--java_out=DST_DIR` | Generate Java code in DST_DIR
| (none) |
-| `--python_out=DST_DIR` | Generate Python code in DST_DIR
| (none) |
-| `--cpp_out=DST_DIR` | Generate C++ code in DST_DIR
| (none) |
-| `--go_out=DST_DIR` | Generate Go code in DST_DIR
| (none) |
-| `--rust_out=DST_DIR` | Generate Rust code in DST_DIR
| (none) |
-| `--csharp_out=DST_DIR` | Generate C# code in DST_DIR
| (none) |
-| `--go_nested_type_style` | Go nested type naming: `camelcase`
or `underscore` | `underscore` |
-| `--emit-fdl` | Emit translated FDL (for non-FDL
inputs) | `false` |
-| `--emit-fdl-path` | Write translated FDL to this path
(file or directory) | (stdout) |
+| Option | Description
| Default |
+| ------------------------------------- |
----------------------------------------------------- | ------------- |
+| `--lang` | Comma-separated target languages
| `all` |
+| `--output`, `-o` | Output directory
| `./generated` |
+| `--package` | Override package name from Fory IDL
file | (from file) |
+| `-I`, `--proto_path`, `--import_path` | Add directory to import search path
(can be repeated) | (none) |
+| `--java_out=DST_DIR` | Generate Java code in DST_DIR
| (none) |
+| `--python_out=DST_DIR` | Generate Python code in DST_DIR
| (none) |
+| `--cpp_out=DST_DIR` | Generate C++ code in DST_DIR
| (none) |
+| `--go_out=DST_DIR` | Generate Go code in DST_DIR
| (none) |
+| `--rust_out=DST_DIR` | Generate Rust code in DST_DIR
| (none) |
+| `--csharp_out=DST_DIR` | Generate C# code in DST_DIR
| (none) |
+| `--go_nested_type_style` | Go nested type naming: `camelcase`
or `underscore` | `underscore` |
+| `--emit-fdl` | Emit translated FDL (for non-FDL
inputs) | `false` |
+| `--emit-fdl-path` | Write translated FDL to this path
(file or directory) | (stdout) |
Scan options (with `--scan-generated`):
diff --git a/swift/.swiftlint.yml b/swift/.swiftlint.yml
new file mode 100644
index 000000000..eb64b45c7
--- /dev/null
+++ b/swift/.swiftlint.yml
@@ -0,0 +1,50 @@
+# 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.
+
+included:
+ - Sources
+ - Tests
+ - Package.swift
+
+excluded:
+ - .build
+
+identifier_name:
+ min_length:
+ warning: 2
+ error: 1
+
+line_length:
+ warning: 160
+ error: 200
+ ignores_comments: true
+
+function_body_length:
+ warning: 200
+ error: 260
+
+type_body_length:
+ warning: 900
+ error: 1400
+
+file_length:
+ warning: 1900
+ error: 2200
+
+cyclomatic_complexity:
+ warning: 50
+ error: 70
diff --git a/swift/Package.swift b/swift/Package.swift
index cb2f60e71..c5a7f8a61 100644
--- a/swift/Package.swift
+++ b/swift/Package.swift
@@ -6,7 +6,7 @@ let package = Package(
name: "fory-swift",
platforms: [
.macOS(.v13),
- .iOS(.v16),
+ .iOS(.v16)
],
products: [
.library(
@@ -16,7 +16,7 @@ let package = Package(
.executable(
name: "ForyXlangTests",
targets: ["ForyXlangTests"]
- ),
+ )
],
dependencies: [
.package(url: "https://github.com/swiftlang/swift-syntax.git", from:
"600.0.0")
@@ -28,7 +28,7 @@ let package = Package(
.product(name: "SwiftCompilerPlugin", package: "swift-syntax"),
.product(name: "SwiftSyntax", package: "swift-syntax"),
.product(name: "SwiftSyntaxBuilder", package: "swift-syntax"),
- .product(name: "SwiftSyntaxMacros", package: "swift-syntax"),
+ .product(name: "SwiftSyntaxMacros", package: "swift-syntax")
],
path: "Sources/ForyMacro"
),
@@ -47,6 +47,6 @@ let package = Package(
name: "ForyTests",
dependencies: ["Fory"],
path: "Tests/ForyTests"
- ),
+ )
]
)
diff --git a/swift/Sources/Fory/AnySerializer.swift
b/swift/Sources/Fory/AnySerializer.swift
index 70f68107b..e0e1968c0 100644
--- a/swift/Sources/Fory/AnySerializer.swift
+++ b/swift/Sources/Fory/AnySerializer.swift
@@ -103,7 +103,7 @@ private protocol OptionalTypeMarker {
}
extension Optional: OptionalTypeMarker {
- static var noneValue: Optional<Wrapped> { nil }
+ static var noneValue: Wrapped? { nil }
}
private struct DynamicAnyValue: Serializer {
@@ -312,6 +312,9 @@ private func writeAnyTypeInfo(_ value: Any, context:
WriteContext) throws {
}
private func writeAnyPayload(_ value: Any, context: WriteContext, hasGenerics:
Bool) throws {
+ try context.enterDynamicAnyDepth()
+ defer { context.leaveDynamicAnyDepth() }
+
if let serializer = value as? any Serializer {
try serializer.foryWriteData(context, hasGenerics: hasGenerics)
return
@@ -340,18 +343,25 @@ private func writeAnyPayload(_ value: Any, context:
WriteContext, hasGenerics: B
public func castAnyDynamicValue<T>(_ value: Any?, to type: T.Type) throws -> T
{
_ = type
+ func castNilSentinel(_ sentinel: Any) throws -> T {
+ guard let casted = sentinel as? T else {
+ throw ForyError.invalidData("cannot cast dynamic Any value to
\(type)")
+ }
+ return casted
+ }
+
if value == nil {
if T.self == Any.self {
- return ForyAnyNullValue() as! T
+ return try castNilSentinel(ForyAnyNullValue())
}
if T.self == AnyObject.self {
- return NSNull() as! T
+ return try castNilSentinel(NSNull())
}
if T.self == (any Serializer).self {
- return ForyAnyNullValue() as! T
+ return try castNilSentinel(ForyAnyNullValue())
}
if let optionalType = T.self as? any OptionalTypeMarker.Type {
- return optionalType.noneValue as! T
+ return try castNilSentinel(optionalType.noneValue)
}
}
diff --git a/swift/Sources/Fory/ByteBuffer.swift
b/swift/Sources/Fory/ByteBuffer.swift
index 96b978580..214c22c5d 100644
--- a/swift/Sources/Fory/ByteBuffer.swift
+++ b/swift/Sources/Fory/ByteBuffer.swift
@@ -870,7 +870,7 @@ public final class ByteBuffer {
if count == 0 {
return []
}
- return try Array<UInt8>(unsafeUninitializedCapacity: count) {
destination, initializedCount in
+ return try [UInt8](unsafeUninitializedCapacity: count) { destination,
initializedCount in
try readBytes(into: UnsafeMutableRawBufferPointer(destination))
initializedCount = count
}
@@ -883,12 +883,11 @@ public final class ByteBuffer {
let start = cursor
let end = start + count
cursor = end
- return storage.withUnsafeBufferPointer { buffer in
- String(
- decoding: UnsafeBufferPointer(rebasing: buffer[start..<end]),
- as: UTF8.self
- )
+ let utf8Bytes = storage[start..<end]
+ guard let decoded = String(bytes: utf8Bytes, encoding: .utf8) else {
+ throw ForyError.invalidData("invalid UTF-8 sequence")
}
+ return decoded
}
@inlinable
diff --git a/swift/Sources/Fory/CollectionSerializers.swift
b/swift/Sources/Fory/CollectionSerializers.swift
index 0a1ed5e09..4e2f04de1 100644
--- a/swift/Sources/Fory/CollectionSerializers.swift
+++ b/swift/Sources/Fory/CollectionSerializers.swift
@@ -44,6 +44,8 @@ private func primitiveArrayTypeID<Element: Serializer>(for _:
Element.Type) -> T
if Element.self == UInt16.self { return .uint16Array }
if Element.self == UInt32.self { return .uint32Array }
if Element.self == UInt64.self { return .uint64Array }
+ if Element.self == Float16.self { return .float16Array }
+ if Element.self == BFloat16.self { return .bfloat16Array }
if Element.self == Float.self { return .float32Array }
if Element.self == Double.self { return .float64Array }
return nil
@@ -62,7 +64,7 @@ private func readArrayUninitialized<Element>(
count: Int,
_ initializer: (UnsafeMutablePointer<Element>) throws -> Void
) rethrows -> [Element] {
- try Array<Element>(unsafeUninitializedCapacity: count) { destination,
initializedCount in
+ try [Element](unsafeUninitializedCapacity: count) { destination,
initializedCount in
if count > 0 {
try initializer(destination.baseAddress!)
}
@@ -186,6 +188,24 @@ private func writePrimitiveArray<Element: Serializer>(_
value: [Element], contex
return
}
+ if Element.self == Float16.self {
+ let values = uncheckedArrayCast(value, to: Float16.self)
+ context.buffer.writeVarUInt32(UInt32(values.count * 2))
+ for item in values {
+ context.buffer.writeUInt16(item.bitPattern)
+ }
+ return
+ }
+
+ if Element.self == BFloat16.self {
+ let values = uncheckedArrayCast(value, to: BFloat16.self)
+ context.buffer.writeVarUInt32(UInt32(values.count * 2))
+ for item in values {
+ context.buffer.writeUInt16(item.rawValue)
+ }
+ return
+ }
+
if Element.self == Float.self {
let values = uncheckedArrayCast(value, to: Float.self)
context.buffer.writeVarUInt32(UInt32(values.count * 4))
@@ -216,13 +236,16 @@ private func writePrimitiveArray<Element: Serializer>(_
value: [Element], contex
private func readPrimitiveArray<Element: Serializer>(_ context: ReadContext)
throws -> [Element] {
let payloadSize = Int(try context.buffer.readVarUInt32())
+ try context.ensureRemainingBytes(payloadSize, label:
"\(Element.self)_array_payload")
if Element.self == UInt8.self {
+ try context.ensureCollectionLength(payloadSize, label: "uint8_array")
let bytes = try context.buffer.readBytes(count: payloadSize)
return uncheckedArrayCast(bytes, to: Element.self)
}
if Element.self == Bool.self {
+ try context.ensureCollectionLength(payloadSize, label: "bool_array")
let out = try readArrayUninitialized(count: payloadSize) { destination
in
for index in 0..<payloadSize {
destination.advanced(by: index).initialize(to: try
context.buffer.readUInt8() != 0)
@@ -232,6 +255,7 @@ private func readPrimitiveArray<Element: Serializer>(_
context: ReadContext) thr
}
if Element.self == Int8.self {
+ try context.ensureCollectionLength(payloadSize, label: "int8_array")
var out = Array(repeating: Int8(0), count: payloadSize)
try out.withUnsafeMutableBytes { rawBytes in
try context.buffer.readBytes(into: rawBytes)
@@ -242,6 +266,7 @@ private func readPrimitiveArray<Element: Serializer>(_
context: ReadContext) thr
if Element.self == Int16.self {
if payloadSize % 2 != 0 { throw ForyError.invalidData("int16 array
payload size mismatch") }
let count = payloadSize / 2
+ try context.ensureCollectionLength(count, label: "int16_array")
if hostIsLittleEndian {
var out = Array(repeating: Int16(0), count: count)
try out.withUnsafeMutableBytes { rawBytes in
@@ -260,6 +285,7 @@ private func readPrimitiveArray<Element: Serializer>(_
context: ReadContext) thr
if Element.self == Int32.self {
if payloadSize % 4 != 0 { throw ForyError.invalidData("int32 array
payload size mismatch") }
let count = payloadSize / 4
+ try context.ensureCollectionLength(count, label: "int32_array")
if hostIsLittleEndian {
var out = Array(repeating: Int32(0), count: count)
try out.withUnsafeMutableBytes { rawBytes in
@@ -278,6 +304,7 @@ private func readPrimitiveArray<Element: Serializer>(_
context: ReadContext) thr
if Element.self == UInt32.self {
if payloadSize % 4 != 0 { throw ForyError.invalidData("uint32 array
payload size mismatch") }
let count = payloadSize / 4
+ try context.ensureCollectionLength(count, label: "uint32_array")
if hostIsLittleEndian {
var out = Array(repeating: UInt32(0), count: count)
try out.withUnsafeMutableBytes { rawBytes in
@@ -296,6 +323,7 @@ private func readPrimitiveArray<Element: Serializer>(_
context: ReadContext) thr
if Element.self == Int64.self {
if payloadSize % 8 != 0 { throw ForyError.invalidData("int64 array
payload size mismatch") }
let count = payloadSize / 8
+ try context.ensureCollectionLength(count, label: "int64_array")
if hostIsLittleEndian {
var out = Array(repeating: Int64(0), count: count)
try out.withUnsafeMutableBytes { rawBytes in
@@ -314,6 +342,7 @@ private func readPrimitiveArray<Element: Serializer>(_
context: ReadContext) thr
if Element.self == UInt64.self {
if payloadSize % 8 != 0 { throw ForyError.invalidData("uint64 array
payload size mismatch") }
let count = payloadSize / 8
+ try context.ensureCollectionLength(count, label: "uint64_array")
if hostIsLittleEndian {
var out = Array(repeating: UInt64(0), count: count)
try out.withUnsafeMutableBytes { rawBytes in
@@ -332,6 +361,7 @@ private func readPrimitiveArray<Element: Serializer>(_
context: ReadContext) thr
if Element.self == UInt16.self {
if payloadSize % 2 != 0 { throw ForyError.invalidData("uint16 array
payload size mismatch") }
let count = payloadSize / 2
+ try context.ensureCollectionLength(count, label: "uint16_array")
if hostIsLittleEndian {
var out = Array(repeating: UInt16(0), count: count)
try out.withUnsafeMutableBytes { rawBytes in
@@ -347,9 +377,34 @@ private func readPrimitiveArray<Element: Serializer>(_
context: ReadContext) thr
return uncheckedArrayCast(out, to: Element.self)
}
+ if Element.self == Float16.self {
+ if payloadSize % 2 != 0 { throw ForyError.invalidData("float16 array
payload size mismatch") }
+ let count = payloadSize / 2
+ try context.ensureCollectionLength(count, label: "float16_array")
+ let values = try readArrayUninitialized(count: count) { destination in
+ for index in 0..<count {
+ destination.advanced(by: index).initialize(to:
Float16(bitPattern: try context.buffer.readUInt16()))
+ }
+ }
+ return uncheckedArrayCast(values, to: Element.self)
+ }
+
+ if Element.self == BFloat16.self {
+ if payloadSize % 2 != 0 { throw ForyError.invalidData("bfloat16 array
payload size mismatch") }
+ let count = payloadSize / 2
+ try context.ensureCollectionLength(count, label: "bfloat16_array")
+ let values = try readArrayUninitialized(count: count) { destination in
+ for index in 0..<count {
+ destination.advanced(by: index).initialize(to:
BFloat16(rawValue: try context.buffer.readUInt16()))
+ }
+ }
+ return uncheckedArrayCast(values, to: Element.self)
+ }
+
if Element.self == Float.self {
if payloadSize % 4 != 0 { throw ForyError.invalidData("float32 array
payload size mismatch") }
let count = payloadSize / 4
+ try context.ensureCollectionLength(count, label: "float32_array")
if hostIsLittleEndian {
var out = Array(repeating: Float(0), count: count)
try out.withUnsafeMutableBytes { rawBytes in
@@ -367,6 +422,7 @@ private func readPrimitiveArray<Element: Serializer>(_
context: ReadContext) thr
if payloadSize % 8 != 0 { throw ForyError.invalidData("float64 array
payload size mismatch") }
let count = payloadSize / 8
+ try context.ensureCollectionLength(count, label: "float64_array")
if hostIsLittleEndian {
var out = Array(repeating: Double(0), count: count)
try out.withUnsafeMutableBytes { rawBytes in
@@ -467,6 +523,7 @@ extension Array: Serializer where Element: Serializer {
let buffer = context.buffer
let length = Int(try buffer.readVarUInt32())
+ try context.ensureCollectionLength(length, label: "array")
if length == 0 {
return []
}
@@ -608,12 +665,12 @@ extension Set: Serializer where Element: Serializer &
Hashable {
}
public static func foryReadData(_ context: ReadContext) throws ->
Set<Element> {
- Set(try Array<Element>.foryReadData(context))
+ Set(try [Element].foryReadData(context))
}
}
extension Dictionary: Serializer where Key: Serializer & Hashable, Value:
Serializer {
- public static func foryDefault() -> Dictionary<Key, Value> { [:] }
+ public static func foryDefault() -> [Key: Value] { [:] }
public static var staticTypeId: TypeId { .map }
@@ -823,14 +880,15 @@ extension Dictionary: Serializer where Key: Serializer &
Hashable, Value: Serial
}
}
- public static func foryReadData(_ context: ReadContext) throws ->
Dictionary<Key, Value> {
+ public static func foryReadData(_ context: ReadContext) throws -> [Key:
Value] {
let totalLength = Int(try context.buffer.readVarUInt32())
+ try context.ensureCollectionLength(totalLength, label: "map")
if totalLength == 0 {
return [:]
}
var map: [Key: Value] = [:]
- map.reserveCapacity(totalLength)
+ map.reserveCapacity(Swift.min(totalLength, context.buffer.remaining))
let keyDynamicType = Key.staticTypeId == .unknown
let valueDynamicType = Value.staticTypeId == .unknown
let canonicalizeValues = context.trackRef &&
Value.isReferenceTrackableType
@@ -891,6 +949,9 @@ extension Dictionary: Serializer where Key: Serializer &
Hashable, Value: Serial
}
let chunkSize = Int(try context.buffer.readUInt8())
+ if chunkSize > (totalLength - dynamicReadCount) {
+ throw ForyError.invalidData("map dynamic chunk size
exceeds remaining entries")
+ }
if !keyDeclared {
try Key.foryReadTypeInfo(context)
}
@@ -987,6 +1048,9 @@ extension Dictionary: Serializer where Key: Serializer &
Hashable, Value: Serial
}
let chunkSize = Int(try context.buffer.readUInt8())
+ if chunkSize > (totalLength - readCount) {
+ throw ForyError.invalidData("map chunk size exceeds remaining
entries")
+ }
if !keyDeclared {
try Key.foryReadTypeInfo(context)
}
diff --git a/swift/Sources/Fory/Context.swift b/swift/Sources/Fory/Context.swift
index 08de2b918..7832f7e67 100644
--- a/swift/Sources/Fory/Context.swift
+++ b/swift/Sources/Fory/Context.swift
@@ -182,11 +182,13 @@ public final class WriteContext {
public let trackRef: Bool
public let compatible: Bool
public let checkClassVersion: Bool
+ public let maxDepth: Int
public let refWriter: RefWriter
public let compatibleTypeDefState: CompatibleTypeDefWriteState
public let metaStringWriteState: MetaStringWriteState
private var compatibleTypeDefStateUsed = false
private var metaStringWriteStateUsed = false
+ private var dynamicAnyDepth = 0
public init(
buffer: ByteBuffer,
@@ -194,6 +196,7 @@ public final class WriteContext {
trackRef: Bool,
compatible: Bool = false,
checkClassVersion: Bool = true,
+ maxDepth: Int = 5,
compatibleTypeDefState: CompatibleTypeDefWriteState =
CompatibleTypeDefWriteState(),
metaStringWriteState: MetaStringWriteState = MetaStringWriteState()
) {
@@ -202,11 +205,33 @@ public final class WriteContext {
self.trackRef = trackRef
self.compatible = compatible
self.checkClassVersion = checkClassVersion
+ self.maxDepth = maxDepth
self.refWriter = RefWriter()
self.compatibleTypeDefState = compatibleTypeDefState
self.metaStringWriteState = metaStringWriteState
}
+ @inline(__always)
+ public func enterDynamicAnyDepth() throws {
+ if maxDepth < 0 {
+ throw ForyError.invalidData("configured maxDepth \(maxDepth) is
negative")
+ }
+ let nextDepth = dynamicAnyDepth + 1
+ if nextDepth > maxDepth {
+ throw ForyError.invalidData(
+ "dynamic Any nesting depth \(nextDepth) exceeds configured
maxDepth \(maxDepth)"
+ )
+ }
+ dynamicAnyDepth = nextDepth
+ }
+
+ @inline(__always)
+ public func leaveDynamicAnyDepth() {
+ if dynamicAnyDepth > 0 {
+ dynamicAnyDepth -= 1
+ }
+ }
+
public func writeCompatibleTypeMeta<T: Serializer>(
for type: T.Type,
typeMeta: TypeMeta
@@ -233,6 +258,9 @@ public final class WriteContext {
}
public func resetObjectState() {
+ if dynamicAnyDepth != 0 {
+ dynamicAnyDepth = 0
+ }
if trackRef {
refWriter.reset()
}
@@ -267,11 +295,15 @@ public final class ReadContext {
public let trackRef: Bool
public let compatible: Bool
public let checkClassVersion: Bool
+ public let maxCollectionLength: Int
+ public let maxBinaryLength: Int
+ public let maxDepth: Int
public let refReader: RefReader
public let compatibleTypeDefState: CompatibleTypeDefReadState
public let metaStringReadState: MetaStringReadState
private var compatibleTypeDefStateUsed = false
private var metaStringReadStateUsed = false
+ private var dynamicAnyDepth = 0
private var pendingRefStack: [PendingRefSlot] = []
private var pendingCompatibleTypeMeta: [ObjectIdentifier: [TypeMeta]] = [:]
@@ -284,6 +316,9 @@ public final class ReadContext {
trackRef: Bool,
compatible: Bool = false,
checkClassVersion: Bool = true,
+ maxCollectionLength: Int = 1_000_000,
+ maxBinaryLength: Int = 64 * 1024 * 1024,
+ maxDepth: Int = 5,
compatibleTypeDefState: CompatibleTypeDefReadState =
CompatibleTypeDefReadState(),
metaStringReadState: MetaStringReadState = MetaStringReadState()
) {
@@ -292,11 +327,72 @@ public final class ReadContext {
self.trackRef = trackRef
self.compatible = compatible
self.checkClassVersion = checkClassVersion
+ self.maxCollectionLength = maxCollectionLength
+ self.maxBinaryLength = maxBinaryLength
+ self.maxDepth = maxDepth
self.refReader = RefReader()
self.compatibleTypeDefState = compatibleTypeDefState
self.metaStringReadState = metaStringReadState
}
+ @inline(__always)
+ public func enterDynamicAnyDepth() throws {
+ if maxDepth < 0 {
+ throw ForyError.invalidData("configured maxDepth \(maxDepth) is
negative")
+ }
+ let nextDepth = dynamicAnyDepth + 1
+ if nextDepth > maxDepth {
+ throw ForyError.invalidData(
+ "dynamic Any nesting depth \(nextDepth) exceeds configured
maxDepth \(maxDepth)"
+ )
+ }
+ dynamicAnyDepth = nextDepth
+ }
+
+ @inline(__always)
+ public func leaveDynamicAnyDepth() {
+ if dynamicAnyDepth > 0 {
+ dynamicAnyDepth -= 1
+ }
+ }
+
+ @inline(__always)
+ public func ensureCollectionLength(_ length: Int, label: String) throws {
+ if length < 0 {
+ throw ForyError.invalidData("\(label) length is negative")
+ }
+ if length > maxCollectionLength {
+ throw ForyError.invalidData(
+ "\(label) length \(length) exceeds configured
maxCollectionLength \(maxCollectionLength)"
+ )
+ }
+ }
+
+ @inline(__always)
+ public func ensureBinaryLength(_ length: Int, label: String) throws {
+ if length < 0 {
+ throw ForyError.invalidData("\(label) size is negative")
+ }
+ if length > maxBinaryLength {
+ throw ForyError.invalidData(
+ "\(label) size \(length) exceeds configured maxBinaryLength
\(maxBinaryLength)"
+ )
+ }
+ }
+
+ @inline(__always)
+ public func ensureRemainingBytes(_ byteCount: Int, label: String) throws {
+ if byteCount < 0 {
+ throw ForyError.invalidData("\(label) size is negative")
+ }
+ let remainingBytes = buffer.remaining
+ if byteCount > remainingBytes {
+ throw ForyError.invalidData(
+ "\(label) requires \(byteCount) bytes but only
\(remainingBytes) remain in buffer"
+ )
+ }
+ }
+
public func pushPendingReference(_ refID: UInt32) {
pendingRefStack.append(PendingRefSlot(refID: refID, bound: false))
}
@@ -410,6 +506,9 @@ public final class ReadContext {
}
public func resetObjectState() {
+ if dynamicAnyDepth != 0 {
+ dynamicAnyDepth = 0
+ }
if trackRef {
refReader.reset()
if !pendingRefStack.isEmpty {
diff --git a/swift/Sources/Fory/DateTimeSerializers.swift
b/swift/Sources/Fory/DateTimeSerializers.swift
index 75afd8af6..3fa502c15 100644
--- a/swift/Sources/Fory/DateTimeSerializers.swift
+++ b/swift/Sources/Fory/DateTimeSerializers.swift
@@ -94,6 +94,34 @@ public struct ForyTimestamp: Serializer, Equatable, Hashable
{
}
}
+extension Duration: Serializer {
+ public static func foryDefault() -> Duration {
+ .zero
+ }
+
+ public static var staticTypeId: TypeId {
+ .duration
+ }
+
+ public func foryWriteData(_ context: WriteContext, hasGenerics: Bool)
throws {
+ _ = hasGenerics
+ let components = self.components
+ let nanos = components.attoseconds / 1_000_000_000
+ let remainder = components.attoseconds % 1_000_000_000
+ if remainder != 0 {
+ throw ForyError.encodingError("Duration precision finer than
nanoseconds is not supported")
+ }
+ context.buffer.writeVarInt64(components.seconds)
+ context.buffer.writeInt32(Int32(nanos))
+ }
+
+ public static func foryReadData(_ context: ReadContext) throws -> Duration
{
+ let seconds = try context.buffer.readVarInt64()
+ let nanos = try context.buffer.readInt32()
+ return .seconds(seconds) + .nanoseconds(Int64(nanos))
+ }
+}
+
extension Date: Serializer {
public static func foryDefault() -> Date {
Date(timeIntervalSince1970: 0)
diff --git a/swift/Sources/Fory/FieldSkipper.swift
b/swift/Sources/Fory/FieldSkipper.swift
index 65af9dd3b..7d346150e 100644
--- a/swift/Sources/Fory/FieldSkipper.swift
+++ b/swift/Sources/Fory/FieldSkipper.swift
@@ -22,105 +22,392 @@ public enum FieldSkipper {
context: ReadContext,
fieldType: TypeMetaFieldType
) throws {
- _ = try readFieldValue(context: context, fieldType: fieldType)
+ _ = try readFieldValue(
+ context: context,
+ fieldType: fieldType,
+ readTypeInfo: needsTypeInfoForField(fieldType.typeID)
+ )
}
- private static func readEnumOrdinal(
- _ context: ReadContext,
- refMode: RefMode
- ) throws -> UInt32? {
+ private static func needsTypeInfoForField(_ typeID: UInt32) -> Bool {
+ guard let resolved = TypeId(rawValue: typeID) else {
+ return true
+ }
+ return TypeId.needsTypeInfoForField(resolved)
+ }
+
+ private static func readFieldValue(
+ context: ReadContext,
+ fieldType: TypeMetaFieldType,
+ runtimeTypeInfo: DynamicTypeInfo? = nil,
+ readTypeInfo: Bool
+ ) throws -> Any? {
+ let refMode = RefMode.from(nullable: fieldType.nullable, trackRef:
fieldType.trackRef)
+ return try readValueWithRefMode(
+ context: context,
+ fieldType: fieldType,
+ runtimeTypeInfo: runtimeTypeInfo,
+ refMode: refMode,
+ readTypeInfo: readTypeInfo
+ )
+ }
+
+ private static func readValueWithRefMode(
+ context: ReadContext,
+ fieldType: TypeMetaFieldType,
+ runtimeTypeInfo: DynamicTypeInfo?,
+ refMode: RefMode,
+ readTypeInfo: Bool
+ ) throws -> Any? {
switch refMode {
case .none:
- return try context.buffer.readVarUInt32()
+ return try readFieldPayload(
+ context: context,
+ fieldType: fieldType,
+ runtimeTypeInfo: runtimeTypeInfo,
+ readTypeInfo: readTypeInfo
+ )
case .nullOnly:
let flag = try context.buffer.readInt8()
if flag == RefFlag.null.rawValue {
return nil
}
- if flag != RefFlag.notNullValue.rawValue {
- throw ForyError.invalidData("unexpected enum nullOnly flag
\(flag)")
+ guard flag == RefFlag.notNullValue.rawValue else {
+ throw ForyError.invalidData("unexpected nullOnly flag \(flag)")
}
- return try context.buffer.readVarUInt32()
+ return try readFieldPayload(
+ context: context,
+ fieldType: fieldType,
+ runtimeTypeInfo: runtimeTypeInfo,
+ readTypeInfo: readTypeInfo
+ )
case .tracking:
- throw ForyError.invalidData("enum tracking ref mode is not
supported")
+ let rawFlag = try context.buffer.readInt8()
+ guard let flag = RefFlag(rawValue: rawFlag) else {
+ throw ForyError.invalidData("unexpected tracking flag
\(rawFlag)")
+ }
+
+ switch flag {
+ case .null:
+ return nil
+ case .ref:
+ let refID = try context.buffer.readVarUInt32()
+ return try context.refReader.readRefValue(refID)
+ case .refValue:
+ let refID = context.refReader.reserveRefID()
+ let value = try readFieldPayload(
+ context: context,
+ fieldType: fieldType,
+ runtimeTypeInfo: runtimeTypeInfo,
+ readTypeInfo: readTypeInfo
+ )
+ context.refReader.storeRef(value, at: refID)
+ return value
+ case .notNullValue:
+ return try readFieldPayload(
+ context: context,
+ fieldType: fieldType,
+ runtimeTypeInfo: runtimeTypeInfo,
+ readTypeInfo: readTypeInfo
+ )
+ }
}
}
- private static func readFieldValue(
+ private static func readFieldPayload(
+ context: ReadContext,
+ fieldType: TypeMetaFieldType,
+ runtimeTypeInfo: DynamicTypeInfo?,
+ readTypeInfo: Bool
+ ) throws -> Any {
+ if let runtimeTypeInfo {
+ return try context.typeResolver.readDynamicValue(typeInfo:
runtimeTypeInfo, context: context)
+ }
+ if readTypeInfo {
+ let typeInfo = try
context.typeResolver.readDynamicTypeInfo(context: context)
+ return try context.typeResolver.readDynamicValue(typeInfo:
typeInfo, context: context)
+ }
+
+ guard let resolvedTypeID = TypeId(rawValue: fieldType.typeID) else {
+ throw ForyError.invalidData("unknown compatible field type id
\(fieldType.typeID)")
+ }
+
+ switch resolvedTypeID {
+ case .none:
+ return ForyAnyNullValue()
+ case .bool:
+ return try Bool.foryRead(context, refMode: .none, readTypeInfo:
false)
+ case .int8:
+ return try Int8.foryRead(context, refMode: .none, readTypeInfo:
false)
+ case .int16:
+ return try Int16.foryRead(context, refMode: .none, readTypeInfo:
false)
+ case .int32:
+ return try ForyInt32Fixed.foryRead(context, refMode: .none,
readTypeInfo: false)
+ case .varint32:
+ return try Int32.foryRead(context, refMode: .none, readTypeInfo:
false)
+ case .int64:
+ return try ForyInt64Fixed.foryRead(context, refMode: .none,
readTypeInfo: false)
+ case .varint64:
+ return try Int64.foryRead(context, refMode: .none, readTypeInfo:
false)
+ case .taggedInt64:
+ return try ForyInt64Tagged.foryRead(context, refMode: .none,
readTypeInfo: false)
+ case .uint8:
+ return try UInt8.foryRead(context, refMode: .none, readTypeInfo:
false)
+ case .uint16:
+ return try UInt16.foryRead(context, refMode: .none, readTypeInfo:
false)
+ case .uint32:
+ return try ForyUInt32Fixed.foryRead(context, refMode: .none,
readTypeInfo: false)
+ case .varUInt32:
+ return try UInt32.foryRead(context, refMode: .none, readTypeInfo:
false)
+ case .uint64:
+ return try ForyUInt64Fixed.foryRead(context, refMode: .none,
readTypeInfo: false)
+ case .varUInt64:
+ return try UInt64.foryRead(context, refMode: .none, readTypeInfo:
false)
+ case .taggedUInt64:
+ return try ForyUInt64Tagged.foryRead(context, refMode: .none,
readTypeInfo: false)
+ case .float16:
+ return try Float16.foryRead(context, refMode: .none, readTypeInfo:
false)
+ case .bfloat16:
+ return try BFloat16.foryRead(context, refMode: .none,
readTypeInfo: false)
+ case .float32:
+ return try Float.foryRead(context, refMode: .none, readTypeInfo:
false)
+ case .float64:
+ return try Double.foryRead(context, refMode: .none, readTypeInfo:
false)
+ case .string:
+ return try String.foryRead(context, refMode: .none, readTypeInfo:
false)
+ case .duration:
+ return try Duration.foryRead(context, refMode: .none,
readTypeInfo: false)
+ case .timestamp:
+ return try Date.foryRead(context, refMode: .none, readTypeInfo:
false)
+ case .date:
+ return try ForyDate.foryRead(context, refMode: .none,
readTypeInfo: false)
+ case .binary, .uint8Array:
+ return try Data.foryRead(context, refMode: .none, readTypeInfo:
false)
+ case .boolArray:
+ return try [Bool].foryRead(context, refMode: .none, readTypeInfo:
false)
+ case .int8Array:
+ return try [Int8].foryRead(context, refMode: .none, readTypeInfo:
false)
+ case .int16Array:
+ return try [Int16].foryRead(context, refMode: .none, readTypeInfo:
false)
+ case .int32Array:
+ return try [Int32].foryRead(context, refMode: .none, readTypeInfo:
false)
+ case .int64Array:
+ return try [Int64].foryRead(context, refMode: .none, readTypeInfo:
false)
+ case .uint16Array:
+ return try [UInt16].foryRead(context, refMode: .none,
readTypeInfo: false)
+ case .uint32Array:
+ return try [UInt32].foryRead(context, refMode: .none,
readTypeInfo: false)
+ case .uint64Array:
+ return try [UInt64].foryRead(context, refMode: .none,
readTypeInfo: false)
+ case .float16Array:
+ return try [Float16].foryRead(context, refMode: .none,
readTypeInfo: false)
+ case .bfloat16Array:
+ return try [BFloat16].foryRead(context, refMode: .none,
readTypeInfo: false)
+ case .float32Array:
+ return try [Float].foryRead(context, refMode: .none, readTypeInfo:
false)
+ case .float64Array:
+ return try [Double].foryRead(context, refMode: .none,
readTypeInfo: false)
+ case .array, .list:
+ return try readCollection(context: context, fieldType: fieldType)
+ case .set:
+ return try readSet(context: context, fieldType: fieldType)
+ case .map:
+ return try readMap(context: context, fieldType: fieldType)
+ case .enumType, .namedEnum:
+ return try context.buffer.readVarUInt32()
+ default:
+ throw ForyError.invalidData("unsupported compatible field type id
\(fieldType.typeID)")
+ }
+ }
+
+ private static func readCollection(
context: ReadContext,
fieldType: TypeMetaFieldType
- ) throws -> Any? {
- let refMode = RefMode.from(nullable: fieldType.nullable, trackRef:
fieldType.trackRef)
- switch fieldType.typeID {
- case TypeId.bool.rawValue:
- return fieldType.nullable
- ? try Bool?.foryRead(context, refMode: refMode, readTypeInfo:
false)
- : try Bool.foryRead(context, refMode: refMode, readTypeInfo:
false)
- case TypeId.int8.rawValue:
- return fieldType.nullable
- ? try Int8?.foryRead(context, refMode: refMode, readTypeInfo:
false)
- : try Int8.foryRead(context, refMode: refMode, readTypeInfo:
false)
- case TypeId.int16.rawValue:
- return fieldType.nullable
- ? try Int16?.foryRead(context, refMode: refMode, readTypeInfo:
false)
- : try Int16.foryRead(context, refMode: refMode, readTypeInfo:
false)
- case TypeId.varint32.rawValue:
- return fieldType.nullable
- ? try Int32?.foryRead(context, refMode: refMode, readTypeInfo:
false)
- : try Int32.foryRead(context, refMode: refMode, readTypeInfo:
false)
- case TypeId.varint64.rawValue:
- return fieldType.nullable
- ? try Int64?.foryRead(context, refMode: refMode, readTypeInfo:
false)
- : try Int64.foryRead(context, refMode: refMode, readTypeInfo:
false)
- case TypeId.float32.rawValue:
- return fieldType.nullable
- ? try Float?.foryRead(context, refMode: refMode, readTypeInfo:
false)
- : try Float.foryRead(context, refMode: refMode, readTypeInfo:
false)
- case TypeId.float64.rawValue:
- return fieldType.nullable
- ? try Double?.foryRead(context, refMode: refMode,
readTypeInfo: false)
- : try Double.foryRead(context, refMode: refMode, readTypeInfo:
false)
- case TypeId.string.rawValue:
- return fieldType.nullable
- ? try String?.foryRead(context, refMode: refMode,
readTypeInfo: false)
- : try String.foryRead(context, refMode: refMode, readTypeInfo:
false)
- case TypeId.timestamp.rawValue:
- return fieldType.nullable
- ? try Date?.foryRead(context, refMode: refMode, readTypeInfo:
false)
- : try Date.foryRead(context, refMode: refMode, readTypeInfo:
false)
- case TypeId.date.rawValue:
- return fieldType.nullable
- ? try ForyDate?.foryRead(context, refMode: refMode,
readTypeInfo: false)
- : try ForyDate.foryRead(context, refMode: refMode,
readTypeInfo: false)
- case TypeId.list.rawValue:
- guard fieldType.generics.count == 1,
- fieldType.generics[0].typeID == TypeId.string.rawValue else {
- throw ForyError.invalidData("unsupported compatible list
element type")
+ ) throws -> [Any] {
+ let elementFieldType = fieldType.generics.first
+ ?? TypeMetaFieldType(typeID: TypeId.unknown.rawValue, nullable:
true)
+ let length = Int(try context.buffer.readVarUInt32())
+ try context.ensureCollectionLength(length, label:
"compatible_collection")
+ if length == 0 {
+ return []
+ }
+
+ let header = try context.buffer.readUInt8()
+ let trackRef = (header & 0b0000_0001) != 0
+ let hasNull = (header & 0b0000_0010) != 0
+ let declared = (header & 0b0000_0100) != 0
+ let sameType = (header & 0b0000_1000) != 0
+
+ var runtimeTypeInfo: DynamicTypeInfo?
+ if sameType, !declared {
+ runtimeTypeInfo = try
context.typeResolver.readDynamicTypeInfo(context: context)
+ }
+
+ for _ in 0..<length {
+ if sameType {
+ if trackRef {
+ _ = try readValueWithRefMode(
+ context: context,
+ fieldType: elementFieldType,
+ runtimeTypeInfo: runtimeTypeInfo,
+ refMode: .tracking,
+ readTypeInfo: false
+ )
+ } else if hasNull {
+ let refFlag = try context.buffer.readInt8()
+ if refFlag == RefFlag.null.rawValue {
+ continue
+ }
+ if refFlag != RefFlag.notNullValue.rawValue {
+ throw ForyError.invalidData("invalid collection
nullability flag \(refFlag)")
+ }
+ _ = try readFieldPayload(
+ context: context,
+ fieldType: elementFieldType,
+ runtimeTypeInfo: runtimeTypeInfo,
+ readTypeInfo: false
+ )
+ } else {
+ _ = try readFieldPayload(
+ context: context,
+ fieldType: elementFieldType,
+ runtimeTypeInfo: runtimeTypeInfo,
+ readTypeInfo: false
+ )
+ }
+ continue
}
- return fieldType.nullable
- ? try [String]?.foryRead(context, refMode: refMode,
readTypeInfo: false)
- : try [String].foryRead(context, refMode: refMode,
readTypeInfo: false)
- case TypeId.set.rawValue:
- guard fieldType.generics.count == 1,
- fieldType.generics[0].typeID == TypeId.string.rawValue else {
- throw ForyError.invalidData("unsupported compatible set
element type")
+
+ if trackRef {
+ _ = try readValueWithRefMode(
+ context: context,
+ fieldType: elementFieldType,
+ runtimeTypeInfo: nil,
+ refMode: .tracking,
+ readTypeInfo: true
+ )
+ } else if hasNull {
+ let refFlag = try context.buffer.readInt8()
+ if refFlag == RefFlag.null.rawValue {
+ continue
+ }
+ if refFlag != RefFlag.notNullValue.rawValue {
+ throw ForyError.invalidData("invalid collection
nullability flag \(refFlag)")
+ }
+ _ = try readFieldPayload(
+ context: context,
+ fieldType: elementFieldType,
+ runtimeTypeInfo: nil,
+ readTypeInfo: true
+ )
+ } else {
+ _ = try readFieldPayload(
+ context: context,
+ fieldType: elementFieldType,
+ runtimeTypeInfo: nil,
+ readTypeInfo: true
+ )
}
- return fieldType.nullable
- ? try Set<String>?.foryRead(context, refMode: refMode,
readTypeInfo: false)
- : try Set<String>.foryRead(context, refMode: refMode,
readTypeInfo: false)
- case TypeId.map.rawValue:
- guard fieldType.generics.count == 2,
- fieldType.generics[0].typeID == TypeId.string.rawValue,
- fieldType.generics[1].typeID == TypeId.string.rawValue else {
- throw ForyError.invalidData("unsupported compatible map
key/value type")
+ }
+
+ return []
+ }
+
+ private static func readSet(
+ context: ReadContext,
+ fieldType: TypeMetaFieldType
+ ) throws -> Set<AnyHashable> {
+ _ = try readCollection(context: context, fieldType: fieldType)
+ return []
+ }
+
+ private static func readMap(
+ context: ReadContext,
+ fieldType: TypeMetaFieldType
+ ) throws -> [AnyHashable: Any] {
+ let keyType = fieldType.generics.first
+ ?? TypeMetaFieldType(typeID: TypeId.unknown.rawValue, nullable:
true)
+ let valueType = fieldType.generics.dropFirst().first
+ ?? TypeMetaFieldType(typeID: TypeId.unknown.rawValue, nullable:
true)
+
+ let totalLength = Int(try context.buffer.readVarUInt32())
+ try context.ensureCollectionLength(totalLength, label:
"compatible_map")
+ if totalLength == 0 {
+ return [:]
+ }
+
+ var readCount = 0
+ while readCount < totalLength {
+ let header = try context.buffer.readUInt8()
+ let trackKeyRef = (header & 0b0000_0001) != 0
+ let keyNull = (header & 0b0000_0010) != 0
+ let keyDeclared = (header & 0b0000_0100) != 0
+
+ let trackValueRef = (header & 0b0000_1000) != 0
+ let valueNull = (header & 0b0001_0000) != 0
+ let valueDeclared = (header & 0b0010_0000) != 0
+
+ if keyNull && valueNull {
+ readCount += 1
+ continue
}
- return fieldType.nullable
- ? try [String: String]?.foryRead(context, refMode: refMode,
readTypeInfo: false)
- : try [String: String].foryRead(context, refMode: refMode,
readTypeInfo: false)
- case TypeId.enumType.rawValue:
- return try readEnumOrdinal(context, refMode: refMode)
- default:
- throw ForyError.invalidData("unsupported compatible field type id
\(fieldType.typeID)")
+
+ if keyNull {
+ let valueRuntimeType = valueDeclared ? nil : try
context.typeResolver.readDynamicTypeInfo(context: context)
+ _ = try readValueWithRefMode(
+ context: context,
+ fieldType: valueType,
+ runtimeTypeInfo: valueRuntimeType,
+ refMode: trackValueRef ? .tracking : .none,
+ readTypeInfo: false
+ )
+ readCount += 1
+ continue
+ }
+
+ if valueNull {
+ let keyRuntimeType = keyDeclared ? nil : try
context.typeResolver.readDynamicTypeInfo(context: context)
+ _ = try readValueWithRefMode(
+ context: context,
+ fieldType: keyType,
+ runtimeTypeInfo: keyRuntimeType,
+ refMode: trackKeyRef ? .tracking : .none,
+ readTypeInfo: false
+ )
+ readCount += 1
+ continue
+ }
+
+ let chunkSize = Int(try context.buffer.readUInt8())
+ if chunkSize <= 0 {
+ throw ForyError.invalidData("invalid map chunk size
\(chunkSize)")
+ }
+ if chunkSize > (totalLength - readCount) {
+ throw ForyError.invalidData("map chunk size exceeds remaining
entries")
+ }
+
+ let keyRuntimeType = keyDeclared ? nil : try
context.typeResolver.readDynamicTypeInfo(context: context)
+ let valueRuntimeType = valueDeclared ? nil : try
context.typeResolver.readDynamicTypeInfo(context: context)
+
+ for _ in 0..<chunkSize {
+ _ = try readValueWithRefMode(
+ context: context,
+ fieldType: keyType,
+ runtimeTypeInfo: keyRuntimeType,
+ refMode: trackKeyRef ? .tracking : .none,
+ readTypeInfo: false
+ )
+ _ = try readValueWithRefMode(
+ context: context,
+ fieldType: valueType,
+ runtimeTypeInfo: valueRuntimeType,
+ refMode: trackValueRef ? .tracking : .none,
+ readTypeInfo: false
+ )
+ }
+ readCount += chunkSize
}
+
+ return [:]
}
}
diff --git a/swift/Sources/Fory/Fory.swift b/swift/Sources/Fory/Fory.swift
index 99019aeca..9edb7a6c5 100644
--- a/swift/Sources/Fory/Fory.swift
+++ b/swift/Sources/Fory/Fory.swift
@@ -22,17 +22,26 @@ public struct ForyConfig {
public var trackRef: Bool
public var compatible: Bool
public var checkClassVersion: Bool
+ public var maxCollectionLength: Int
+ public var maxBinaryLength: Int
+ public var maxDepth: Int
public init(
xlang: Bool = true,
trackRef: Bool = false,
compatible: Bool = false,
- checkClassVersion: Bool = true
+ checkClassVersion: Bool = true,
+ maxCollectionLength: Int = 1_000_000,
+ maxBinaryLength: Int = 64 * 1024 * 1024,
+ maxDepth: Int = 5
) {
self.xlang = xlang
self.trackRef = trackRef
self.compatible = compatible
self.checkClassVersion = checkClassVersion
+ self.maxCollectionLength = maxCollectionLength
+ self.maxBinaryLength = maxBinaryLength
+ self.maxDepth = maxDepth
}
}
@@ -56,6 +65,7 @@ private final class ForyRuntimeContext {
trackRef: config.trackRef,
compatible: config.compatible,
checkClassVersion: config.checkClassVersion,
+ maxDepth: config.maxDepth,
compatibleTypeDefState: CompatibleTypeDefWriteState(),
metaStringWriteState: MetaStringWriteState()
)
@@ -67,6 +77,9 @@ private final class ForyRuntimeContext {
trackRef: config.trackRef,
compatible: config.compatible,
checkClassVersion: config.checkClassVersion,
+ maxCollectionLength: config.maxCollectionLength,
+ maxBinaryLength: config.maxBinaryLength,
+ maxDepth: config.maxDepth,
compatibleTypeDefState: CompatibleTypeDefReadState(),
metaStringReadState: MetaStringReadState()
)
@@ -138,14 +151,20 @@ public final class Fory {
xlang: Bool = true,
trackRef: Bool = false,
compatible: Bool = false,
- checkClassVersion: Bool? = nil
+ checkClassVersion: Bool? = nil,
+ maxCollectionLength: Int = 1_000_000,
+ maxBinaryLength: Int = 64 * 1024 * 1024,
+ maxDepth: Int = 5
) {
let effectiveCheckClassVersion = checkClassVersion ?? (xlang &&
!compatible)
self.config = ForyConfig(
xlang: xlang,
trackRef: trackRef,
compatible: compatible,
- checkClassVersion: effectiveCheckClassVersion
+ checkClassVersion: effectiveCheckClassVersion,
+ maxCollectionLength: maxCollectionLength,
+ maxBinaryLength: maxBinaryLength,
+ maxDepth: maxDepth
)
self.typeResolver = TypeResolver()
self.instanceID = Self.allocateInstanceID()
@@ -156,7 +175,10 @@ public final class Fory {
xlang: config.xlang,
trackRef: config.trackRef,
compatible: config.compatible,
- checkClassVersion: config.checkClassVersion
+ checkClassVersion: config.checkClassVersion,
+ maxCollectionLength: config.maxCollectionLength,
+ maxBinaryLength: config.maxBinaryLength,
+ maxDepth: config.maxDepth
)
}
@@ -534,6 +556,7 @@ public final class Fory {
trackRef: config.trackRef,
compatible: config.compatible,
checkClassVersion: config.checkClassVersion,
+ maxDepth: config.maxDepth,
compatibleTypeDefState: CompatibleTypeDefWriteState(),
metaStringWriteState: MetaStringWriteState()
)
@@ -547,6 +570,9 @@ public final class Fory {
trackRef: config.trackRef,
compatible: config.compatible,
checkClassVersion: config.checkClassVersion,
+ maxCollectionLength: config.maxCollectionLength,
+ maxBinaryLength: config.maxBinaryLength,
+ maxDepth: config.maxDepth,
compatibleTypeDefState: CompatibleTypeDefReadState(),
metaStringReadState: MetaStringReadState()
)
@@ -602,20 +628,10 @@ public final class Fory {
}
runtimeContext.readInUse = true
- let shouldReplace = data.withUnsafeBytes { rawBytes in
- if rawBytes.count != runtimeContext.lastReadDataCount {
- return true
- }
- return rawBytes.baseAddress != runtimeContext.lastReadDataAddress
- }
- if shouldReplace {
- runtimeContext.readBuffer.replace(with: data)
- data.withUnsafeBytes { rawBytes in
- runtimeContext.lastReadDataAddress = rawBytes.baseAddress
- runtimeContext.lastReadDataCount = rawBytes.count
- }
- } else {
- runtimeContext.readBuffer.setCursor(0)
+ runtimeContext.readBuffer.replace(with: data)
+ data.withUnsafeBytes { rawBytes in
+ runtimeContext.lastReadDataAddress = rawBytes.baseAddress
+ runtimeContext.lastReadDataCount = rawBytes.count
}
defer {
runtimeContext.readContext.reset()
@@ -673,7 +689,11 @@ public final class Fory {
if try readHead(buffer: context.buffer) {
return nilValue()
}
- return try body(context)
+ let value = try body(context)
+ if context.buffer.remaining != 0 {
+ throw ForyError.invalidData("unexpected trailing bytes at
root: \(context.buffer.remaining)")
+ }
+ return value
}
}
diff --git a/swift/Sources/Fory/MetaString.swift
b/swift/Sources/Fory/MetaString.swift
index cc128fe2a..72dabec5e 100644
--- a/swift/Sources/Fory/MetaString.swift
+++ b/swift/Sources/Fory/MetaString.swift
@@ -57,13 +57,16 @@ public struct MetaString: Equatable, Hashable, Sendable {
}
public static func empty(specialChar1: Character, specialChar2: Character)
-> MetaString {
- try! MetaString(
+ guard let emptyMetaString = try? MetaString(
value: "",
encoding: .utf8,
specialChar1: specialChar1,
specialChar2: specialChar2,
bytes: []
- )
+ ) else {
+ preconditionFailure("failed to create empty MetaString")
+ }
+ return emptyMetaString
}
}
@@ -176,11 +179,11 @@ public struct MetaStringEncoder: Sendable {
var canLowerUpperDigitSpecial = true
for scalar in input.unicodeScalars {
- let c = Character(scalar)
+ let character = Character(scalar)
if canLowerSpecial {
let isValid =
(scalar.value >= 97 && scalar.value <= 122) ||
- c == "." || c == "_" || c == "$" || c == "|"
+ character == "." || character == "_" || character == "$"
|| character == "|"
if !isValid {
canLowerSpecial = false
}
@@ -189,7 +192,7 @@ public struct MetaStringEncoder: Sendable {
let isLower = scalar.value >= 97 && scalar.value <= 122
let isUpper = scalar.value >= 65 && scalar.value <= 90
let isDigit = scalar.value >= 48 && scalar.value <= 57
- let isSpecial = c == specialChar1 || c == specialChar2
+ let isSpecial = character == specialChar1 || character ==
specialChar2
if !(isLower || isUpper || isDigit || isSpecial) {
canLowerUpperDigitSpecial = false
}
@@ -211,8 +214,7 @@ public struct MetaStringEncoder: Sendable {
}
if upperCount == 1,
input.first?.isUppercase == true,
- allow(.firstToLowerSpecial)
- {
+ allow(.firstToLowerSpecial) {
return .firstToLowerSpecial
}
if ((input.count + upperCount) * 5) < (input.count * 6),
allow(.allToLowerSpecial) {
@@ -236,10 +238,10 @@ public struct MetaStringEncoder: Sendable {
var bytes = Array(repeating: UInt8(0), count: byteLength)
var currentBit = 1
- for c in chars {
- let value = try mapper(c)
- for i in stride(from: bitsPerChar - 1, through: 0, by: -1) {
- if ((value >> UInt8(i)) & 0x01) != 0 {
+ for character in chars {
+ let value = try mapper(character)
+ for bitOffset in stride(from: bitsPerChar - 1, through: 0, by: -1)
{
+ if ((value >> UInt8(bitOffset)) & 0x01) != 0 {
let bytePos = currentBit / 8
let bitPos = currentBit % 8
bytes[bytePos] |= UInt8(1 << (7 - bitPos))
@@ -254,14 +256,14 @@ public struct MetaStringEncoder: Sendable {
return bytes
}
- private func mapLowerSpecial(_ c: Character) throws -> UInt8 {
- guard let scalar = c.unicodeScalars.first, c.unicodeScalars.count == 1
else {
+ private func mapLowerSpecial(_ character: Character) throws -> UInt8 {
+ guard let scalar = character.unicodeScalars.first,
character.unicodeScalars.count == 1 else {
throw ForyError.encodingError("unsupported character in
LOWER_SPECIAL")
}
if scalar.value >= 97 && scalar.value <= 122 {
return UInt8(scalar.value - 97)
}
- switch c {
+ switch character {
case ".": return 26
case "_": return 27
case "$": return 28
@@ -271,8 +273,8 @@ public struct MetaStringEncoder: Sendable {
}
}
- private func mapLowerUpperDigitSpecial(_ c: Character) throws -> UInt8 {
- guard let scalar = c.unicodeScalars.first, c.unicodeScalars.count == 1
else {
+ private func mapLowerUpperDigitSpecial(_ character: Character) throws ->
UInt8 {
+ guard let scalar = character.unicodeScalars.first,
character.unicodeScalars.count == 1 else {
throw ForyError.encodingError("unsupported character in
LOWER_UPPER_DIGIT_SPECIAL")
}
if scalar.value >= 97 && scalar.value <= 122 {
@@ -284,10 +286,10 @@ public struct MetaStringEncoder: Sendable {
if scalar.value >= 48 && scalar.value <= 57 {
return UInt8(52 + scalar.value - 48)
}
- if c == specialChar1 {
+ if character == specialChar1 {
return 62
}
- if c == specialChar2 {
+ if character == specialChar2 {
return 63
}
throw ForyError.encodingError("unsupported character in
LOWER_UPPER_DIGIT_SPECIAL")
@@ -304,12 +306,12 @@ public struct MetaStringEncoder: Sendable {
private func escapeAllUpper(_ input: String) -> String {
var out = String()
out.reserveCapacity(input.count * 2)
- for c in input {
- if c.isUppercase {
+ for character in input {
+ if character.isUppercase {
out.append("|")
- out.append(String(c).lowercased())
+ out.append(String(character).lowercased())
} else {
- out.append(c)
+ out.append(character)
}
}
return out
@@ -340,7 +342,10 @@ public struct MetaStringDecoder: Sendable {
let value: String
switch encoding {
case .utf8:
- value = String(decoding: bytes, as: UTF8.self)
+ guard let decoded = String(bytes: bytes, encoding: .utf8) else {
+ throw ForyError.encodingError("invalid UTF-8 meta string
payload")
+ }
+ value = decoded
case .lowerSpecial:
value = try decodeGeneric(bytes: bytes, bitsPerChar: 5, mapper:
unmapLowerSpecial)
case .lowerUpperDigitSpecial:
@@ -431,12 +436,12 @@ public struct MetaStringDecoder: Sendable {
private func unescapeAllUpper(_ input: String) -> String {
var out = String()
out.reserveCapacity(input.count)
- var it = input.makeIterator()
- while let c = it.next() {
- if c == "|", let next = it.next() {
- out.append(String(next).uppercased())
+ var iterator = input.makeIterator()
+ while let currentCharacter = iterator.next() {
+ if currentCharacter == "|", let nextCharacter = iterator.next() {
+ out.append(String(nextCharacter).uppercased())
} else {
- out.append(c)
+ out.append(currentCharacter)
}
}
return out
diff --git a/swift/Sources/Fory/MurmurHash3.swift
b/swift/Sources/Fory/MurmurHash3.swift
index bbde19a19..aef3c9d48 100644
--- a/swift/Sources/Fory/MurmurHash3.swift
+++ b/swift/Sources/Fory/MurmurHash3.swift
@@ -29,8 +29,8 @@ enum MurmurHash3 {
let nblocks = length / 16
if nblocks > 0 {
- for i in 0..<nblocks {
- let base = i * 16
+ for blockIndex in 0..<nblocks {
+ let base = blockIndex * 16
var k1 = readUInt64LE(bytes, offset: base)
var k2 = readUInt64LE(bytes, offset: base + 8)
@@ -142,17 +142,17 @@ enum MurmurHash3 {
return value
}
- private static func rotl64(_ x: UInt64, _ r: UInt64) -> UInt64 {
- (x << r) | (x >> (64 - r))
+ private static func rotl64(_ value: UInt64, _ rotationBits: UInt64) ->
UInt64 {
+ (value << rotationBits) | (value >> (64 - rotationBits))
}
private static func fmix64(_ value: UInt64) -> UInt64 {
- var x = value
- x ^= x >> 33
- x &*= 0xff51afd7ed558ccd
- x ^= x >> 33
- x &*= 0xc4ceb9fe1a85ec53
- x ^= x >> 33
- return x
+ var mixed = value
+ mixed ^= mixed >> 33
+ mixed &*= 0xff51afd7ed558ccd
+ mixed ^= mixed >> 33
+ mixed &*= 0xc4ceb9fe1a85ec53
+ mixed ^= mixed >> 33
+ return mixed
}
}
diff --git a/swift/Sources/Fory/OptionalSerializer.swift
b/swift/Sources/Fory/OptionalSerializer.swift
index bad89c615..70f38bcf7 100644
--- a/swift/Sources/Fory/OptionalSerializer.swift
+++ b/swift/Sources/Fory/OptionalSerializer.swift
@@ -18,7 +18,7 @@
import Foundation
extension Optional: Serializer where Wrapped: Serializer {
- public static func foryDefault() -> Optional<Wrapped> {
+ public static func foryDefault() -> Wrapped? {
nil
}
@@ -45,7 +45,7 @@ extension Optional: Serializer where Wrapped: Serializer {
try wrapped.foryWriteData(context, hasGenerics: hasGenerics)
}
- public static func foryReadData(_ context: ReadContext) throws ->
Optional<Wrapped> {
+ public static func foryReadData(_ context: ReadContext) throws -> Wrapped?
{
.some(try Wrapped.foryReadData(context))
}
@@ -89,7 +89,7 @@ extension Optional: Serializer where Wrapped: Serializer {
_ context: ReadContext,
refMode: RefMode,
readTypeInfo: Bool
- ) throws -> Optional<Wrapped> {
+ ) throws -> Wrapped? {
switch refMode {
case .none:
return .some(try Wrapped.foryRead(context, refMode: .none,
readTypeInfo: readTypeInfo))
diff --git a/swift/Sources/Fory/PrimitiveSerializers.swift
b/swift/Sources/Fory/PrimitiveSerializers.swift
index c2531c45d..b1b010612 100644
--- a/swift/Sources/Fory/PrimitiveSerializers.swift
+++ b/swift/Sources/Fory/PrimitiveSerializers.swift
@@ -315,6 +315,41 @@ extension Double: Serializer {
}
}
+public struct BFloat16: Serializer, Equatable, Hashable, Sendable {
+ public var rawValue: UInt16
+
+ public init(rawValue: UInt16 = 0) {
+ self.rawValue = rawValue
+ }
+
+ public static func foryDefault() -> BFloat16 { .init() }
+ public static var staticTypeId: TypeId { .bfloat16 }
+
+ public func foryWriteData(_ context: WriteContext, hasGenerics: Bool)
throws {
+ _ = hasGenerics
+ context.buffer.writeUInt16(rawValue)
+ }
+
+ public static func foryReadData(_ context: ReadContext) throws -> BFloat16
{
+ .init(rawValue: try context.buffer.readUInt16())
+ }
+}
+
+extension Float16: Serializer {
+ public static var staticTypeId: TypeId { .float16 }
+
+ public static func foryDefault() -> Float16 { 0 }
+
+ public func foryWriteData(_ context: WriteContext, hasGenerics: Bool)
throws {
+ _ = hasGenerics
+ context.buffer.writeUInt16(bitPattern)
+ }
+
+ public static func foryReadData(_ context: ReadContext) throws -> Float16 {
+ Float16(bitPattern: try context.buffer.readUInt16())
+ }
+}
+
private enum StringEncoding: UInt64 {
case latin1 = 0
case utf16 = 1
@@ -386,7 +421,10 @@ extension Data: Serializer {
public static func foryReadData(_ context: ReadContext) throws -> Data {
let length = try context.buffer.readVarUInt32()
- let bytes = try context.buffer.readBytes(count: Int(length))
+ let byteLength = Int(length)
+ try context.ensureBinaryLength(byteLength, label: "binary")
+ try context.ensureRemainingBytes(byteLength, label: "binary")
+ let bytes = try context.buffer.readBytes(count: byteLength)
return Data(bytes)
}
}
diff --git a/swift/Sources/Fory/TypeMeta.swift
b/swift/Sources/Fory/TypeMeta.swift
index d85b61f7c..4e4585aa0 100644
--- a/swift/Sources/Fory/TypeMeta.swift
+++ b/swift/Sources/Fory/TypeMeta.swift
@@ -32,20 +32,20 @@ private let noUserTypeID: UInt32 = UInt32.max
public let namespaceMetaStringEncodings: [MetaStringEncoding] = [
.utf8,
.allToLowerSpecial,
- .lowerUpperDigitSpecial,
+ .lowerUpperDigitSpecial
]
public let typeNameMetaStringEncodings: [MetaStringEncoding] = [
.utf8,
.allToLowerSpecial,
.lowerUpperDigitSpecial,
- .firstToLowerSpecial,
+ .firstToLowerSpecial
]
public let fieldNameMetaStringEncodings: [MetaStringEncoding] = [
.utf8,
.allToLowerSpecial,
- .lowerUpperDigitSpecial,
+ .lowerUpperDigitSpecial
]
public struct TypeMetaFieldType: Equatable, Sendable {
@@ -372,6 +372,11 @@ public struct TypeMeta: Equatable, Sendable {
}
var fieldInfos: [TypeMetaFieldInfo] = []
+ if numFields > bodyReader.remaining {
+ throw ForyError.invalidData(
+ "type meta field count \(numFields) exceeds remaining bytes
\(bodyReader.remaining)"
+ )
+ }
fieldInfos.reserveCapacity(numFields)
for _ in 0..<numFields {
fieldInfos.append(try TypeMetaFieldInfo.read(bodyReader))
diff --git a/swift/Sources/Fory/TypeResolver.swift
b/swift/Sources/Fory/TypeResolver.swift
index 36c2c4e09..b63dd3a48 100644
--- a/swift/Sources/Fory/TypeResolver.swift
+++ b/swift/Sources/Fory/TypeResolver.swift
@@ -17,7 +17,7 @@
import Foundation
-public struct RegisteredTypeInfo {
+public struct RegisteredTypeInfo: Equatable {
public let userTypeID: UInt32?
public let kind: TypeId
public let registerByName: Bool
@@ -51,6 +51,7 @@ private enum DynamicRegistrationMode {
}
private struct TypeReader {
+ let swiftType: ObjectIdentifier
let kind: TypeId
let reader: (ReadContext) throws -> Any
let compatibleReader: (ReadContext, TypeMeta) throws -> Any
@@ -65,7 +66,16 @@ public final class TypeResolver {
public init() {}
public func register<T: Serializer>(_ type: T.Type, id: UInt32) {
+ do {
+ try registerByID(type, id: id)
+ } catch {
+ preconditionFailure("conflicting registration for \(type):
\(error)")
+ }
+ }
+
+ private func registerByID<T: Serializer>(_ type: T.Type, id: UInt32)
throws {
let key = ObjectIdentifier(type)
+ try validateIDRegistration(key: key, type: type, id: id)
let info = RegisteredTypeInfo(
userTypeID: id,
kind: T.staticTypeId,
@@ -73,9 +83,13 @@ public final class TypeResolver {
namespace: nil,
typeName: MetaString.empty(specialChar1: "$", specialChar2: "_")
)
+ if bySwiftType[key] == info {
+ return
+ }
bySwiftType[key] = info
markRegistrationMode(kind: info.kind, registerByName: false)
byUserTypeID[id] = TypeReader(
+ swiftType: key,
kind: T.staticTypeId,
reader: { context in
try T.foryRead(context, refMode: .none, readTypeInfo: false)
@@ -97,6 +111,12 @@ public final class TypeResolver {
allowedEncodings: typeNameMetaStringEncodings
)
let key = ObjectIdentifier(type)
+ try validateNameRegistration(
+ key: key,
+ type: type,
+ namespace: namespace,
+ typeName: typeName
+ )
let info = RegisteredTypeInfo(
userTypeID: nil,
kind: T.staticTypeId,
@@ -104,9 +124,13 @@ public final class TypeResolver {
namespace: namespaceMeta,
typeName: typeNameMeta
)
+ if bySwiftType[key] == info {
+ return
+ }
bySwiftType[key] = info
markRegistrationMode(kind: info.kind, registerByName: true)
byTypeName[TypeNameKey(namespace: namespace, typeName: typeName)] =
TypeReader(
+ swiftType: key,
kind: T.staticTypeId,
reader: { context in
try T.foryRead(context, refMode: .none, readTypeInfo: false)
@@ -254,7 +278,14 @@ public final class TypeResolver {
compatibleTypeMeta: nil
)
case .mixed:
- throw ForyError.invalidData("ambiguous dynamic type
registration mode for \(wireTypeID)")
+ // Wire ids for user kinds are explicit: plain ids always
carry user_type_id.
+ return DynamicTypeInfo(
+ wireTypeID: wireTypeID,
+ userTypeID: try context.buffer.readVarUInt32(),
+ namespace: nil,
+ typeName: nil,
+ compatibleTypeMeta: nil
+ )
}
default:
return DynamicTypeInfo(
@@ -268,6 +299,9 @@ public final class TypeResolver {
}
public func readDynamicValue(typeInfo: DynamicTypeInfo, context:
ReadContext) throws -> Any {
+ try context.enterDynamicAnyDepth()
+ defer { context.leaveDynamicAnyDepth() }
+
let value: Any
switch typeInfo.wireTypeID {
case .bool:
@@ -300,12 +334,18 @@ public final class TypeResolver {
value = try UInt64.foryRead(context, refMode: .none, readTypeInfo:
false)
case .taggedUInt64:
value = try ForyUInt64Tagged.foryRead(context, refMode: .none,
readTypeInfo: false)
+ case .float16:
+ value = try Float16.foryRead(context, refMode: .none,
readTypeInfo: false)
+ case .bfloat16:
+ value = try BFloat16.foryRead(context, refMode: .none,
readTypeInfo: false)
case .float32:
value = try Float.foryRead(context, refMode: .none, readTypeInfo:
false)
case .float64:
value = try Double.foryRead(context, refMode: .none, readTypeInfo:
false)
case .string:
value = try String.foryRead(context, refMode: .none, readTypeInfo:
false)
+ case .duration:
+ value = try Duration.foryRead(context, refMode: .none,
readTypeInfo: false)
case .timestamp:
value = try Date.foryRead(context, refMode: .none, readTypeInfo:
false)
case .date:
@@ -328,12 +368,18 @@ public final class TypeResolver {
value = try [UInt32].foryRead(context, refMode: .none,
readTypeInfo: false)
case .uint64Array:
value = try [UInt64].foryRead(context, refMode: .none,
readTypeInfo: false)
+ case .float16Array:
+ value = try [Float16].foryRead(context, refMode: .none,
readTypeInfo: false)
+ case .bfloat16Array:
+ value = try [BFloat16].foryRead(context, refMode: .none,
readTypeInfo: false)
case .float32Array:
value = try [Float].foryRead(context, refMode: .none,
readTypeInfo: false)
case .float64Array:
value = try [Double].foryRead(context, refMode: .none,
readTypeInfo: false)
- case .list:
+ case .array, .list:
value = try context.readAnyList(refMode: .none) ?? []
+ case .set:
+ value = try Set<AnyHashable>.foryRead(context, refMode: .none,
readTypeInfo: false)
case .map:
value = try readDynamicAnyMapValue(context: context)
case .structType, .enumType, .ext, .typedUnion:
@@ -404,6 +450,60 @@ public final class TypeResolver {
return mode
}
+ private func validateIDRegistration<T: Serializer>(
+ key: ObjectIdentifier,
+ type: T.Type,
+ id: UInt32
+ ) throws {
+ if let existing = bySwiftType[key] {
+ if existing.registerByName {
+ throw ForyError.invalidData(
+ "\(type) was already registered by name, cannot
re-register by id"
+ )
+ }
+ if existing.kind != T.staticTypeId || existing.userTypeID != id {
+ let existingID = existing.userTypeID.map { String($0) } ??
"nil"
+ throw ForyError.invalidData(
+ "\(type) registration conflict: existing id=\(existingID),
new id=\(id)"
+ )
+ }
+ }
+
+ if let existing = byUserTypeID[id], existing.swiftType != key {
+ throw ForyError.invalidData("user type id \(id) is already
registered by another type")
+ }
+ }
+
+ private func validateNameRegistration<T: Serializer>(
+ key: ObjectIdentifier,
+ type: T.Type,
+ namespace: String,
+ typeName: String
+ ) throws {
+ if let existing = bySwiftType[key] {
+ if !existing.registerByName {
+ throw ForyError.invalidData(
+ "\(type) was already registered by id, cannot re-register
by name"
+ )
+ }
+ if existing.kind != T.staticTypeId ||
+ existing.namespace?.value != namespace ||
+ existing.typeName.value != typeName {
+ throw ForyError.invalidData(
+ """
+ \(type) registration conflict: existing
name=\(existing.namespace?.value ?? "")::\(existing.typeName.value), \
+ new name=\(namespace)::\(typeName)
+ """
+ )
+ }
+ }
+
+ let nameKey = TypeNameKey(namespace: namespace, typeName: typeName)
+ if let existing = byTypeName[nameKey], existing.swiftType != key {
+ throw ForyError.invalidData("type name \(namespace)::\(typeName)
is already registered by another type")
+ }
+ }
+
private static func readMetaString(
buffer: ByteBuffer,
decoder: MetaStringDecoder,
diff --git a/swift/Sources/ForyMacro/ForyObjectMacro.swift
b/swift/Sources/ForyMacro/ForyObjectMacro.swift
index dd951b6aa..c29a2f252 100644
--- a/swift/Sources/ForyMacro/ForyObjectMacro.swift
+++ b/swift/Sources/ForyMacro/ForyObjectMacro.swift
@@ -68,7 +68,7 @@ public struct ForyObjectMacro: MemberMacro, ExtensionMacro {
writeWrapperDecl,
readWrapperDecl,
writeDecl,
- readDecl,
+ readDecl
].compactMap { $0 }
}
@@ -972,7 +972,13 @@ private func buildWriteDataDecl(sortedFields:
[ParsedField]) -> String {
}
let schemaBody = schemaBodyLines.joined(separator: "\n ")
- let compatibleWriteLines = compatibleLines.isEmpty ? "\n _ =
hasGenerics" : "\n \(compatibleLines.joined(separator: "\n
"))"
+ let compatibleWriteLines: String
+ if compatibleLines.isEmpty {
+ compatibleWriteLines = "\n _ = hasGenerics"
+ } else {
+ let joinedCompatibleLines = compatibleLines.joined(separator: "\n
")
+ compatibleWriteLines = "\n \(joinedCompatibleLines)"
+ }
return """
@inlinable
@@ -1045,9 +1051,23 @@ private func schemaWriteLine(for field: ParsedField) ->
String {
if let codecType = field.customCodecType {
let refMode = fieldRefModeExpression(field)
if field.isOptional {
- return "try (self.\(field.name).map { \(codecType)(rawValue: $0)
}).foryWrite(context, refMode: \(refMode), writeTypeInfo: false, hasGenerics:
false)"
+ return """
+ try (self.\(field.name).map { \(codecType)(rawValue: $0)
}).foryWrite(
+ context,
+ refMode: \(refMode),
+ writeTypeInfo: false,
+ hasGenerics: false
+ )
+ """
}
- return "try \(codecType)(rawValue:
self.\(field.name)).foryWrite(context, refMode: \(refMode), writeTypeInfo:
false, hasGenerics: false)"
+ return """
+ try \(codecType)(rawValue: self.\(field.name)).foryWrite(
+ context,
+ refMode: \(refMode),
+ writeTypeInfo: false,
+ hasGenerics: false
+ )
+ """
}
if !field.isOptional, field.typeID != 27 {
if let primitiveLine = primitiveSchemaWriteLine(field) {
@@ -1071,11 +1091,32 @@ private func compatibleWriteLine(for field:
ParsedField) -> String {
let hasGenerics = field.isCollection ? "true" : "false"
if let codecType = field.customCodecType {
if field.isOptional {
- return "try (self.\(field.name).map { \(codecType)(rawValue: $0)
}).foryWrite(context, refMode: \(refMode), writeTypeInfo: false, hasGenerics:
false)"
+ return """
+ try (self.\(field.name).map { \(codecType)(rawValue: $0)
}).foryWrite(
+ context,
+ refMode: \(refMode),
+ writeTypeInfo: false,
+ hasGenerics: false
+ )
+ """
}
- return "try \(codecType)(rawValue:
self.\(field.name)).foryWrite(context, refMode: \(refMode), writeTypeInfo:
false, hasGenerics: false)"
+ return """
+ try \(codecType)(rawValue: self.\(field.name)).foryWrite(
+ context,
+ refMode: \(refMode),
+ writeTypeInfo: false,
+ hasGenerics: false
+ )
+ """
}
- return "try self.\(field.name).foryWrite(context, refMode: \(refMode),
writeTypeInfo: TypeId.needsTypeInfoForField(\(field.typeText).staticTypeId),
hasGenerics: \(hasGenerics))"
+ return """
+ try self.\(field.name).foryWrite(
+ context,
+ refMode: \(refMode),
+ writeTypeInfo:
TypeId.needsTypeInfoForField(\(field.typeText).staticTypeId),
+ hasGenerics: \(hasGenerics)
+ )
+ """
}
private func buildReadDataDecl(isClass: Bool, fields: [ParsedField],
sortedFields: [ParsedField]) -> String {
@@ -1583,16 +1624,66 @@ private func classifyType(_ typeText: String) ->
TypeClassification {
if elem.typeID == 9 { // UInt8
return .init(typeID: 41, isPrimitive: false, isBuiltIn: true,
isCollection: true, isMap: false, isCompressedNumeric: false, primitiveSize: 0)
}
- if elem.typeID == 1 { return .init(typeID: 43, isPrimitive: false,
isBuiltIn: true, isCollection: true, isMap: false, isCompressedNumeric: false,
primitiveSize: 0) }
- if elem.typeID == 2 { return .init(typeID: 44, isPrimitive: false,
isBuiltIn: true, isCollection: true, isMap: false, isCompressedNumeric: false,
primitiveSize: 0) }
- if elem.typeID == 3 { return .init(typeID: 45, isPrimitive: false,
isBuiltIn: true, isCollection: true, isMap: false, isCompressedNumeric: false,
primitiveSize: 0) }
- if elem.typeID == 5 { return .init(typeID: 46, isPrimitive: false,
isBuiltIn: true, isCollection: true, isMap: false, isCompressedNumeric: false,
primitiveSize: 0) }
- if elem.typeID == 7 { return .init(typeID: 47, isPrimitive: false,
isBuiltIn: true, isCollection: true, isMap: false, isCompressedNumeric: false,
primitiveSize: 0) }
- if elem.typeID == 10 { return .init(typeID: 49, isPrimitive: false,
isBuiltIn: true, isCollection: true, isMap: false, isCompressedNumeric: false,
primitiveSize: 0) }
- if elem.typeID == 12 { return .init(typeID: 50, isPrimitive: false,
isBuiltIn: true, isCollection: true, isMap: false, isCompressedNumeric: false,
primitiveSize: 0) }
- if elem.typeID == 14 { return .init(typeID: 51, isPrimitive: false,
isBuiltIn: true, isCollection: true, isMap: false, isCompressedNumeric: false,
primitiveSize: 0) }
- if elem.typeID == 19 { return .init(typeID: 55, isPrimitive: false,
isBuiltIn: true, isCollection: true, isMap: false, isCompressedNumeric: false,
primitiveSize: 0) }
- if elem.typeID == 20 { return .init(typeID: 56, isPrimitive: false,
isBuiltIn: true, isCollection: true, isMap: false, isCompressedNumeric: false,
primitiveSize: 0) }
+ if elem.typeID == 1 {
+ return .init(
+ typeID: 43, isPrimitive: false, isBuiltIn: true, isCollection:
true, isMap: false,
+ isCompressedNumeric: false, primitiveSize: 0
+ )
+ }
+ if elem.typeID == 2 {
+ return .init(
+ typeID: 44, isPrimitive: false, isBuiltIn: true, isCollection:
true, isMap: false,
+ isCompressedNumeric: false, primitiveSize: 0
+ )
+ }
+ if elem.typeID == 3 {
+ return .init(
+ typeID: 45, isPrimitive: false, isBuiltIn: true, isCollection:
true, isMap: false,
+ isCompressedNumeric: false, primitiveSize: 0
+ )
+ }
+ if elem.typeID == 5 {
+ return .init(
+ typeID: 46, isPrimitive: false, isBuiltIn: true, isCollection:
true, isMap: false,
+ isCompressedNumeric: false, primitiveSize: 0
+ )
+ }
+ if elem.typeID == 7 {
+ return .init(
+ typeID: 47, isPrimitive: false, isBuiltIn: true, isCollection:
true, isMap: false,
+ isCompressedNumeric: false, primitiveSize: 0
+ )
+ }
+ if elem.typeID == 10 {
+ return .init(
+ typeID: 49, isPrimitive: false, isBuiltIn: true, isCollection:
true, isMap: false,
+ isCompressedNumeric: false, primitiveSize: 0
+ )
+ }
+ if elem.typeID == 12 {
+ return .init(
+ typeID: 50, isPrimitive: false, isBuiltIn: true, isCollection:
true, isMap: false,
+ isCompressedNumeric: false, primitiveSize: 0
+ )
+ }
+ if elem.typeID == 14 {
+ return .init(
+ typeID: 51, isPrimitive: false, isBuiltIn: true, isCollection:
true, isMap: false,
+ isCompressedNumeric: false, primitiveSize: 0
+ )
+ }
+ if elem.typeID == 19 {
+ return .init(
+ typeID: 55, isPrimitive: false, isBuiltIn: true, isCollection:
true, isMap: false,
+ isCompressedNumeric: false, primitiveSize: 0
+ )
+ }
+ if elem.typeID == 20 {
+ return .init(
+ typeID: 56, isPrimitive: false, isBuiltIn: true, isCollection:
true, isMap: false,
+ isCompressedNumeric: false, primitiveSize: 0
+ )
+ }
return .init(typeID: 22, isPrimitive: false, isBuiltIn: true,
isCollection: true, isMap: false, isCompressedNumeric: false, primitiveSize: 0)
}
diff --git a/swift/Tests/ForyTests/AnyTests.swift
b/swift/Tests/ForyTests/AnyTests.swift
index a90b9f6bc..ca30be8e5 100644
--- a/swift/Tests/ForyTests/AnyTests.swift
+++ b/swift/Tests/ForyTests/AnyTests.swift
@@ -44,7 +44,7 @@ private final class AnyObjectDynamicNode {
@ForyObject
private struct AnyHashableMapHolder {
var map: [AnyHashable: Any] = [:]
- var optionalMap: [AnyHashable: Any]? = nil
+ var optionalMap: [AnyHashable: Any]?
}
@ForyObject
@@ -60,7 +60,7 @@ private struct AnyCoreFieldHolder {
@ForyObject
private struct AnyHashableSetHolder {
var set: Set<AnyHashable> = []
- var optionalSet: Set<AnyHashable>? = nil
+ var optionalSet: Set<AnyHashable>?
}
@ForyObject
@@ -68,6 +68,17 @@ private struct AnyHashableValueHolder {
var value: AnyHashable = AnyHashable(Int32(0))
}
+private func nestedDynamicAnyList(depth: Int) -> Any {
+ var value: Any = Int32(1)
+ if depth <= 0 {
+ return value
+ }
+ for _ in 0..<depth {
+ value = [value] as [Any]
+ }
+ return value
+}
+
@Test
func topLevelAnyHashableRoundTrip() throws {
let fory = Fory()
@@ -93,7 +104,7 @@ func topLevelAnyHashableAnyMapRoundTrip() throws {
AnyHashable("name"): "fory",
AnyHashable(Int32(7)): Int64(9001),
AnyHashable(true): NSNull(),
- AnyHashable(AnyHashableDynamicKey(id: 3)):
AnyHashableDynamicValue(label: "swift", score: 99),
+ AnyHashable(AnyHashableDynamicKey(id: 3)):
AnyHashableDynamicValue(label: "swift", score: 99)
]
let data = try fory.serialize(value)
@@ -124,7 +135,7 @@ func topLevelAnyHashableSetRoundTrip() throws {
AnyHashable("name"),
AnyHashable(Int32(7)),
AnyHashable(true),
- AnyHashable(AnyHashableDynamicKey(id: 11)),
+ AnyHashable(AnyHashableDynamicKey(id: 11))
]
let data = try fory.serialize(value)
@@ -137,6 +148,26 @@ func topLevelAnyHashableSetRoundTrip() throws {
#expect(decoded.contains(AnyHashable(AnyHashableDynamicKey(id: 11))))
}
+@Test
+func topLevelDynamicAnySetRoundTrip() throws {
+ let fory = Fory()
+ fory.register(AnyHashableDynamicKey.self, id: 413)
+
+ let value: Any = Set<AnyHashable>([
+ AnyHashable("name"),
+ AnyHashable(Int32(9)),
+ AnyHashable(AnyHashableDynamicKey(id: 12))
+ ])
+
+ let data = try fory.serialize(value)
+ let decoded: Any = try fory.deserialize(data)
+ let set = decoded as? Set<AnyHashable>
+ #expect(set != nil)
+ #expect(set?.contains(AnyHashable("name")) == true)
+ #expect(set?.contains(AnyHashable(Int32(9))) == true)
+ #expect(set?.contains(AnyHashable(AnyHashableDynamicKey(id: 12))) == true)
+}
+
@Test
func macroAnyHashableAnyMapFieldsRoundTrip() throws {
let fory = Fory()
@@ -148,10 +179,10 @@ func macroAnyHashableAnyMapFieldsRoundTrip() throws {
map: [
AnyHashable("id"): Int32(1),
AnyHashable(Int32(2)): "value2",
- AnyHashable(AnyHashableDynamicKey(id: 5)):
AnyHashableDynamicValue(label: "nested", score: 8),
+ AnyHashable(AnyHashableDynamicKey(id: 5)):
AnyHashableDynamicValue(label: "nested", score: 8)
],
optionalMap: [
- AnyHashable(false): NSNull(),
+ AnyHashable(false): NSNull()
]
)
@@ -177,10 +208,10 @@ func macroAnyHashableSetFieldsRoundTrip() throws {
set: [
AnyHashable("a"),
AnyHashable(Int32(3)),
- AnyHashable(AnyHashableDynamicKey(id: 9)),
+ AnyHashable(AnyHashableDynamicKey(id: 9))
],
optionalSet: [
- AnyHashable(false),
+ AnyHashable(false)
]
)
@@ -208,11 +239,11 @@ func macroCoreAnyFieldsRoundTrip() throws {
anyList: [Int32(44), "core-list", AnyHashableDynamicValue(label:
"core-list-obj", score: 45)],
stringAnyMap: [
"k1": Int32(46),
- "k2": AnyHashableDynamicValue(label: "core-map-a", score: 47),
+ "k2": AnyHashableDynamicValue(label: "core-map-a", score: 47)
],
int32AnyMap: [
48: "core-map-b",
- 49: AnyHashableDynamicValue(label: "core-map-c", score: 50),
+ 49: AnyHashableDynamicValue(label: "core-map-c", score: 50)
]
)
@@ -254,7 +285,7 @@ func dynamicAnyMapNormalizationForAnyHashableKeys() throws {
let heterogeneous: Any = [
AnyHashable("k"): Int32(1),
- AnyHashable(Int32(2)): "v2",
+ AnyHashable(Int32(2)): "v2"
] as [AnyHashable: Any]
let heteroData = try fory.serialize(heterogeneous)
let heteroDecoded: Any = try fory.deserialize(heteroData)
@@ -265,7 +296,7 @@ func dynamicAnyMapNormalizationForAnyHashableKeys() throws {
let homogeneous: Any = [
AnyHashable("a"): Int32(10),
- AnyHashable("b"): Int32(20),
+ AnyHashable("b"): Int32(20)
] as [AnyHashable: Any]
let homogeneousData = try fory.serialize(homogeneous)
let homogeneousDecoded: Any = try fory.deserialize(homogeneousData)
@@ -305,7 +336,7 @@ func topLevelAllSupportedAnyTypesRoundTrip() throws {
let anyListValue: [Any] = [
Int32(4),
"list",
- AnyHashableDynamicValue(label: "list-obj", score: 5),
+ AnyHashableDynamicValue(label: "list-obj", score: 5)
]
let anyListData = try fory.serialize(anyListValue)
let anyListDecoded: [Any] = try fory.deserialize(anyListData)
@@ -316,7 +347,7 @@ func topLevelAllSupportedAnyTypesRoundTrip() throws {
let stringAnyMapValue: [String: Any] = [
"a": Int32(6),
- "b": AnyHashableDynamicValue(label: "map-a", score: 7),
+ "b": AnyHashableDynamicValue(label: "map-a", score: 7)
]
let stringAnyMapData = try fory.serialize(stringAnyMapValue)
let stringAnyMapDecoded: [String: Any] = try
fory.deserialize(stringAnyMapData)
@@ -325,7 +356,7 @@ func topLevelAllSupportedAnyTypesRoundTrip() throws {
let int32AnyMapValue: [Int32: Any] = [
8: "v8",
- 9: AnyHashableDynamicValue(label: "map-b", score: 9),
+ 9: AnyHashableDynamicValue(label: "map-b", score: 9)
]
let int32AnyMapData = try fory.serialize(int32AnyMapValue)
let int32AnyMapDecoded: [Int32: Any] = try
fory.deserialize(int32AnyMapData)
@@ -334,7 +365,7 @@ func topLevelAllSupportedAnyTypesRoundTrip() throws {
let anyHashableAnyMapValue: [AnyHashable: Any] = [
AnyHashable("x"): Int32(10),
- AnyHashable(Int32(11)): AnyHashableDynamicValue(label: "map-c", score:
11),
+ AnyHashable(Int32(11)): AnyHashableDynamicValue(label: "map-c", score:
11)
]
let anyHashableAnyMapData = try fory.serialize(anyHashableAnyMapValue)
let anyHashableAnyMapDecoded: [AnyHashable: Any] = try
fory.deserialize(anyHashableAnyMapData)
@@ -347,7 +378,7 @@ func topLevelAllSupportedAnyTypesRoundTrip() throws {
let anyHashableSetValue: Set<AnyHashable> = [
AnyHashable("set"),
AnyHashable(Int32(12)),
- AnyHashable(AnyHashableDynamicKey(id: 13)),
+ AnyHashable(AnyHashableDynamicKey(id: 13))
]
let anyHashableSetData = try fory.serialize(anyHashableSetValue)
let anyHashableSetDecoded: Set<AnyHashable> = try
fory.deserialize(anyHashableSetData)
@@ -356,3 +387,36 @@ func topLevelAllSupportedAnyTypesRoundTrip() throws {
#expect(anyHashableSetDecoded.contains(AnyHashable(Int32(12))))
#expect(anyHashableSetDecoded.contains(AnyHashable(AnyHashableDynamicKey(id:
13))))
}
+
+@Test
+func dynamicAnyMaxDepthRejectsDeepNesting() throws {
+ let value = nestedDynamicAnyList(depth: 3)
+ let writer = Fory(config: .init(maxDepth: 8))
+ let payload = try writer.serialize(value)
+
+ let limited = Fory(config: .init(maxDepth: 3))
+ do {
+ let _: Any = try limited.deserialize(payload)
+ #expect(Bool(false))
+ } catch {
+ #expect(String(describing: error).contains("maxDepth"))
+ }
+}
+
+@Test
+func dynamicAnyMaxDepthAllowsBoundaryDepth() throws {
+ let value = nestedDynamicAnyList(depth: 3)
+ let fory = Fory(config: .init(maxDepth: 4))
+
+ let payload = try fory.serialize(value)
+ let decoded: Any = try fory.deserialize(payload)
+
+ let level1 = decoded as? [Any]
+ let level2 = level1?.first as? [Any]
+ let level3 = level2?.first as? [Any]
+
+ #expect(level1 != nil)
+ #expect(level2 != nil)
+ #expect(level3 != nil)
+ #expect(level3?.first as? Int32 == 1)
+}
diff --git a/swift/Tests/ForyTests/DateTimeTests.swift
b/swift/Tests/ForyTests/DateTimeTests.swift
index 399b20479..599897a34 100644
--- a/swift/Tests/ForyTests/DateTimeTests.swift
+++ b/swift/Tests/ForyTests/DateTimeTests.swift
@@ -30,6 +30,7 @@ private struct DateMacroHolder {
func dateAndTimestampTypeIds() {
#expect(ForyDate.staticTypeId == .date)
#expect(ForyTimestamp.staticTypeId == .timestamp)
+ #expect(Duration.staticTypeId == .duration)
#expect(Date.staticTypeId == .timestamp)
}
@@ -47,6 +48,11 @@ func dateAndTimestampRoundTrip() throws {
let tsDecoded: ForyTimestamp = try fory.deserialize(tsData)
#expect(tsDecoded == ts)
+ let duration = Duration.seconds(-7) + Duration.nanoseconds(12_000_000)
+ let durationData = try fory.serialize(duration)
+ let durationDecoded: Duration = try fory.deserialize(durationData)
+ #expect(durationDecoded == duration)
+
let instant = Date(timeIntervalSince1970: 1_731_234_567.123_456_7)
let instantData = try fory.serialize(instant)
let instantDecoded: Date = try fory.deserialize(instantData)
diff --git a/swift/Tests/ForyTests/EnumTests.swift
b/swift/Tests/ForyTests/EnumTests.swift
index 4053066ee..f16c2c6be 100644
--- a/swift/Tests/ForyTests/EnumTests.swift
+++ b/swift/Tests/ForyTests/EnumTests.swift
@@ -100,7 +100,7 @@ func mixedEnumShapesRoundTrip() throws {
let nestedMap: [String: Token] = [
"one": .number(1),
"plus": .plus,
- "nested": .child(.ident("deep")),
+ "nested": .child(.ident("deep"))
]
let tokens: [Token] = [
@@ -111,7 +111,7 @@ func mixedEnumShapesRoundTrip() throws {
.other(42),
.other(nil),
.child(.child(.other(nil))),
- .map(nestedMap),
+ .map(nestedMap)
]
let data = try fory.serialize(tokens)
diff --git a/swift/Tests/ForyTests/ForySwiftTests.swift
b/swift/Tests/ForyTests/ForySwiftTests.swift
index 9e5fd6541..96fb536fe 100644
--- a/swift/Tests/ForyTests/ForySwiftTests.swift
+++ b/swift/Tests/ForyTests/ForySwiftTests.swift
@@ -38,10 +38,10 @@ struct Person: Equatable {
@ForyObject
struct FieldOrder: Equatable {
- var z: String
- var a: Int64
- var b: Int16
- var c: Int32
+ var textTail: String
+ var longValue: Int64
+ var shortValue: Int16
+ var intValue: Int32
}
@ForyObject
@@ -144,22 +144,96 @@ func primitiveRoundTrip() throws {
#expect(binaryValue == binary)
}
+@Test
+func extendedWireTypesRoundTrip() throws {
+ let fory = Fory()
+
+ let float16Value = Float16(3.5)
+ let float16Data = try fory.serialize(float16Value)
+ let float16Decoded: Float16 = try fory.deserialize(float16Data)
+ #expect(float16Decoded.bitPattern == float16Value.bitPattern)
+
+ let bfloatValue = BFloat16(rawValue: 0x3F80)
+ let bfloatData = try fory.serialize(bfloatValue)
+ let bfloatDecoded: BFloat16 = try fory.deserialize(bfloatData)
+ #expect(bfloatDecoded == bfloatValue)
+
+ let durationValue = Duration.seconds(-2) +
Duration.nanoseconds(123_456_789)
+ let durationData = try fory.serialize(durationValue)
+ let durationDecoded: Duration = try fory.deserialize(durationData)
+ #expect(durationDecoded == durationValue)
+
+ let float16Array: [Float16] = [Float16(1), Float16(-2), Float16(4.5)]
+ let float16ArrayData = try fory.serialize(float16Array)
+ let float16ArrayDecoded: [Float16] = try fory.deserialize(float16ArrayData)
+ #expect(float16ArrayDecoded.map(\.bitPattern) ==
float16Array.map(\.bitPattern))
+}
+
@Test
func namedInitializerBuildsConfig() {
let defaultConfig = Fory()
#expect(defaultConfig.config.xlang == true)
#expect(defaultConfig.config.trackRef == false)
#expect(defaultConfig.config.compatible == false)
+ #expect(defaultConfig.config.maxDepth == 5)
- let explicitConfig = Fory(xlang: false, trackRef: true, compatible: true)
+ let explicitConfig = Fory(xlang: false, trackRef: true, compatible: true,
maxDepth: 7)
#expect(explicitConfig.config.xlang == false)
#expect(explicitConfig.config.trackRef == true)
#expect(explicitConfig.config.compatible == true)
+ #expect(explicitConfig.config.maxDepth == 7)
- let configInit = Fory(config: .init(xlang: false, trackRef: false,
compatible: true))
+ let configInit = Fory(config: .init(xlang: false, trackRef: false,
compatible: true, maxDepth: 9))
#expect(configInit.config.xlang == false)
#expect(configInit.config.trackRef == false)
#expect(configInit.config.compatible == true)
+ #expect(configInit.config.maxDepth == 9)
+}
+
+@Test
+func decodeLimitsRejectOversizedPayloads() throws {
+ let writer = Fory()
+
+ let oversizedCollection = try writer.serialize(["a", "b", "c"])
+ let collectionLimited = Fory(config: .init(maxCollectionLength: 2))
+ do {
+ let _: [String] = try
collectionLimited.deserialize(oversizedCollection)
+ #expect(Bool(false))
+ } catch {}
+
+ let oversizedMap = try writer.serialize([Int32(1): Int32(1), 2: 2, 3: 3])
+ do {
+ let _: [Int32: Int32] = try collectionLimited.deserialize(oversizedMap)
+ #expect(Bool(false))
+ } catch {}
+
+ let oversizedBinary = try writer.serialize(Data([0x01, 0x02, 0x03, 0x04]))
+ let binaryLimited = Fory(config: .init(maxBinaryLength: 3))
+ do {
+ let _: Data = try binaryLimited.deserialize(oversizedBinary)
+ #expect(Bool(false))
+ } catch {}
+
+ let oversizedArrayPayload = try writer.serialize([UInt16(1), 2])
+ let payloadLimited = Fory(config: .init(maxCollectionLength: 1))
+ do {
+ let _: [UInt16] = try payloadLimited.deserialize(oversizedArrayPayload)
+ #expect(Bool(false))
+ } catch {}
+}
+
+@Test
+func deserializeRejectsTrailingBytes() throws {
+ let fory = Fory()
+ let payload = try fory.serialize(Int32(7))
+ var bytes = [UInt8](payload)
+ bytes.append(0xFF)
+ let withTrailing = Data(bytes)
+
+ do {
+ let _: Int32 = try fory.deserialize(withTrailing)
+ #expect(Bool(false))
+ } catch {}
}
@Test
@@ -299,6 +373,29 @@ func topLevelAnyRoundTrip() throws {
#expect(nullDecoded is ForyAnyNullValue)
}
+@Test
+func mixedDynamicRegistrationModesCanDecodeByID() throws {
+ let fory = Fory()
+ fory.register(Address.self, id: 600)
+ try fory.register(Person.self, name: "demo.person")
+
+ let value: Any = Address(street: "mixed", zip: 7788)
+ let data = try fory.serialize(value)
+ let decoded: Any = try fory.deserialize(data)
+ #expect(decoded as? Address == Address(street: "mixed", zip: 7788))
+}
+
+@Test
+func duplicateNameRegistrationIsRejected() throws {
+ let resolver = TypeResolver()
+ try resolver.register(Address.self, namespace: "demo", typeName: "entity")
+
+ do {
+ try resolver.register(Person.self, namespace: "demo", typeName:
"entity")
+ #expect(Bool(false))
+ } catch {}
+}
+
@Test
func topLevelAnyObjectRoundTrip() throws {
let fory = Fory(config: .init(xlang: true, trackRef: true))
@@ -363,7 +460,7 @@ func macroDynamicAnyObjectAndAnySerializerFieldsRoundTrip()
throws {
items: [Int32(11), Address(street: "Nested", zip: 10002)],
map: [
"age": Int64(19),
- "address": Address(street: "Mapped", zip: 10003),
+ "address": Address(street: "Mapped", zip: 10003)
]
)
let serializerData = try fory.serialize(serializerHolder)
@@ -391,13 +488,13 @@ func macroAnyFieldsRoundTrip() throws {
"count": Int64(3),
"name": "map",
"address": Address(street: "AnyMap", zip: 11003),
- "empty": NSNull(),
+ "empty": NSNull()
],
int32Map: [
1: Int32(-9),
2: "v2",
3: Address(street: "AnyIntMap", zip: 11004),
- 4: NSNull(),
+ 4: NSNull()
]
)
let data = try fory.serialize(value)
@@ -464,7 +561,7 @@ func macroFieldOrderFollowsForyRules() throws {
let fory = Fory()
fory.register(FieldOrder.self, id: 300)
- let value = FieldOrder(z: "tail", a: 123456789, b: 17, c: 99)
+ let value = FieldOrder(textTail: "tail", longValue: 123456789, shortValue:
17, intValue: 99)
let data = try fory.serialize(value)
let buffer = ByteBuffer(data: data)
@@ -481,10 +578,10 @@ func macroFieldOrderFollowsForyRules() throws {
let tailContext = ReadContext(buffer: buffer, typeResolver:
fory.typeResolver, trackRef: false)
let fourth = try String.foryReadData(tailContext)
- #expect(first == value.b)
- #expect(second == value.a)
- #expect(third == value.c)
- #expect(fourth == value.z)
+ #expect(first == value.shortValue)
+ #expect(second == value.longValue)
+ #expect(third == value.intValue)
+ #expect(fourth == value.textTail)
}
@Test
@@ -543,7 +640,7 @@ func pvlVarInt64AndVarUInt64Extremes() throws {
72_057_594_037_927_935,
72_057_594_037_927_936,
UInt64(Int64.max),
- UInt64.max,
+ UInt64.max
]
let intValues: [Int64] = [
Int64.min,
@@ -560,7 +657,7 @@ func pvlVarInt64AndVarUInt64Extremes() throws {
1_000_000,
1_000_000_000_000,
Int64.max - 1,
- Int64.max,
+ Int64.max
]
let writeBuffer = ByteBuffer()
@@ -641,7 +738,7 @@ func typeMetaRoundTripByName() throws {
nullable: true,
generics: [
.init(typeID: TypeId.string.rawValue, nullable: false),
- .init(typeID: TypeId.varint32.rawValue, nullable: true),
+ .init(typeID: TypeId.varint32.rawValue, nullable: true)
]
)
),
@@ -649,7 +746,7 @@ func typeMetaRoundTripByName() throws {
fieldID: 7,
fieldName: "ignored_for_tag_mode",
fieldType: .init(typeID: TypeId.varint32.rawValue, nullable: false)
- ),
+ )
]
let meta = try TypeMeta(
diff --git a/swift/Tests/ForyXlangTests/main.swift
b/swift/Tests/ForyXlangTests/main.swift
index d9a3d2791..707c0c261 100644
--- a/swift/Tests/ForyXlangTests/main.swift
+++ b/swift/Tests/ForyXlangTests/main.swift
@@ -76,7 +76,7 @@ private struct StructWithMap {
@ForyObject
private struct VersionCheckStruct {
var f1: Int32 = 0
- var f2: String? = nil
+ var f2: String?
var f3: Double = 0
}
@@ -85,7 +85,7 @@ private struct EmptyStructEvolution {}
@ForyObject
private struct OneStringFieldStruct {
- var f1: String? = nil
+ var f1: String?
}
@ForyObject
@@ -120,16 +120,16 @@ private struct NullableComprehensiveSchemaConsistent {
var setField: Set<String> = []
var mapField: [String: String] = [:]
- var nullableInt: Int32? = nil
- var nullableLong: Int64? = nil
- var nullableFloat: Float? = nil
+ var nullableInt: Int32?
+ var nullableLong: Int64?
+ var nullableFloat: Float?
- var nullableDouble: Double? = nil
- var nullableBool: Bool? = nil
- var nullableString: String? = nil
- var nullableList: [String]? = nil
- var nullableSet: Set<String>? = nil
- var nullableMap: [String: String]? = nil
+ var nullableDouble: Double?
+ var nullableBool: Bool?
+ var nullableString: String?
+ var nullableList: [String]?
+ var nullableSet: Set<String>?
+ var nullableMap: [String: String]?
}
@ForyObject
@@ -175,8 +175,8 @@ private final class RefInnerSchemaConsistent {
@ForyObject
private final class RefOuterSchemaConsistent {
- var inner1: RefInnerSchemaConsistent? = nil
- var inner2: RefInnerSchemaConsistent? = nil
+ var inner1: RefInnerSchemaConsistent?
+ var inner2: RefInnerSchemaConsistent?
required init() {}
}
@@ -191,8 +191,8 @@ private final class RefInnerCompatible {
@ForyObject
private final class RefOuterCompatible {
- var inner1: RefInnerCompatible? = nil
- var inner2: RefInnerCompatible? = nil
+ var inner1: RefInnerCompatible?
+ var inner2: RefInnerCompatible?
required init() {}
}
@@ -216,7 +216,7 @@ private final class RefOverrideContainer {
@ForyObject
private final class CircularRefStruct {
var name: String = ""
- weak var selfRef: CircularRefStruct? = nil
+ weak var selfRef: CircularRefStruct?
required init() {}
}
@@ -260,7 +260,7 @@ private struct EmptyWrapper {}
@ForyObject
private struct Dog {
var age: Int32 = 0
- var name: String? = nil
+ var name: String?
}
@ForyObject
@@ -295,7 +295,7 @@ private struct UnsignedSchemaConsistentSimple {
@ForyField(encoding: .tagged)
var u64Tagged: UInt64 = 0
@ForyField(encoding: .tagged)
- var u64TaggedNullable: UInt64? = nil
+ var u64TaggedNullable: UInt64?
}
@ForyObject
@@ -311,30 +311,30 @@ private struct UnsignedSchemaConsistent {
@ForyField(encoding: .tagged)
var u64TaggedField: UInt64 = 0
- var u8NullableField: UInt8? = nil
- var u16NullableField: UInt16? = nil
- var u32VarNullableField: UInt32? = nil
+ var u8NullableField: UInt8?
+ var u16NullableField: UInt16?
+ var u32VarNullableField: UInt32?
@ForyField(encoding: .fixed)
- var u32FixedNullableField: UInt32? = nil
- var u64VarNullableField: UInt64? = nil
+ var u32FixedNullableField: UInt32?
+ var u64VarNullableField: UInt64?
@ForyField(encoding: .fixed)
- var u64FixedNullableField: UInt64? = nil
+ var u64FixedNullableField: UInt64?
@ForyField(encoding: .tagged)
- var u64TaggedNullableField: UInt64? = nil
+ var u64TaggedNullableField: UInt64?
}
@ForyObject
private struct UnsignedSchemaCompatible {
- var u8Field1: UInt8? = nil
- var u16Field1: UInt16? = nil
- var u32VarField1: UInt32? = nil
+ var u8Field1: UInt8?
+ var u16Field1: UInt16?
+ var u32VarField1: UInt32?
@ForyField(encoding: .fixed)
- var u32FixedField1: UInt32? = nil
- var u64VarField1: UInt64? = nil
+ var u32FixedField1: UInt32?
+ var u64VarField1: UInt64?
@ForyField(encoding: .fixed)
- var u64FixedField1: UInt64? = nil
+ var u64FixedField1: UInt64?
@ForyField(encoding: .tagged)
- var u64TaggedField1: UInt64? = nil
+ var u64TaggedField1: UInt64?
var u8Field2: UInt8 = 0
var u16Field2: UInt16 = 0
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]