diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index a6320d2bef..7b7e070d7d 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -155,7 +155,7 @@ jobs: echo "rust=false" >> "$GITHUB_OUTPUT" fi - if grep -Eq '^(swift/)' <<< "$changed_files"; then + if grep -Eq '^(Package\.swift$|swift/)' <<< "$changed_files"; then echo "swift=true" >> "$GITHUB_OUTPUT" else echo "swift=false" >> "$GITHUB_OUTPUT" @@ -481,6 +481,50 @@ jobs: cd swift swift-format lint --configuration .swift-format --recursive --strict Sources Tests Package.swift + swift_linux: + name: Swift Linux CI + needs: changes + if: needs.changes.outputs.swift == 'true' + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v5 + - name: Run Swift unit tests + env: + ENABLE_FORY_DEBUG_OUTPUT: "1" + run: | + swift --version + cd swift + swift test + + swift_apple_platforms: + name: Swift ${{ matrix.platform }} Build + needs: changes + if: needs.changes.outputs.swift == 'true' + runs-on: macos-latest + strategy: + fail-fast: false + matrix: + include: + - platform: tvOS + sdk: appletvos + triple: arm64-apple-tvos16.0 + - platform: watchOS + sdk: watchos + triple: arm64_32-apple-watchos9.0 + - platform: visionOS + sdk: xros + triple: arm64-apple-xros1.0 + steps: + - uses: actions/checkout@v5 + - name: Build Swift package + run: | + sdk_path="$(xcrun --sdk "${{ matrix.sdk }}" --show-sdk-path)" + swift build \ + --package-path . \ + --target Fory \ + --triple "${{ matrix.triple }}" \ + --sdk "$sdk_path" + swift_xlang: name: Swift Xlang Test runs-on: macos-latest diff --git a/Package.swift b/Package.swift index a69bcc1b69..67b68f8e92 100644 --- a/Package.swift +++ b/Package.swift @@ -23,7 +23,10 @@ let package = Package( name: "fory", platforms: [ .macOS(.v13), - .iOS(.v16) + .iOS(.v16), + .tvOS(.v16), + .watchOS(.v9), + .visionOS(.v1) ], products: [ .library( diff --git a/docs/object-serialization/swift/basic-serialization.md b/docs/object-serialization/swift/basic-serialization.md index 4374739db7..9df5b2d576 100644 --- a/docs/object-serialization/swift/basic-serialization.md +++ b/docs/object-serialization/swift/basic-serialization.md @@ -139,6 +139,10 @@ directly by a type, retroactive conformances, and separate custom serializers. - `String` - `Data` +`Int` and `UInt` keep 64-bit wire encodings on every platform. When a decoded +value is outside the native range on a 32-bit target, deserialization throws +`ForyError.invalidData`. + ### Date and time - `Date` diff --git a/docs/start/swift.md b/docs/start/swift.md index c294e97ced..f4aad79e1b 100644 --- a/docs/start/swift.md +++ b/docs/start/swift.md @@ -21,7 +21,8 @@ license: | Fory Swift provides xlang Object Serialization and compiler-generated models. It is distributed through Swift Package Manager, uses Swift tools 6.0, and -targets macOS 13 or later and iOS 16 or later. +targets macOS 13 or later, iOS and tvOS 16 or later, watchOS 9 or later, and +visionOS 1 or later. Linux requires a Swift 6.0 or later toolchain. ## Verify the Toolchain diff --git a/swift/Package.swift b/swift/Package.swift index af8a25ff8c..4f66d4c650 100644 --- a/swift/Package.swift +++ b/swift/Package.swift @@ -23,7 +23,10 @@ let package = Package( name: "fory-swift", platforms: [ .macOS(.v13), - .iOS(.v16) + .iOS(.v16), + .tvOS(.v16), + .watchOS(.v9), + .visionOS(.v1) ], products: [ .library( diff --git a/swift/Sources/Fory/ByteBuffer.swift b/swift/Sources/Fory/ByteBuffer.swift index 6d2fe3f063..b7231b094e 100644 --- a/swift/Sources/Fory/ByteBuffer.swift +++ b/swift/Sources/Fory/ByteBuffer.swift @@ -836,7 +836,7 @@ public final class ByteBuffer { if isASCII { return String(decoding: utf8Bytes, as: UTF8.self) } - if #available(macOS 15.0, iOS 18.0, *) { + if #available(macOS 15.0, iOS 18.0, tvOS 18.0, watchOS 11.0, visionOS 2.0, *) { return String(validating: utf8Bytes, as: UTF8.self) } return String(bytes: utf8Bytes, encoding: .utf8) diff --git a/swift/Sources/Fory/Decimal.swift b/swift/Sources/Fory/Decimal.swift index f7c8aac553..94dc3b8468 100644 --- a/swift/Sources/Fory/Decimal.swift +++ b/swift/Sources/Fory/Decimal.swift @@ -19,9 +19,18 @@ import Foundation private let decimalSmallPositiveMax: UInt64 = 0x3fff_ffff_ffff_ffff private let decimalSmallNegativeAbsMax: UInt64 = 0x4000_0000_0000_0000 -private let decimalLengthMask: UInt8 = 0x0f -private let decimalNegativeMask: UInt8 = 0x10 -private let decimalCompactMask: UInt8 = 0x20 +#if os(Linux) + // Corelibs Foundation reverses the NSDecimal flag bitfield order used by Darwin. + private let decimalLengthMask: UInt8 = 0xf0 + private let decimalLengthShift: UInt8 = 4 + private let decimalNegativeMask: UInt8 = 0x08 + private let decimalCompactMask: UInt8 = 0x04 +#else + private let decimalLengthMask: UInt8 = 0x0f + private let decimalLengthShift: UInt8 = 0 + private let decimalNegativeMask: UInt8 = 0x10 + private let decimalCompactMask: UInt8 = 0x20 +#endif private let decimalHeaderSize = 4 private let decimalMaxMantissaWords = 8 private let decimalMaxMagnitudeBytes = decimalMaxMantissaWords * 2 @@ -146,7 +155,8 @@ private func foundationDecimalWireState(_ value: Decimal) -> FoundationDecimalWi return withUnsafeBytes(of: &compact) { raw in let exponent = Int8(bitPattern: raw[0]) let flags = raw[1] - let length = min(Int(flags & decimalLengthMask), decimalMaxMantissaWords) + let length = min( + Int((flags & decimalLengthMask) >> decimalLengthShift), decimalMaxMantissaWords) let isNegative = (flags & decimalNegativeMask) != 0 var magnitude: [UInt8] = [] @@ -178,7 +188,7 @@ private func buildFoundationDecimal( withUnsafeMutableBytes(of: &value) { raw in raw.initializeMemory(as: UInt8.self, repeating: 0) raw[0] = UInt8(bitPattern: exponent) - var flags = UInt8(truncatingIfNeeded: mantissa.length) + var flags = UInt8(truncatingIfNeeded: mantissa.length) << decimalLengthShift if signum < 0 && !normalized.isEmpty { flags |= decimalNegativeMask } diff --git a/swift/Sources/Fory/FieldCodecs.swift b/swift/Sources/Fory/FieldCodecs.swift index f8f8a1c299..6b73c9e143 100644 --- a/swift/Sources/Fory/FieldCodecs.swift +++ b/swift/Sources/Fory/FieldCodecs.swift @@ -859,7 +859,7 @@ public enum IntVarintCodec: FieldCodec { } public static func readFieldData(_ context: ReadContext) throws -> Int { - Int(try context.buffer.readVarInt64()) + try checkedInt64ToInt(context.buffer.readVarInt64()) } } @@ -874,7 +874,7 @@ public enum IntFixedCodec: FieldCodec { } public static func readFieldData(_ context: ReadContext) throws -> Int { - Int(try context.buffer.readInt64()) + try checkedInt64ToInt(context.buffer.readInt64()) } } @@ -889,7 +889,7 @@ public enum IntTaggedCodec: FieldCodec { } public static func readFieldData(_ context: ReadContext) throws -> Int { - Int(try context.buffer.readTaggedInt64()) + try checkedInt64ToInt(context.buffer.readTaggedInt64()) } } @@ -904,7 +904,7 @@ public enum UIntVarintCodec: FieldCodec { } public static func readFieldData(_ context: ReadContext) throws -> UInt { - UInt(try context.buffer.readVarUInt64()) + try checkedUInt64ToUInt(context.buffer.readVarUInt64()) } } @@ -919,7 +919,7 @@ public enum UIntFixedCodec: FieldCodec { } public static func readFieldData(_ context: ReadContext) throws -> UInt { - UInt(try context.buffer.readUInt64()) + try checkedUInt64ToUInt(context.buffer.readUInt64()) } } @@ -934,7 +934,7 @@ public enum UIntTaggedCodec: FieldCodec { } public static func readFieldData(_ context: ReadContext) throws -> UInt { - UInt(try context.buffer.readTaggedUInt64()) + try checkedUInt64ToUInt(context.buffer.readTaggedUInt64()) } } @@ -1514,7 +1514,7 @@ private func readIntArrayPayload( var values: [Int] = [] values.reserveCapacity(count) for _ in 0..( { switch remoteTypeID { case .int64: - return uncheckedScalarCast(Int(try context.buffer.readInt64()), to: ElementCodec.Target.self) + return uncheckedScalarCast( + try IntFixedCodec.readFieldData(context), to: ElementCodec.Target.self) case .varint64: return uncheckedScalarCast( - Int(try context.buffer.readVarInt64()), to: ElementCodec.Target.self) + try IntVarintCodec.readFieldData(context), to: ElementCodec.Target.self) case .taggedInt64: return uncheckedScalarCast( - Int(try context.buffer.readTaggedInt64()), to: ElementCodec.Target.self) + try IntTaggedCodec.readFieldData(context), to: ElementCodec.Target.self) default: break } @@ -1747,13 +1748,14 @@ private func readCompatibleElementPayload( { switch remoteTypeID { case .uint64: - return uncheckedScalarCast(UInt(try context.buffer.readUInt64()), to: ElementCodec.Target.self) + return uncheckedScalarCast( + try UIntFixedCodec.readFieldData(context), to: ElementCodec.Target.self) case .varUInt64: return uncheckedScalarCast( - UInt(try context.buffer.readVarUInt64()), to: ElementCodec.Target.self) + try UIntVarintCodec.readFieldData(context), to: ElementCodec.Target.self) case .taggedUInt64: return uncheckedScalarCast( - UInt(try context.buffer.readTaggedUInt64()), to: ElementCodec.Target.self) + try UIntTaggedCodec.readFieldData(context), to: ElementCodec.Target.self) default: break } diff --git a/swift/Sources/Fory/PrimitiveSerializers.swift b/swift/Sources/Fory/PrimitiveSerializers.swift index c7cbd17ddd..ea8016d8d7 100644 --- a/swift/Sources/Fory/PrimitiveSerializers.swift +++ b/swift/Sources/Fory/PrimitiveSerializers.swift @@ -17,6 +17,46 @@ import Foundation +// Int and UInt keep their 64-bit wire encodings on every architecture. Native +// 32-bit targets must reject out-of-range values instead of trapping during conversion. +@usableFromInline +@inline(__always) +internal func checkedInt64ToInt(_ value: Int64) throws -> Int { + #if arch(arm64) || arch(x86_64) + return Int(value) + #else + guard let result = Int(exactly: value) else { + throw int64ToIntOverflow(value) + } + return result + #endif +} + +@usableFromInline +@inline(__always) +internal func checkedUInt64ToUInt(_ value: UInt64) throws -> UInt { + #if arch(arm64) || arch(x86_64) + return UInt(value) + #else + guard let result = UInt(exactly: value) else { + throw uint64ToUIntOverflow(value) + } + return result + #endif +} + +@usableFromInline +@inline(never) +internal func int64ToIntOverflow(_ value: Int64) -> ForyError { + ForyError.invalidData("int64 value \(value) overflows Int") +} + +@usableFromInline +@inline(never) +internal func uint64ToUIntOverflow(_ value: UInt64) -> ForyError { + ForyError.invalidData("uint64 value \(value) overflows UInt") +} + extension Bool: Serializer { public static var staticTypeId: TypeId { .bool } public static var readDataAlwaysAdvances: Bool { true } @@ -224,53 +264,51 @@ extension UInt64: Serializer { } } -#if arch(arm64) || arch(x86_64) - extension Int: Serializer { - public static var staticTypeId: TypeId { .varint64 } - public static var readDataAlwaysAdvances: Bool { true } +extension Int: Serializer { + public static var staticTypeId: TypeId { .varint64 } + public static var readDataAlwaysAdvances: Bool { true } - public static func defaultValue(_ context: ReadContext) throws -> Int { 0 } + public static func defaultValue(_ context: ReadContext) throws -> Int { 0 } - public static func writeTypeInfo(_ context: WriteContext) throws { - context.writeStaticTypeInfo(staticTypeId) - } + public static func writeTypeInfo(_ context: WriteContext) throws { + context.writeStaticTypeInfo(staticTypeId) + } - public static func readTypeInfo(_ context: ReadContext) throws -> TypeInfo? { - try context.readStaticTypeInfo(staticTypeId) - } + public static func readTypeInfo(_ context: ReadContext) throws -> TypeInfo? { + try context.readStaticTypeInfo(staticTypeId) + } - public static func writeData(_ value: Self, _ context: WriteContext) throws { - context.buffer.writeVarInt64(Int64(value)) - } + public static func writeData(_ value: Self, _ context: WriteContext) throws { + context.buffer.writeVarInt64(Int64(value)) + } - public static func readData(_ context: ReadContext) throws -> Int { - Int(try context.buffer.readVarInt64()) - } + public static func readData(_ context: ReadContext) throws -> Int { + try checkedInt64ToInt(context.buffer.readVarInt64()) } +} - extension UInt: Serializer { - public static var staticTypeId: TypeId { .varUInt64 } - public static var readDataAlwaysAdvances: Bool { true } +extension UInt: Serializer { + public static var staticTypeId: TypeId { .varUInt64 } + public static var readDataAlwaysAdvances: Bool { true } - public static func defaultValue(_ context: ReadContext) throws -> UInt { 0 } + public static func defaultValue(_ context: ReadContext) throws -> UInt { 0 } - public static func writeTypeInfo(_ context: WriteContext) throws { - context.writeStaticTypeInfo(staticTypeId) - } + public static func writeTypeInfo(_ context: WriteContext) throws { + context.writeStaticTypeInfo(staticTypeId) + } - public static func readTypeInfo(_ context: ReadContext) throws -> TypeInfo? { - try context.readStaticTypeInfo(staticTypeId) - } + public static func readTypeInfo(_ context: ReadContext) throws -> TypeInfo? { + try context.readStaticTypeInfo(staticTypeId) + } - public static func writeData(_ value: Self, _ context: WriteContext) throws { - context.buffer.writeVarUInt64(UInt64(value)) - } + public static func writeData(_ value: Self, _ context: WriteContext) throws { + context.buffer.writeVarUInt64(UInt64(value)) + } - public static func readData(_ context: ReadContext) throws -> UInt { - UInt(try context.buffer.readVarUInt64()) - } + public static func readData(_ context: ReadContext) throws -> UInt { + try checkedUInt64ToUInt(context.buffer.readVarUInt64()) } -#endif +} extension Float: Serializer { public static var staticTypeId: TypeId { .float32 } diff --git a/swift/Sources/Fory/TypeResolver.swift b/swift/Sources/Fory/TypeResolver.swift index 498b44f4af..3f90359b32 100644 --- a/swift/Sources/Fory/TypeResolver.swift +++ b/swift/Sources/Fory/TypeResolver.swift @@ -654,10 +654,8 @@ final class TypeResolver { seedBuiltin(UInt16.self) seedBuiltin(UInt32.self) seedBuiltin(UInt64.self) - #if arch(arm64) || arch(x86_64) - seedBuiltin(Int.self, wireLookup: false) - seedBuiltin(UInt.self, wireLookup: false) - #endif + seedBuiltin(Int.self, wireLookup: false) + seedBuiltin(UInt.self, wireLookup: false) seedBuiltin(Float16.self) seedBuiltin(BFloat16.self) seedBuiltin(Float.self) diff --git a/swift/Sources/Fory/UnsafeUtil.swift b/swift/Sources/Fory/UnsafeUtil.swift index ce458d6359..084e17e0f8 100644 --- a/swift/Sources/Fory/UnsafeUtil.swift +++ b/swift/Sources/Fory/UnsafeUtil.swift @@ -511,7 +511,7 @@ public enum UnsafeUtil { from bytes: UnsafeBufferPointer, index: inout Int ) throws -> Int { - Int(try readVarInt64(from: bytes, index: &index)) + try checkedInt64ToInt(readVarInt64(from: bytes, index: &index)) } @inlinable @@ -520,7 +520,7 @@ public enum UnsafeUtil { from bytes: UnsafeBufferPointer, index: inout Int ) throws -> UInt { - UInt(try readVarUInt64(from: bytes, index: &index)) + try checkedUInt64ToUInt(readVarUInt64(from: bytes, index: &index)) } @inlinable @@ -608,7 +608,7 @@ public enum UnsafeUtil { length: Int, index: inout Int ) throws -> Int { - Int(try readVarInt64(from: base, length: length, index: &index)) + try checkedInt64ToInt(readVarInt64(from: base, length: length, index: &index)) } @inlinable @@ -618,7 +618,7 @@ public enum UnsafeUtil { length: Int, index: inout Int ) throws -> UInt { - UInt(try readVarUInt64(from: base, length: length, index: &index)) + try checkedUInt64ToUInt(readVarUInt64(from: base, length: length, index: &index)) } @inlinable diff --git a/swift/Sources/ForyMacro/ForyObjectMacro.swift b/swift/Sources/ForyMacro/ForyObjectMacro.swift index 022cf052f5..4c69563c80 100644 --- a/swift/Sources/ForyMacro/ForyObjectMacro.swift +++ b/swift/Sources/ForyMacro/ForyObjectMacro.swift @@ -16,6 +16,7 @@ // under the License. // swiftlint:disable file_length +import Foundation import SwiftCompilerPlugin import SwiftDiagnostics import SwiftSyntax @@ -1734,7 +1735,7 @@ private func resolveFieldType( isCompressedNumeric: false, primitiveSize: 8 ), - customCodecType: "Int64FixedCodec" + customCodecType: normalized == "Int" ? "IntFixedCodec" : "Int64FixedCodec" ) case .tagged: return .init( @@ -1747,7 +1748,7 @@ private func resolveFieldType( isCompressedNumeric: true, primitiveSize: 8 ), - customCodecType: "Int64TaggedCodec" + customCodecType: normalized == "Int" ? "IntTaggedCodec" : "Int64TaggedCodec" ) } case "UInt64", "UInt": @@ -1765,7 +1766,7 @@ private func resolveFieldType( isCompressedNumeric: false, primitiveSize: 8 ), - customCodecType: "UInt64FixedCodec" + customCodecType: normalized == "UInt" ? "UIntFixedCodec" : "UInt64FixedCodec" ) case .tagged: return .init( @@ -1778,7 +1779,7 @@ private func resolveFieldType( isCompressedNumeric: true, primitiveSize: 8 ), - customCodecType: "UInt64TaggedCodec" + customCodecType: normalized == "UInt" ? "UIntTaggedCodec" : "UInt64TaggedCodec" ) } default: diff --git a/swift/Sources/ForyMacro/ForyObjectMacroReadGeneration.swift b/swift/Sources/ForyMacro/ForyObjectMacroReadGeneration.swift index b318ecbc92..ca65930565 100644 --- a/swift/Sources/ForyMacro/ForyObjectMacroReadGeneration.swift +++ b/swift/Sources/ForyMacro/ForyObjectMacroReadGeneration.swift @@ -15,6 +15,8 @@ // specific language governing permissions and limitations // under the License. +import Foundation + func buildReadDataDecl( declaration: ParsedDecl, sortedFields: [ParsedField], @@ -909,7 +911,7 @@ private func primitiveSchemaReadExpr(_ field: ParsedField) -> String? { case "Int64": return "try __buffer.readVarInt64()" case "Int": - return "Int(try __buffer.readVarInt64())" + return "try IntVarintCodec.readFieldData(context)" case "UInt8": return "try __buffer.readUInt8()" case "UInt16": @@ -919,7 +921,7 @@ private func primitiveSchemaReadExpr(_ field: ParsedField) -> String? { case "UInt64": return "try __buffer.readVarUInt64()" case "UInt": - return "UInt(try __buffer.readVarUInt64())" + return "try UIntVarintCodec.readFieldData(context)" case "Float": return "try __buffer.readFloat32()" case "Double": diff --git a/swift/Tests/ForyTests/CollectionSerializerTests.swift b/swift/Tests/ForyTests/CollectionSerializerTests.swift index a89eeeac62..56e91ab5ac 100644 --- a/swift/Tests/ForyTests/CollectionSerializerTests.swift +++ b/swift/Tests/ForyTests/CollectionSerializerTests.swift @@ -428,111 +428,95 @@ func annotatedCarrierMetadata() throws { ($0.fieldName, $0.fieldType) } ) - #expect( - fields["id"] - == (try UInt32FixedCodec.fieldType( - nullable: false, - trackRef: false, - resolveSerializerTypeId: resolveSerializerTypeId - )) + let expectedID = try UInt32FixedCodec.fieldType( + nullable: false, + trackRef: false, + resolveSerializerTypeId: resolveSerializerTypeId ) - #expect( - fields["values"] - == (try ArraySerializer>.fieldType( - nullable: false, - trackRef: false, - resolveSerializerTypeId: resolveSerializerTypeId - )) + let expectedValues = try ArraySerializer>.fieldType( + nullable: false, + trackRef: false, + resolveSerializerTypeId: resolveSerializerTypeId ) - #expect( - fields["packedValues"] - == (try ArraySerializer.fieldType( - nullable: false, - trackRef: false, - resolveSerializerTypeId: resolveSerializerTypeId - )) + let expectedPackedValues = try ArraySerializer.fieldType( + nullable: false, + trackRef: false, + resolveSerializerTypeId: resolveSerializerTypeId ) - #expect( - fields["packedUInt64Values"] - == (try ArraySerializer.fieldType( - nullable: false, - trackRef: false, - resolveSerializerTypeId: resolveSerializerTypeId - )) + let expectedPackedUInt64Values = try ArraySerializer.fieldType( + nullable: false, + trackRef: false, + resolveSerializerTypeId: resolveSerializerTypeId ) + + #expect(fields["id"] == expectedID) + #expect(fields["values"] == expectedValues) + #expect(fields["packedValues"] == expectedPackedValues) + #expect(fields["packedUInt64Values"] == expectedPackedUInt64Values) #expect( fields["denseValues"] == TypeMeta.FieldType(typeID: TypeId.int32Array.rawValue, nullable: false, trackRef: false) ) #expect( fields["denseUInt64Values"] == TypeMeta.FieldType(typeID: TypeId.uint64Array.rawValue, nullable: false, trackRef: false) ) - #expect( - fields["fixedSet"] - == (try SetSerializer>.fieldType( - nullable: false, - trackRef: false, - resolveSerializerTypeId: resolveSerializerTypeId - )) + let expectedFixedSet = try SetSerializer>.fieldType( + nullable: false, + trackRef: false, + resolveSerializerTypeId: resolveSerializerTypeId ) - #expect( - fields["fixedNonNullSet"] - == (try SetSerializer.fieldType( - nullable: false, - trackRef: false, - resolveSerializerTypeId: resolveSerializerTypeId - )) + let expectedFixedNonNullSet = try SetSerializer.fieldType( + nullable: false, + trackRef: false, + resolveSerializerTypeId: resolveSerializerTypeId ) - #expect(fields["fixedNonNullSet"]?.typeID == TypeId.set.rawValue) - #expect( - fields["data"] - == (try DictionarySerializer< - OptionalSerializer, - OptionalSerializer - >.fieldType( - nullable: false, - trackRef: false, - resolveSerializerTypeId: resolveSerializerTypeId - )) + let expectedData = try DictionarySerializer< + OptionalSerializer, + OptionalSerializer + >.fieldType( + nullable: false, + trackRef: false, + resolveSerializerTypeId: resolveSerializerTypeId ) + #expect(fields["fixedSet"] == expectedFixedSet) + #expect(fields["fixedNonNullSet"] == expectedFixedNonNullSet) + #expect(fields["fixedNonNullSet"]?.typeID == TypeId.set.rawValue) + #expect(fields["data"] == expectedData) + let deepFields = Dictionary( uniqueKeysWithValues: DeepAnnotatedFieldCodecHolder.foryFieldsInfo(trackRef: false).map { ($0.fieldName, $0.fieldType) } ) - #expect( - deepFields["data"] - == (try DictionarySerializer< - StringCodec, - ArraySerializer< - DictionarySerializer< - Int32FixedCodec, - ArraySerializer - > - > - >.fieldType( - nullable: false, - trackRef: false, - resolveSerializerTypeId: resolveSerializerTypeId - )) + let expectedDeepData = try DictionarySerializer< + StringCodec, + ArraySerializer< + DictionarySerializer< + Int32FixedCodec, + ArraySerializer + > + > + >.fieldType( + nullable: false, + trackRef: false, + resolveSerializerTypeId: resolveSerializerTypeId ) + #expect(deepFields["data"] == expectedDeepData) let aliasFields = Dictionary( uniqueKeysWithValues: AliasAnnotatedFieldCodecHolder.foryFieldsInfo(trackRef: false).map { ($0.fieldName, $0.fieldType) } ) - #expect( - aliasFields["data"] - == (try DictionarySerializer< - StringCodec, - ArraySerializer> - >.fieldType( - nullable: false, - trackRef: false, - resolveSerializerTypeId: resolveSerializerTypeId - )) + let expectedAliasData = try DictionarySerializer< + StringCodec, + ArraySerializer> + >.fieldType( + nullable: false, + trackRef: false, + resolveSerializerTypeId: resolveSerializerTypeId ) + #expect(aliasFields["data"] == expectedAliasData) } @Test diff --git a/swift/Tests/ForyTests/NativeIntegerTests.swift b/swift/Tests/ForyTests/NativeIntegerTests.swift new file mode 100644 index 0000000000..65c309384e --- /dev/null +++ b/swift/Tests/ForyTests/NativeIntegerTests.swift @@ -0,0 +1,96 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +import Testing + +@testable import Fory + +@ForyStruct +private struct NativeIntegerFields: Equatable { + var marker: String = "" + var intValue: Int = 0 + var uintValue: UInt = 0 + + @ForyField(encoding: .fixed) + var fixedInt: Int = 0 + + @ForyField(encoding: .fixed) + var fixedUInt: UInt = 0 + + @ForyField(encoding: .tagged) + var taggedInt: Int = 0 + + @ForyField(encoding: .tagged) + var taggedUInt: UInt = 0 + + @ArrayField(element: .encoding(.fixed)) + var intArray: [Int] = [] + + @ArrayField(element: .encoding(.fixed)) + var uintArray: [UInt] = [] +} + +@Test +func nativeIntegersRoundTrip() throws { + let fory = Fory(config: .init(trackRef: false)) + try fory.register(NativeIntegerFields.self, id: 9_901) + + for value in [Int.min, -1, 0, 1, Int.max] { + let decoded: Int = try fory.deserialize(try fory.serialize(value)) + #expect(decoded == value) + } + for value in [0, 1, UInt.max] { + let decoded: UInt = try fory.deserialize(try fory.serialize(value)) + #expect(decoded == value) + } + + let value = NativeIntegerFields( + marker: "native-width", + intValue: Int.min, + uintValue: UInt.max, + fixedInt: Int.max, + fixedUInt: UInt.max, + taggedInt: Int.min, + taggedUInt: UInt.max, + intArray: [Int.min, 0, Int.max], + uintArray: [0, UInt.max] + ) + let decoded: NativeIntegerFields = try fory.deserialize(try fory.serialize(value)) + #expect(decoded == value) + + let compatible = Fory(config: .init(trackRef: false, compatible: true)) + try compatible.register(NativeIntegerFields.self, id: 9_901) + let compatibleDecoded: NativeIntegerFields = try compatible.deserialize( + try compatible.serialize(value)) + #expect(compatibleDecoded == value) +} + +@Test +func nativeIntegerConversionChecksBounds() throws { + #expect(try checkedInt64ToInt(Int64(Int.min)) == Int.min) + #expect(try checkedInt64ToInt(Int64(Int.max)) == Int.max) + #expect(try checkedUInt64ToUInt(UInt64(UInt.max)) == UInt.max) + + #if arch(arm64_32) + #expect(throws: ForyError.invalidData("int64 value \(Int64.max) overflows Int")) { + try checkedInt64ToInt(Int64.max) + } + #expect(throws: ForyError.invalidData("uint64 value \(UInt64.max) overflows UInt")) { + try checkedUInt64ToUInt(UInt64.max) + } + #endif +} diff --git a/swift/Tests/ForyXlangTests/main.swift b/swift/Tests/ForyXlangTests/main.swift index 9e2b409a14..14d1904aa3 100644 --- a/swift/Tests/ForyXlangTests/main.swift +++ b/swift/Tests/ForyXlangTests/main.swift @@ -453,7 +453,7 @@ private func isDebugEnabled() -> Bool { private func debugLog(_ message: String) { if isDebugEnabled() { - fputs("[swift-xlang-peer] \(message)\n", stderr) + FileHandle.standardError.write(Data("[swift-xlang-peer] \(message)\n".utf8)) } } @@ -1280,6 +1280,6 @@ private func run() throws { do { try run() } catch { - fputs("Swift xlang peer failed: \(error)\n", stderr) + FileHandle.standardError.write(Data("Swift xlang peer failed: \(error)\n".utf8)) exit(1) }