Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 32 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
10 changes: 5 additions & 5 deletions Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,11 @@ import PackageDescription
let package = Package(
name: "mdns-swift",
platforms: [
.macOS(.v15),
.iOS(.v18),
.tvOS(.v18),
.watchOS(.v11),
.visionOS(.v2),
.macOS(.v14),
.iOS(.v17),
.tvOS(.v17),
.watchOS(.v10),
.visionOS(.v1),
],
products: [
.library(name: "MDNSCore", targets: ["MDNSCore"]),
Expand Down
25 changes: 22 additions & 3 deletions Sources/MDNSApple/AppleServiceDiscovery.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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:
Expand Down
30 changes: 29 additions & 1 deletion Tests/MDNSAppleTests/AppleServiceDiscoveryTests.swift
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
// AppleServiceDiscoveryTests.swift
// Copyright 2026 Monagle Pty Ltd

import Foundation
import Testing
import MDNSCore

Expand All @@ -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 {
Expand All @@ -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
Loading