From bc1992dcce54b061d53295514b01eae6f3ffaa75 Mon Sep 17 00:00:00 2001 From: Mike Burgh Date: Tue, 9 Jun 2026 21:00:25 +1000 Subject: [PATCH 1/4] Lower macOS platform floor from 15 to 14 The package declares .macOS(.v15) but uses no macOS-15-only API: there are no @available guards, no import Synchronization, and no Mutex/Atomic usage anywhere in Sources. The newest API in use is NWBrowser, available since macOS 10.15. Lowering the floor to .v14 lets consumers that must support macOS 14 adopt the package without forcing their deployment target up. --- Package.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Package.swift b/Package.swift index 63140e1..d61fdc9 100644 --- a/Package.swift +++ b/Package.swift @@ -4,7 +4,7 @@ import PackageDescription let package = Package( name: "mdns-swift", platforms: [ - .macOS(.v15), + .macOS(.v14), .iOS(.v18), .tvOS(.v18), .watchOS(.v11), From 867615d3f275e4a4ed282e3ccf8d1c7f30f62ff6 Mon Sep 17 00:00:00 2001 From: David Monagle Date: Sun, 9 Aug 2026 15:36:53 +1000 Subject: [PATCH 2/4] Replace NetService TXT encoding with a hand-rolled encoder MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit NetService is API_UNAVAILABLE(watchos), so the declared watchOS platform support has never actually compiled — and the API is deprecated on every other platform. The TXT wire format (RFC 6763) is just length-prefixed "key=value" entries, so encode it directly. Keys are sorted for deterministic output and entries are truncated at 255 bytes, matching the Linux responder's TXT encoding in MDNSMessage. --- Sources/MDNSApple/AppleServiceDiscovery.swift | 25 ++++++++++++++-- .../AppleServiceDiscoveryTests.swift | 30 ++++++++++++++++++- 2 files changed, 51 insertions(+), 4 deletions(-) diff --git a/Sources/MDNSApple/AppleServiceDiscovery.swift b/Sources/MDNSApple/AppleServiceDiscovery.swift index 7772e37..b489fc9 100644 --- a/Sources/MDNSApple/AppleServiceDiscovery.swift +++ b/Sources/MDNSApple/AppleServiceDiscovery.swift @@ -83,6 +83,26 @@ public final class AppleServiceDiscovery: ServiceDiscovery, @unchecked Sendable self.logger = logger } + // MARK: - TXT Encoding + + /// Encode TXT key/value pairs into RFC 6763 wire format: each entry is a + /// length byte followed by the UTF-8 bytes of `"key=value"`. + /// + /// Hand-rolled because `NetService.data(fromTXTRecord:)` is unavailable on + /// watchOS (and deprecated everywhere else). Keys are sorted so the output + /// is deterministic; entries longer than 255 bytes are truncated, matching + /// the Linux responder's TXT encoding. An empty dictionary yields empty + /// `Data`, which `DNSServiceRegister` accepts as "no TXT record". + static func txtRecordData(_ records: [String: String]) -> Data { + var out = Data() + for (key, value) in records.sorted(by: { $0.key < $1.key }) { + let bytes = Array("\(key)=\(value)".utf8) + out.append(UInt8(min(bytes.count, 255))) + out.append(contentsOf: bytes.prefix(255)) + } + return out + } + // MARK: - ServiceDiscovery public func advertise(service: ServiceRecord) async throws { @@ -96,9 +116,8 @@ public final class AppleServiceDiscovery: ServiceDiscovery, @unchecked Sendable DNSServiceRefDeallocate(ref) } - // Build TXT record payload using the NetService helper for correct wire encoding. - let txtDict = service.txtRecords.mapValues { $0.data(using: .utf8) ?? Data() } - let txtData = NetService.data(fromTXTRecord: txtDict) + // Build the TXT record payload in RFC 6763 wire format. + let txtData = Self.txtRecordData(service.txtRecords) // Build the regtype string. // DNSServiceRegister natively supports comma-separated subtypes: diff --git a/Tests/MDNSAppleTests/AppleServiceDiscoveryTests.swift b/Tests/MDNSAppleTests/AppleServiceDiscoveryTests.swift index f656574..0fceb4c 100644 --- a/Tests/MDNSAppleTests/AppleServiceDiscoveryTests.swift +++ b/Tests/MDNSAppleTests/AppleServiceDiscoveryTests.swift @@ -1,6 +1,7 @@ // AppleServiceDiscoveryTests.swift // Copyright 2026 Monagle Pty Ltd +import Foundation import Testing import MDNSCore @@ -9,7 +10,7 @@ import MDNSCore // This file verifies that the MDNSApple module compiles and types are correct. #if canImport(Network) -import MDNSApple +@testable import MDNSApple @Suite("AppleServiceDiscovery Compile-time Tests") struct AppleServiceDiscoveryTests { @@ -28,4 +29,31 @@ struct AppleServiceDiscoveryTests { _ = e2 } } + +@Suite("TXT Record Encoding") +struct TXTRecordEncodingTests { + + @Test("Entries are length-prefixed key=value, sorted by key") + func wireFormat() { + let data = AppleServiceDiscovery.txtRecordData(["id": "AA:BB", "c#": "1"]) + var expected = Data() + expected.append(4) + expected.append(contentsOf: Array("c#=1".utf8)) + expected.append(8) + expected.append(contentsOf: Array("id=AA:BB".utf8)) + #expect(data == expected) + } + + @Test("Empty dictionary encodes as empty data") + func emptyRecords() { + #expect(AppleServiceDiscovery.txtRecordData([:]).isEmpty) + } + + @Test("Entries longer than 255 bytes are truncated") + func oversizedEntry() { + let data = AppleServiceDiscovery.txtRecordData(["k": String(repeating: "x", count: 300)]) + #expect(data.count == 256) + #expect(data.first == 255) + } +} #endif From 03acacab68fd8ef7eafd31f3e7e1c21d2d77def8 Mon Sep 17 00:00:00 2001 From: David Monagle Date: Sun, 9 Aug 2026 15:38:46 +1000 Subject: [PATCH 3/4] Lower iOS/tvOS/watchOS/visionOS floors to the macOS 14 generation With macOS lowered to 14 (PR #1), align the remaining Apple platforms on the same SDK generation: iOS 17, tvOS 17, watchOS 10, visionOS 1. The newest APIs in use are Duration and Task.sleep(for:), whose floors are the macOS 13 generation, so all of these clear comfortably. Verified by cross-compiling each platform against its SDK. watchOS additionally needed the NetService removal in the previous commit to compile at any version. --- Package.swift | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Package.swift b/Package.swift index d61fdc9..c5ece9c 100644 --- a/Package.swift +++ b/Package.swift @@ -5,10 +5,10 @@ let package = Package( name: "mdns-swift", platforms: [ .macOS(.v14), - .iOS(.v18), - .tvOS(.v18), - .watchOS(.v11), - .visionOS(.v2), + .iOS(.v17), + .tvOS(.v17), + .watchOS(.v10), + .visionOS(.v1), ], products: [ .library(name: "MDNSCore", targets: ["MDNSCore"]), From 3b632e8157f61d7655aaa7ac407e8a97cfdbdeef Mon Sep 17 00:00:00 2001 From: David Monagle Date: Sun, 9 Aug 2026 15:39:26 +1000 Subject: [PATCH 4/4] Add cross-compile CI jobs validating Apple platform floors MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit CI previously built only macOS and Linux, so the declared iOS, tvOS, watchOS, and visionOS floors were never validated — which is how the watchOS NetService breakage went unnoticed. Build-only cross-compiles are enough to catch unavailable APIs and availability-floor violations. --- .github/workflows/ci.yml | 32 ++++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 4984c49..3ca6887 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -41,6 +41,38 @@ jobs: - name: Test run: swift test + # ── Apple platform floors ────────────────────────────────────────────────── + # Build-only cross-compiles that validate the platform floors declared in + # Package.swift. Catches APIs that are unavailable on a platform (e.g. + # NetService on watchOS) or newer than a declared floor — neither of which + # the native macOS build can see. + # + # Versionless triples pick up the deployment target from Package.swift. + # visionOS needs an explicit version in the triple because SwiftPM + # synthesizes an invalid default (xros10.13) for a versionless xros triple; + # keep it in sync with the .visionOS floor in Package.swift. + apple-platforms: + name: ${{ matrix.platform.name }} (build only) + runs-on: macos-15 + timeout-minutes: 15 + strategy: + fail-fast: false + matrix: + platform: + - { name: iOS, triple: arm64-apple-ios, sdk: iphoneos } + - { name: tvOS, triple: arm64-apple-tvos, sdk: appletvos } + - { name: watchOS, triple: arm64-apple-watchos, sdk: watchos } + - { name: visionOS, triple: arm64-apple-xros1.0, sdk: xros } + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Build + run: > + swift build + --triple ${{ matrix.platform.triple }} + --sdk "$(xcrun --sdk ${{ matrix.platform.sdk }} --show-sdk-path)" + # ── Linux ────────────────────────────────────────────────────────────────── linux: name: Linux