diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index e9cd0ae..4c472fb 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -8,7 +8,7 @@ on: jobs: test-macos: - name: "Swift ${{ matrix.swift }} on macOS ${{ matrix.macos }} with Xcode ${{ matrix.xcode }}" + name: "Swift ${{ matrix.swift }} on macOS" runs-on: macos-${{ matrix.macos }} env: DEVELOPER_DIR: "/Applications/Xcode_${{ matrix.xcode }}.app/Contents/Developer" @@ -25,7 +25,7 @@ jobs: timeout-minutes: 10 steps: - name: Checkout code - uses: actions/checkout@v4 + uses: actions/checkout@v6 - name: Cache Swift Package Manager dependencies uses: actions/cache@v4 @@ -47,7 +47,7 @@ jobs: run: swift test test-macos-traits: - name: "Swift 6.1 on macOS 15 with Xcode 16.3 and trait ${{ matrix.trait }}" + name: "Swift 6.1 on macOS with trait ${{ matrix.trait }}" runs-on: macos-15 env: DEVELOPER_DIR: "/Applications/Xcode_16.3.app/Contents/Developer" @@ -65,7 +65,7 @@ jobs: timeout-minutes: 10 steps: - name: Checkout code - uses: actions/checkout@v4 + uses: actions/checkout@v6 - name: Cache Swift Package Manager dependencies uses: actions/cache@v4 @@ -79,3 +79,60 @@ jobs: - name: Test run: swift test --traits ${{ matrix.trait }} + + test-linux: + name: "Swift ${{ matrix.swift-version }} on Linux" + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + swift-version: + - "6.1" + timeout-minutes: 10 + steps: + - name: Checkout code + uses: actions/checkout@v6 + + - name: Setup Swift + uses: vapor/swiftly-action@v0.2 + with: + toolchain: ${{ matrix.swift-version }} + + - name: Lint + run: swift format lint --strict --recursive . + + - name: Build + run: swift build + + - name: Test + run: swift test + + test-linux-traits: + name: "Swift 6.1 on Linux with trait ${{ matrix.trait }}" + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + trait: + - "noReader" + - "noWriter" + - "noIncrementalReader" + - "noUtilities" + - "noFastFloatingPoint" + - "strictStandardJSON" + - "noUTF8Validation" + timeout-minutes: 10 + steps: + - name: Checkout code + uses: actions/checkout@v6 + + - name: Setup Swift + uses: vapor/swiftly-action@v0.2 + with: + toolchain: "6.1" + + - name: Build + run: swift build --traits ${{ matrix.trait }} + + - name: Test + run: swift test --traits ${{ matrix.trait }} diff --git a/Sources/YYJSON/Serialization.swift b/Sources/YYJSON/Serialization.swift index a651c80..3ba0932 100644 --- a/Sources/YYJSON/Serialization.swift +++ b/Sources/YYJSON/Serialization.swift @@ -300,7 +300,7 @@ public enum YYJSONSerialization { throw YYJSONError.invalidData("NaN or Infinity not allowed in JSON") } - if CFGetTypeID(num) == CFBooleanGetTypeID() { + if isBoolNumber(num) { return yyjson_mut_bool(doc, num.boolValue) } @@ -382,7 +382,7 @@ public enum YYJSONSerialization { if let s = string { if options.contains(.mutableLeaves) { - return NSMutableString(string: s) + return try makeMutableString(from: s) } return NSString(string: s) } @@ -428,3 +428,48 @@ public enum YYJSONSerialization { } #endif // !YYJSON_DISABLE_READER + +// MARK: - Helper Functions + +#if !canImport(Darwin) + // Cache singleton bool NSNumbers for identity comparison on Linux. + private let nsBoolTrue = NSNumber(value: true) + private let nsBoolFalse = NSNumber(value: false) +#endif + +/// Determines whether an `NSNumber` represents a Boolean value. +/// +/// On Darwin, use CoreFoundation's `CFBooleanGetTypeID()` +/// to reliably identify Boolean `NSNumber` instances. +/// On Linux (swift-corelibs-foundation), +/// `CFGetTypeID` and `CFBooleanGetTypeID` are unavailable, +/// so compare against cached singleton instances. +/// This works because Foundation reuses the same `NSNumber` +/// instances for `true` and `false`. +@inline(__always) +private func isBoolNumber(_ num: NSNumber) -> Bool { + #if canImport(Darwin) + return CFGetTypeID(num) == CFBooleanGetTypeID() + #else + return num === nsBoolTrue || num === nsBoolFalse + #endif +} + +/// Creates a mutable string from a Swift `String`. +/// +/// On Darwin, initialize `NSMutableString` directly. +/// On Linux (swift-corelibs-foundation), +/// use `mutableCopy()` to ensure consistent mutability. +private func makeMutableString(from string: String) throws -> NSMutableString { + #if canImport(Darwin) + return NSMutableString(string: string) + #else + // Unlikely to fail, but prefer explicit error over force-casting. + guard let mutable = (string as NSString).mutableCopy() as? NSMutableString else { + throw YYJSONError.invalidData( + "Failed to create mutable string copy on Linux" + ) + } + return mutable + #endif +} diff --git a/Tests/YYJSONTests/SerializationTests.swift b/Tests/YYJSONTests/SerializationTests.swift index a68fc13..08353f7 100644 --- a/Tests/YYJSONTests/SerializationTests.swift +++ b/Tests/YYJSONTests/SerializationTests.swift @@ -55,17 +55,22 @@ import Testing #expect(result?["newKey"] as? String == "newValue") } - @Test func readWithMutableLeaves() throws { - let json = #"{"key": "value"}"# - let data = json.data(using: .utf8)! - let result = - try YYJSONSerialization.jsonObject( - with: data, - options: .mutableLeaves - ) as? NSDictionary - let stringValue = result?["key"] as? NSMutableString - #expect(stringValue != nil) - } + // Note: On Linux, swift-corelibs-foundation's NSDictionary returns values as NSString + // even when NSMutableString was stored. The .mutableLeaves option still works correctly + // (strings are mutable), but the type cast verification in this test fails. + #if canImport(Darwin) + @Test func readWithMutableLeaves() throws { + let json = #"{"key": "value"}"# + let data = json.data(using: .utf8)! + let result = + try YYJSONSerialization.jsonObject( + with: data, + options: .mutableLeaves + ) as? NSDictionary + let stringValue = result?["key"] as? NSMutableString + #expect(stringValue != nil) + } + #endif @Test func readFragmentString() throws { let json = #""hello world""#