From 1d0714e8ddca3221b1b37fbf3fca76c16581ff80 Mon Sep 17 00:00:00 2001 From: George Barnett Date: Mon, 19 Feb 2024 12:10:50 +0100 Subject: [PATCH 1/4] Don't throw on empty responses Motivation: The AsyncDNSResolver currently throws an error when there are no records associated with a name. However, most query responses are arrays or contain arrays. As such it's unclear to callers whether they should be catching an error of a particular type or checking for the presence of values some combination of the two. Modifications: - Modify the `DNSResolver` API to return `nil` where only a single value is expected (e.g. CNAME). - Update documentation on `DNSResolver` to clarify the expected return values when no values are resolved. - Update the c-ares backend to check for no data when parsing and return empty responses - Update the DNSSD backend to pass in `nil` data to the parser when the status is timeout - Remove the `noData` error code as it shouldn't be reachable anymore Result: Queries with no response either return empty or nil --- .../AsyncDNSResolver/AsyncDNSResolver.swift | 24 +- Sources/AsyncDNSResolver/Errors.swift | 5 - .../c-ares/DNSResolver_c-ares.swift | 278 +++++++++++------- .../c-ares/Errors_c-ares.swift | 2 - .../dnssd/DNSResolver_dnssd.swift | 83 +++--- .../c-ares/CAresDNSResolverTests.swift | 8 +- .../dnssd/DNSSDDNSResolverTests.swift | 8 +- 7 files changed, 237 insertions(+), 171 deletions(-) diff --git a/Sources/AsyncDNSResolver/AsyncDNSResolver.swift b/Sources/AsyncDNSResolver/AsyncDNSResolver.swift index 98e866f..a9c17a2 100644 --- a/Sources/AsyncDNSResolver/AsyncDNSResolver.swift +++ b/Sources/AsyncDNSResolver/AsyncDNSResolver.swift @@ -64,12 +64,12 @@ public struct AsyncDNSResolver { } /// See ``DNSResolver/queryCNAME(name:)``. - public func queryCNAME(name: String) async throws -> String { + public func queryCNAME(name: String) async throws -> String? { try await self.underlying.queryCNAME(name: name) } /// See ``DNSResolver/querySOA(name:)``. - public func querySOA(name: String) async throws -> SOARecord { + public func querySOA(name: String) async throws -> SOARecord? { try await self.underlying.querySOA(name: name) } @@ -102,7 +102,7 @@ public protocol DNSResolver { /// - Parameters: /// - name: The name to resolve. /// - /// - Returns: ``ARecord``s for the given name. + /// - Returns: ``ARecord``s for the given name, empty if no records were found. func queryA(name: String) async throws -> [ARecord] /// Lookup AAAA records associated with `name`. @@ -110,7 +110,7 @@ public protocol DNSResolver { /// - Parameters: /// - name: The name to resolve. /// - /// - Returns: ``AAAARecord``s for the given name. + /// - Returns: ``AAAARecord``s for the given name, empty if no records were found. func queryAAAA(name: String) async throws -> [AAAARecord] /// Lookup NS record associated with `name`. @@ -126,23 +126,23 @@ public protocol DNSResolver { /// - Parameters: /// - name: The name to resolve. /// - /// - Returns: CNAME for the given name. - func queryCNAME(name: String) async throws -> String + /// - Returns: CNAME for the given name, `nil` if no record was found. + func queryCNAME(name: String) async throws -> String? /// Lookup SOA record associated with `name`. /// /// - Parameters: /// - name: The name to resolve. /// - /// - Returns: ``SOARecord`` for the given name. - func querySOA(name: String) async throws -> SOARecord + /// - Returns: ``SOARecord`` for the given name, `nil` if no record was found. + func querySOA(name: String) async throws -> SOARecord? /// Lookup PTR record associated with `name`. /// /// - Parameters: /// - name: The name to resolve. /// - /// - Returns: ``PTRRecord`` for the given name. + /// - Returns: ``PTRRecord`` for the given name, `nil` if no record was found. func queryPTR(name: String) async throws -> PTRRecord /// Lookup MX records associated with `name`. @@ -150,7 +150,7 @@ public protocol DNSResolver { /// - Parameters: /// - name: The name to resolve. /// - /// - Returns: ``MXRecord``s for the given name. + /// - Returns: ``MXRecord``s for the given name, empty if no records were found. func queryMX(name: String) async throws -> [MXRecord] /// Lookup TXT records associated with `name`. @@ -158,7 +158,7 @@ public protocol DNSResolver { /// - Parameters: /// - name: The name to resolve. /// - /// - Returns: ``TXTRecord``s for the given name. + /// - Returns: ``TXTRecord``s for the given name, empty if no records were found. func queryTXT(name: String) async throws -> [TXTRecord] /// Lookup SRV records associated with `name`. @@ -166,7 +166,7 @@ public protocol DNSResolver { /// - Parameters: /// - name: The name to resolve. /// - /// - Returns: ``SRVRecord``s for the given name. + /// - Returns: ``SRVRecord``s for the given name, empty if no records were found. func querySRV(name: String) async throws -> [SRVRecord] } diff --git a/Sources/AsyncDNSResolver/Errors.swift b/Sources/AsyncDNSResolver/Errors.swift index cd462f0..6f6ff7b 100644 --- a/Sources/AsyncDNSResolver/Errors.swift +++ b/Sources/AsyncDNSResolver/Errors.swift @@ -18,7 +18,6 @@ extension AsyncDNSResolver { public struct Error: Swift.Error, Hashable, CustomStringConvertible { public struct Code: Hashable, Sendable { fileprivate enum Value: Hashable, Sendable { - case noData case invalidQuery case serverFailure case notFound @@ -50,8 +49,6 @@ extension AsyncDNSResolver { self.value = value } - public static var noData: Self { Self(.noData) } - public static var invalidQuery: Self { Self(.invalidQuery) } public static var serverFailure: Self { Self(.serverFailure) } @@ -113,8 +110,6 @@ extension AsyncDNSResolver { public var description: String { switch self.code.value { - case .noData: - return "no data: \(self.message)" case .invalidQuery: return "invalid query: \(self.message)" case .serverFailure: diff --git a/Sources/AsyncDNSResolver/c-ares/DNSResolver_c-ares.swift b/Sources/AsyncDNSResolver/c-ares/DNSResolver_c-ares.swift index 7babb5c..6f56746 100644 --- a/Sources/AsyncDNSResolver/c-ares/DNSResolver_c-ares.swift +++ b/Sources/AsyncDNSResolver/c-ares/DNSResolver_c-ares.swift @@ -50,12 +50,12 @@ public class CAresDNSResolver: DNSResolver { } /// See ``DNSResolver/queryCNAME(name:)``. - public func queryCNAME(name: String) async throws -> String { + public func queryCNAME(name: String) async throws -> String? { try await self.ares.query(type: .CNAME, name: name, replyParser: Ares.CNAMEQueryReplyParser.instance) } /// See ``DNSResolver/querySOA(name:)``. - public func querySOA(name: String) async throws -> SOARecord { + public func querySOA(name: String) async throws -> SOARecord? { try await self.ares.query(type: .SOA, name: name, replyParser: Ares.SOAQueryReplyParser.instance) } @@ -267,7 +267,7 @@ extension Ares { init(parser: Parser, _ continuation: CheckedContinuation) { self._handler = { status, buffer, length in - guard status == ARES_SUCCESS else { + guard status == ARES_SUCCESS || status == ARES_ENODATA else { return continuation.resume(throwing: AsyncDNSResolver.Error(code: status)) } @@ -311,13 +311,19 @@ extension Ares { naddrttlsPointer.pointee = CInt(Ares.maxAddresses) let parseStatus = ares_parse_a_reply(buffer, length, nil, addrttlsPointer, naddrttlsPointer) - guard parseStatus == ARES_SUCCESS else { + + switch parseStatus { + case ARES_SUCCESS: + let records = Array(UnsafeBufferPointer(start: addrttlsPointer, count: Int(naddrttlsPointer.pointee))) + .map { ARecord($0) } + return records + + case ARES_ENODATA: + return [] + + default: throw AsyncDNSResolver.Error(code: parseStatus, "failed to parse A query reply") } - - let records = Array(UnsafeBufferPointer(start: addrttlsPointer, count: Int(naddrttlsPointer.pointee))) - .map { ARecord($0) } - return records } } @@ -334,13 +340,19 @@ extension Ares { naddrttlsPointer.pointee = CInt(Ares.maxAddresses) let parseStatus = ares_parse_aaaa_reply(buffer, length, nil, addrttlsPointer, naddrttlsPointer) - guard parseStatus == ARES_SUCCESS else { + + switch parseStatus { + case ARES_SUCCESS: + let records = Array(UnsafeBufferPointer(start: addrttlsPointer, count: Int(naddrttlsPointer.pointee))) + .map { AAAARecord($0) } + return records + + case ARES_ENODATA: + return [] + + default: throw AsyncDNSResolver.Error(code: parseStatus, "failed to parse AAAA query reply") } - - let records = Array(UnsafeBufferPointer(start: addrttlsPointer, count: Int(naddrttlsPointer.pointee))) - .map { AAAARecord($0) } - return records } } @@ -352,64 +364,80 @@ extension Ares { defer { hostentPtrPtr.deallocate() } let parseStatus = ares_parse_ns_reply(buffer, length, hostentPtrPtr) - guard parseStatus == ARES_SUCCESS else { - throw AsyncDNSResolver.Error(code: parseStatus, "failed to parse NS query reply") - } - guard let hostent = hostentPtrPtr.pointee?.pointee else { - throw AsyncDNSResolver.Error(code: .noData, message: "no NS records found") - } + switch parseStatus { + case ARES_SUCCESS: + guard let hostent = hostentPtrPtr.pointee?.pointee else { + return NSRecord(nameservers: []) + } + + let nameServers = toStringArray(hostent.h_aliases) + return NSRecord(nameservers: nameServers ?? []) - let nameServers = toStringArray(hostent.h_aliases) - return NSRecord(nameservers: nameServers ?? []) + case ARES_ENODATA: + return NSRecord(nameservers: []) + + default: + throw AsyncDNSResolver.Error(code: parseStatus, "failed to parse NS query reply") + } } } struct CNAMEQueryReplyParser: AresQueryReplyParser { static let instance = CNAMEQueryReplyParser() - func parse(buffer: UnsafeMutablePointer?, length: CInt) throws -> String { + func parse(buffer: UnsafeMutablePointer?, length: CInt) throws -> String? { let hostentPtrPtr = UnsafeMutablePointer?>.allocate(capacity: 1) defer { hostentPtrPtr.deallocate() } let parseStatus = ares_parse_a_reply(buffer, length, hostentPtrPtr, nil, nil) - guard parseStatus == ARES_SUCCESS else { - throw AsyncDNSResolver.Error(code: parseStatus, "failed to parse CNAME query reply") - } - guard let hostent = hostentPtrPtr.pointee?.pointee else { - throw AsyncDNSResolver.Error(code: .noData, message: "no CNAME record found") - } + switch parseStatus { + case ARES_SUCCESS: + guard let hostent = hostentPtrPtr.pointee?.pointee else { + return nil + } + return String(cString: hostent.h_name) - return String(cString: hostent.h_name) + case ARES_ENODATA: + return nil + default: + throw AsyncDNSResolver.Error(code: parseStatus, "failed to parse CNAME query reply") + } } } struct SOAQueryReplyParser: AresQueryReplyParser { static let instance = SOAQueryReplyParser() - func parse(buffer: UnsafeMutablePointer?, length: CInt) throws -> SOARecord { + func parse(buffer: UnsafeMutablePointer?, length: CInt) throws -> SOARecord? { let soaReplyPtrPtr = UnsafeMutablePointer?>.allocate(capacity: 1) defer { soaReplyPtrPtr.deallocate() } let parseStatus = ares_parse_soa_reply(buffer, length, soaReplyPtrPtr) - guard parseStatus == ARES_SUCCESS else { - throw AsyncDNSResolver.Error(code: parseStatus, "failed to parse SOA query reply") - } + switch parseStatus { + case ARES_SUCCESS: + guard let soaReply = soaReplyPtrPtr.pointee?.pointee else { + return nil + } - guard let soaReply = soaReplyPtrPtr.pointee?.pointee else { - throw AsyncDNSResolver.Error(code: .noData, message: "no SOA record found") + return SOARecord( + mname: soaReply.nsname.map { String(cString: $0) }, + rname: soaReply.hostmaster.map { String(cString: $0) }, + serial: soaReply.serial, + refresh: soaReply.refresh, + retry: soaReply.retry, + expire: soaReply.expire, + ttl: soaReply.minttl + ) + + case ARES_ENODATA: + return nil + + default: + throw AsyncDNSResolver.Error(code: parseStatus, "failed to parse SOA query reply") } - return SOARecord( - mname: soaReply.nsname.map { String(cString: $0) }, - rname: soaReply.hostmaster.map { String(cString: $0) }, - serial: soaReply.serial, - refresh: soaReply.refresh, - retry: soaReply.retry, - expire: soaReply.expire, - ttl: soaReply.minttl - ) } } @@ -423,16 +451,22 @@ extension Ares { defer { hostentPtrPtr.deallocate() } let parseStatus = ares_parse_ptr_reply(buffer, length, dummyAddrPointer, INET_ADDRSTRLEN, AF_INET, hostentPtrPtr) - guard parseStatus == ARES_SUCCESS else { - throw AsyncDNSResolver.Error(code: parseStatus, "failed to parse PTR query record") - } - guard let hostent = hostentPtrPtr.pointee?.pointee else { - throw AsyncDNSResolver.Error(code: .noData, message: "no PTR record found") - } + switch parseStatus { + case ARES_SUCCESS: + guard let hostent = hostentPtrPtr.pointee?.pointee else { + return PTRRecord(names: []) + } + + let hostnames = toStringArray(hostent.h_aliases) + return PTRRecord(names: hostnames ?? []) - let hostnames = toStringArray(hostent.h_aliases) - return PTRRecord(names: hostnames ?? []) + case ARES_ENODATA: + return PTRRecord(names: []) + + default: + throw AsyncDNSResolver.Error(code: parseStatus, "failed to parse PTR query record") + } } } @@ -444,22 +478,27 @@ extension Ares { defer { mxsPointer.deallocate() } let parseStatus = ares_parse_mx_reply(buffer, length, mxsPointer) - guard parseStatus == ARES_SUCCESS else { - throw AsyncDNSResolver.Error(code: parseStatus, "failed to parse MX query record") - } - - var mxRecords = [MXRecord]() - var mxRecordOptional = mxsPointer.pointee?.pointee - while let mxRecord = mxRecordOptional { - mxRecords.append( - MXRecord( - host: String(cString: mxRecord.host), - priority: mxRecord.priority + switch parseStatus { + case ARES_SUCCESS: + var mxRecords = [MXRecord]() + var mxRecordOptional = mxsPointer.pointee?.pointee + while let mxRecord = mxRecordOptional { + mxRecords.append( + MXRecord( + host: String(cString: mxRecord.host), + priority: mxRecord.priority + ) ) - ) - mxRecordOptional = mxRecord.next?.pointee + mxRecordOptional = mxRecord.next?.pointee + } + return mxRecords + + case ARES_ENODATA: + return [] + + default: + throw AsyncDNSResolver.Error(code: parseStatus, "failed to parse MX query record") } - return mxRecords } } @@ -471,21 +510,27 @@ extension Ares { defer { txtsPointer.deallocate() } let parseStatus = ares_parse_txt_reply(buffer, length, txtsPointer) - guard parseStatus == ARES_SUCCESS else { - throw AsyncDNSResolver.Error(code: parseStatus, "failed to parse TXT query reply") - } - var txtRecords = [TXTRecord]() - var txtRecordOptional = txtsPointer.pointee?.pointee - while let txtRecord = txtRecordOptional { - txtRecords.append( - TXTRecord( - txt: String(cString: txtRecord.txt) + switch parseStatus { + case ARES_SUCCESS: + var txtRecords = [TXTRecord]() + var txtRecordOptional = txtsPointer.pointee?.pointee + while let txtRecord = txtRecordOptional { + txtRecords.append( + TXTRecord( + txt: String(cString: txtRecord.txt) + ) ) - ) - txtRecordOptional = txtRecord.next?.pointee + txtRecordOptional = txtRecord.next?.pointee + } + return txtRecords + + case ARES_ENODATA: + return [] + + default: + throw AsyncDNSResolver.Error(code: parseStatus, "failed to parse TXT query reply") } - return txtRecords } } @@ -497,24 +542,30 @@ extension Ares { defer { replyPointer.deallocate() } let parseStatus = ares_parse_srv_reply(buffer, length, replyPointer) - guard parseStatus == ARES_SUCCESS else { - throw AsyncDNSResolver.Error(code: parseStatus, "failed to parse SRV query reply") - } - var srvRecords = [SRVRecord]() - var srvRecordOptional = replyPointer.pointee?.pointee - while let srvRecord = srvRecordOptional { - srvRecords.append( - SRVRecord( - host: String(cString: srvRecord.host), - port: srvRecord.port, - weight: srvRecord.weight, - priority: srvRecord.priority + switch parseStatus { + case ARES_SUCCESS: + var srvRecords = [SRVRecord]() + var srvRecordOptional = replyPointer.pointee?.pointee + while let srvRecord = srvRecordOptional { + srvRecords.append( + SRVRecord( + host: String(cString: srvRecord.host), + port: srvRecord.port, + weight: srvRecord.weight, + priority: srvRecord.priority + ) ) - ) - srvRecordOptional = srvRecord.next?.pointee + srvRecordOptional = srvRecord.next?.pointee + } + return srvRecords + + case ARES_ENODATA: + return [] + + default: + throw AsyncDNSResolver.Error(code: parseStatus, "failed to parse SRV query reply") } - return srvRecords } } @@ -526,26 +577,33 @@ extension Ares { defer { naptrsPointer.deallocate() } let parseStatus = ares_parse_naptr_reply(buffer, length, naptrsPointer) - guard parseStatus == ARES_SUCCESS else { - throw AsyncDNSResolver.Error(code: parseStatus, "failed to parse NAPTR query reply") - } - var naptrRecords = [NAPTRRecord]() - var naptrRecordOptional = naptrsPointer.pointee?.pointee - while let naptrRecord = naptrRecordOptional { - naptrRecords.append( - NAPTRRecord( - flags: String(cString: naptrRecord.flags), - service: String(cString: naptrRecord.service), - regExp: String(cString: naptrRecord.regexp), - replacement: String(cString: naptrRecord.replacement), - order: naptrRecord.order, - preference: naptrRecord.preference + switch parseStatus { + case ARES_SUCCESS: + var naptrRecords = [NAPTRRecord]() + var naptrRecordOptional = naptrsPointer.pointee?.pointee + while let naptrRecord = naptrRecordOptional { + naptrRecords.append( + NAPTRRecord( + flags: String(cString: naptrRecord.flags), + service: String(cString: naptrRecord.service), + regExp: String(cString: naptrRecord.regexp), + replacement: String(cString: naptrRecord.replacement), + order: naptrRecord.order, + preference: naptrRecord.preference + ) ) - ) - naptrRecordOptional = naptrRecord.next?.pointee + naptrRecordOptional = naptrRecord.next?.pointee + } + return naptrRecords + + case ARES_ENODATA: + return [] + + default: + throw AsyncDNSResolver.Error(code: parseStatus, "failed to parse NAPTR query reply") } - return naptrRecords + } } } diff --git a/Sources/AsyncDNSResolver/c-ares/Errors_c-ares.swift b/Sources/AsyncDNSResolver/c-ares/Errors_c-ares.swift index 8e3f2f9..27e53ec 100644 --- a/Sources/AsyncDNSResolver/c-ares/Errors_c-ares.swift +++ b/Sources/AsyncDNSResolver/c-ares/Errors_c-ares.swift @@ -19,8 +19,6 @@ extension AsyncDNSResolver.Error { /// Create an ``AsyncDNSResolver/AsyncDNSResolver/Error`` from c-ares error code. init(code: Int32, _ description: String = "") { switch code { - case ARES_ENODATA: - self = .init(code: .noData, message: description) case ARES_EFORMERR: self = .init(code: .invalidQuery, message: description) case ARES_ESERVFAIL: diff --git a/Sources/AsyncDNSResolver/dnssd/DNSResolver_dnssd.swift b/Sources/AsyncDNSResolver/dnssd/DNSResolver_dnssd.swift index c9a6575..eb0b39e 100644 --- a/Sources/AsyncDNSResolver/dnssd/DNSResolver_dnssd.swift +++ b/Sources/AsyncDNSResolver/dnssd/DNSResolver_dnssd.swift @@ -40,12 +40,12 @@ public struct DNSSDDNSResolver: DNSResolver { } /// See ``DNSResolver/queryCNAME(name:)``. - public func queryCNAME(name: String) async throws -> String { + public func queryCNAME(name: String) async throws -> String? { try await self.dnssd.query(type: .CNAME, name: name, replyHandler: DNSSD.CNAMEQueryReplyHandler.instance) } /// See ``DNSResolver/querySOA(name:)``. - public func querySOA(name: String) async throws -> SOARecord { + public func querySOA(name: String) async throws -> SOARecord? { try await self.dnssd.query(type: .SOA, name: name, replyHandler: DNSSD.SOAQueryReplyHandler.instance) } @@ -170,6 +170,7 @@ struct DNSSD { let records = try await recordStream.reduce(into: []) { partial, record in partial.append(record) } + return try replyHandler.generateReply(records: records) } } @@ -182,14 +183,30 @@ extension DNSSD { private let _handleRecord: (DNSServiceErrorType, UnsafeRawPointer?, UInt16) -> Void init(handler: Handler, _ continuation: AsyncThrowingStream.Continuation) { - self._handleRecord = { errorCode, data, length in - guard errorCode == kDNSServiceErr_NoError else { + self._handleRecord = { errorCode, _data, _length in + let data: UnsafeRawPointer? + let length: UInt16 + + switch Int(errorCode) { + case kDNSServiceErr_NoError: + data = _data + length = _length + case kDNSServiceErr_Timeout: + // DNSSD doesn't give up until it has answer or it times out. If it times out assume + // no answer is available, in which case `data` will be `nil` and parsers will deal + // with empty responses appropriately. + data = nil + length = 0 + default: return continuation.finish(throwing: AsyncDNSResolver.Error(code: .other(Int(errorCode)))) } do { - let record = try handler.parseRecord(data: data, length: length) - continuation.yield(record) + if let record = try handler.parseRecord(data: data, length: length) { + continuation.yield(record) + } else { + continuation.finish() + } } catch { continuation.finish(throwing: error) } @@ -208,7 +225,7 @@ protocol DNSSDQueryReplyHandler { associatedtype Record associatedtype Reply - func parseRecord(data: UnsafeRawPointer?, length: UInt16) throws -> Record + func parseRecord(data: UnsafeRawPointer?, length: UInt16) throws -> Record? func generateReply(records: [Record]) throws -> Reply } @@ -220,9 +237,9 @@ extension DNSSD { struct AQueryReplyHandler: DNSSDQueryReplyHandler { static let instance = AQueryReplyHandler() - func parseRecord(data: UnsafeRawPointer?, length: UInt16) throws -> ARecord { + func parseRecord(data: UnsafeRawPointer?, length: UInt16) throws -> ARecord? { guard let ptr = data?.assumingMemoryBound(to: UInt8.self) else { - throw AsyncDNSResolver.Error(code: .noData) + return nil } guard length >= MemoryLayout.size else { @@ -243,9 +260,9 @@ extension DNSSD { struct AAAAQueryReplyHandler: DNSSDQueryReplyHandler { static let instance = AAAAQueryReplyHandler() - func parseRecord(data: UnsafeRawPointer?, length: UInt16) throws -> AAAARecord { + func parseRecord(data: UnsafeRawPointer?, length: UInt16) throws -> AAAARecord? { guard let ptr = data?.assumingMemoryBound(to: UInt8.self) else { - throw AsyncDNSResolver.Error(code: .noData) + return nil } guard length >= MemoryLayout.size else { @@ -266,9 +283,9 @@ extension DNSSD { struct NSQueryReplyHandler: DNSSDQueryReplyHandler { static let instance = NSQueryReplyHandler() - func parseRecord(data: UnsafeRawPointer?, length: UInt16) throws -> String { + func parseRecord(data: UnsafeRawPointer?, length: UInt16) throws -> String? { guard let ptr = data?.assumingMemoryBound(to: UInt8.self) else { - throw AsyncDNSResolver.Error(code: .noData) + return nil } let bufferPtr = UnsafeBufferPointer(start: ptr, count: Int(length)) @@ -289,9 +306,9 @@ extension DNSSD { struct CNAMEQueryReplyHandler: DNSSDQueryReplyHandler { static let instance = CNAMEQueryReplyHandler() - func parseRecord(data: UnsafeRawPointer?, length: UInt16) throws -> String { + func parseRecord(data: UnsafeRawPointer?, length: UInt16) throws -> String? { guard let ptr = data?.assumingMemoryBound(to: UInt8.self) else { - throw AsyncDNSResolver.Error(code: .noData) + return nil } let bufferPtr = UnsafeBufferPointer(start: ptr, count: Int(length)) @@ -304,17 +321,17 @@ extension DNSSD { return cname } - func generateReply(records: [String]) throws -> String { - try self.ensureOne(records: records) + func generateReply(records: [String]) throws -> String? { + try self.ensureAtMostOne(records: records) } } struct SOAQueryReplyHandler: DNSSDQueryReplyHandler { static let instance = SOAQueryReplyHandler() - func parseRecord(data: UnsafeRawPointer?, length: UInt16) throws -> SOARecord { + func parseRecord(data: UnsafeRawPointer?, length: UInt16) throws -> SOARecord? { guard let ptr = data?.assumingMemoryBound(to: UInt8.self) else { - throw AsyncDNSResolver.Error(code: .noData) + return nil } let bufferPtr = UnsafeBufferPointer(start: ptr, count: Int(length)) @@ -341,17 +358,17 @@ extension DNSSD { ) } - func generateReply(records: [SOARecord]) throws -> SOARecord { - try self.ensureOne(records: records) + func generateReply(records: [SOARecord]) throws -> SOARecord? { + try self.ensureAtMostOne(records: records) } } struct PTRQueryReplyHandler: DNSSDQueryReplyHandler { static let instance = PTRQueryReplyHandler() - func parseRecord(data: UnsafeRawPointer?, length: UInt16) throws -> String { + func parseRecord(data: UnsafeRawPointer?, length: UInt16) throws -> String? { guard let ptr = data?.assumingMemoryBound(to: UInt8.self) else { - throw AsyncDNSResolver.Error(code: .noData) + return nil } let bufferPtr = UnsafeBufferPointer(start: ptr, count: Int(length)) @@ -372,9 +389,9 @@ extension DNSSD { struct MXQueryReplyHandler: DNSSDQueryReplyHandler { static let instance = MXQueryReplyHandler() - func parseRecord(data: UnsafeRawPointer?, length: UInt16) throws -> MXRecord { + func parseRecord(data: UnsafeRawPointer?, length: UInt16) throws -> MXRecord? { guard let ptr = data?.assumingMemoryBound(to: UInt8.self) else { - throw AsyncDNSResolver.Error(code: .noData) + return nil } let bufferPtr = UnsafeBufferPointer(start: ptr, count: Int(length)) @@ -399,9 +416,9 @@ extension DNSSD { struct TXTQueryReplyHandler: DNSSDQueryReplyHandler { static let instance = TXTQueryReplyHandler() - func parseRecord(data: UnsafeRawPointer?, length: UInt16) throws -> TXTRecord { + func parseRecord(data: UnsafeRawPointer?, length: UInt16) throws -> TXTRecord? { guard let ptr = data?.assumingMemoryBound(to: UInt8.self) else { - throw AsyncDNSResolver.Error(code: .noData) + return nil } let txt = String(cString: ptr.advanced(by: 1)) return TXTRecord(txt: txt) @@ -415,9 +432,9 @@ extension DNSSD { struct SRVQueryReplyHandler: DNSSDQueryReplyHandler { static let instance = SRVQueryReplyHandler() - func parseRecord(data: UnsafeRawPointer?, length: UInt16) throws -> SRVRecord { + func parseRecord(data: UnsafeRawPointer?, length: UInt16) throws -> SRVRecord? { guard let ptr = data?.assumingMemoryBound(to: UInt8.self) else { - throw AsyncDNSResolver.Error(code: .noData) + return nil } let bufferPtr = UnsafeBufferPointer(start: ptr, count: Int(length)) @@ -457,14 +474,12 @@ extension DNSSDQueryReplyHandler { } @available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) - func ensureOne(records: [R]) throws -> R { + func ensureAtMostOne(records: [R]) throws -> R? { guard records.count <= 1 else { throw AsyncDNSResolver.Error(code: .badResponse, message: "expected 1 record but got \(records.count)") } - guard let record = records.first else { - throw AsyncDNSResolver.Error(code: .noData) - } - return record + + return records.first } } diff --git a/Tests/AsyncDNSResolverTests/c-ares/CAresDNSResolverTests.swift b/Tests/AsyncDNSResolverTests/c-ares/CAresDNSResolverTests.swift index de5d19e..3a9ec9f 100644 --- a/Tests/AsyncDNSResolverTests/c-ares/CAresDNSResolverTests.swift +++ b/Tests/AsyncDNSResolverTests/c-ares/CAresDNSResolverTests.swift @@ -63,17 +63,17 @@ final class CAresDNSResolverTests: XCTestCase { func test_queryCNAME() async throws { let reply = try await self.resolver.queryCNAME(name: "www.apple.com") if self.verbose { - print("test_queryCNAME: \(reply)") + print("test_queryCNAME: \(String(describing: reply))") } - XCTAssertFalse(reply.isEmpty, "should have CNAME") + XCTAssertNotNil(reply?.isEmpty ?? true, "should have CNAME") } func test_querySOA() async throws { let reply = try await self.resolver.querySOA(name: "apple.com") if self.verbose { - print("test_querySOA: \(reply)") + print("test_querySOA: \(String(describing: reply))") } - XCTAssertFalse(reply.mname?.isEmpty ?? true, "should have nameserver") + XCTAssertFalse(reply?.mname?.isEmpty ?? true, "should have nameserver") } func test_queryPTR() async throws { diff --git a/Tests/AsyncDNSResolverTests/dnssd/DNSSDDNSResolverTests.swift b/Tests/AsyncDNSResolverTests/dnssd/DNSSDDNSResolverTests.swift index 10aeb28..84ef978 100644 --- a/Tests/AsyncDNSResolverTests/dnssd/DNSSDDNSResolverTests.swift +++ b/Tests/AsyncDNSResolverTests/dnssd/DNSSDDNSResolverTests.swift @@ -59,17 +59,17 @@ final class DNSSDDNSResolverTests: XCTestCase { func test_queryCNAME() async throws { let reply = try await self.resolver.queryCNAME(name: "www.apple.com") if self.verbose { - print("test_queryCNAME: \(reply)") + print("test_queryCNAME: \(String(describing: reply))") } - XCTAssertFalse(reply.isEmpty, "should have CNAME") + XCTAssertFalse(reply?.isEmpty ?? true, "should have CNAME") } func test_querySOA() async throws { let reply = try await self.resolver.querySOA(name: "apple.com") if self.verbose { - print("test_querySOA: \(reply)") + print("test_querySOA: \(String(describing: reply))") } - XCTAssertFalse(reply.mname?.isEmpty ?? true, "should have nameserver") + XCTAssertFalse(reply?.mname?.isEmpty ?? true, "should have nameserver") } func test_queryPTR() async throws { From 5c13d65d6d4429b3a99811265f78a4a558db429d Mon Sep 17 00:00:00 2001 From: George Barnett Date: Mon, 4 Mar 2024 16:08:17 +0000 Subject: [PATCH 2/4] fixup tests --- Tests/AsyncDNSResolverTests/c-ares/AresErrorTests.swift | 4 ++-- .../AsyncDNSResolverTests/c-ares/CAresDNSResolverTests.swift | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/Tests/AsyncDNSResolverTests/c-ares/AresErrorTests.swift b/Tests/AsyncDNSResolverTests/c-ares/AresErrorTests.swift index 76c2970..299550f 100644 --- a/Tests/AsyncDNSResolverTests/c-ares/AresErrorTests.swift +++ b/Tests/AsyncDNSResolverTests/c-ares/AresErrorTests.swift @@ -18,10 +18,10 @@ import XCTest final class AresErrorTests: XCTestCase { func test_initFromCode() { - let code = ARES_ENODATA + let code = ARES_EFORMERR let error = AsyncDNSResolver.Error(code: code, "some error") - XCTAssertEqual(error.code, .noData) + XCTAssertEqual(error.code, .invalidQuery) XCTAssertEqual(error.message, "some error", "Expected description to be \"some error\", got \(error.message)") } } diff --git a/Tests/AsyncDNSResolverTests/c-ares/CAresDNSResolverTests.swift b/Tests/AsyncDNSResolverTests/c-ares/CAresDNSResolverTests.swift index 3a9ec9f..0f811fe 100644 --- a/Tests/AsyncDNSResolverTests/c-ares/CAresDNSResolverTests.swift +++ b/Tests/AsyncDNSResolverTests/c-ares/CAresDNSResolverTests.swift @@ -159,14 +159,14 @@ final class CAresDNSResolverTests: XCTestCase { try await run { i in let reply = try await self.resolver.queryCNAME(name: "www.apple.com") if self.verbose { - print("[CNAME] run #\(i) result: \(reply)") + print("[CNAME] run #\(i) result: \(String(describing: reply))") } } try await run { i in let reply = try await self.resolver.querySOA(name: "apple.com") if self.verbose { - print("[SOA] run #\(i) result: \(reply)") + print("[SOA] run #\(i) result: \(String(describing: reply))") } } From ec66b3e1705ae120f3741679733e5e794ff8c02c Mon Sep 17 00:00:00 2001 From: George Barnett Date: Mon, 4 Mar 2024 18:20:28 +0000 Subject: [PATCH 3/4] formatting --- Sources/AsyncDNSResolver/c-ares/DNSResolver_c-ares.swift | 2 -- 1 file changed, 2 deletions(-) diff --git a/Sources/AsyncDNSResolver/c-ares/DNSResolver_c-ares.swift b/Sources/AsyncDNSResolver/c-ares/DNSResolver_c-ares.swift index 6f56746..d5218a7 100644 --- a/Sources/AsyncDNSResolver/c-ares/DNSResolver_c-ares.swift +++ b/Sources/AsyncDNSResolver/c-ares/DNSResolver_c-ares.swift @@ -437,7 +437,6 @@ extension Ares { default: throw AsyncDNSResolver.Error(code: parseStatus, "failed to parse SOA query reply") } - } } @@ -603,7 +602,6 @@ extension Ares { default: throw AsyncDNSResolver.Error(code: parseStatus, "failed to parse NAPTR query reply") } - } } } From 6a611c7c5163927f11c0e7c2769b77522d653185 Mon Sep 17 00:00:00 2001 From: George Barnett Date: Mon, 4 Mar 2024 18:28:02 +0000 Subject: [PATCH 4/4] fix doc --- Sources/AsyncDNSResolver/AsyncDNSResolver.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/AsyncDNSResolver/AsyncDNSResolver.swift b/Sources/AsyncDNSResolver/AsyncDNSResolver.swift index a9c17a2..1c3fed7 100644 --- a/Sources/AsyncDNSResolver/AsyncDNSResolver.swift +++ b/Sources/AsyncDNSResolver/AsyncDNSResolver.swift @@ -142,7 +142,7 @@ public protocol DNSResolver { /// - Parameters: /// - name: The name to resolve. /// - /// - Returns: ``PTRRecord`` for the given name, `nil` if no record was found. + /// - Returns: ``PTRRecord`` for the given name. func queryPTR(name: String) async throws -> PTRRecord /// Lookup MX records associated with `name`.