5

The input to dictionary(fromTXTRecord:) comes from the network, potentially from outside the app, or even the device. However, Apple's docs say:

... Fails an assertion if txtData cannot be represented as an NSDictionary object.

Failing an assertion leaves the programmer (me) with no way of handling the error, which seems illogic for a method that processes external data.

If I run this in Terminal on a Mac:

dns-sd -R 'My Service Name' _myservice._tcp local 4567 asdf asdf

my app, running in an iPhone, crashes.

dictionary(fromTXTRecord:) expects the TXT record data (asdf asdf) to be in key=val form. If, like above, a word doesn't contain any = the method won't be able to parse it and fail the assertion.

I see no way of solving this problem other than not using that method at all and implementing my own parsing, which feels wrong.

Am I missing something?

ateijelo
  • 280
  • 1
  • 8
  • 2
    Here is a link to quite good solution that I used: https://lapcatsoftware.com/articles/netservice-nuthouse.html – polech Jun 09 '20 at 14:02

3 Answers3

3

Here's a solution in Swift 4.2, assuming the TXT record has only strings:

/// Decode the TXT record as a string dictionary, or [:] if the data is malformed
public func dictionary(fromTXTRecord txtData: Data) -> [String: String] {

    var result = [String: String]()
    var data = txtData

    while !data.isEmpty {
        // The first byte of each record is its length, so prefix that much data
        let recordLength = Int(data.removeFirst())
        guard data.count >= recordLength else { return [:] }
        let recordData = data[..<(data.startIndex + recordLength)]
        data = data.dropFirst(recordLength)

        guard let record = String(bytes: recordData, encoding: .utf8) else { return [:] }
        // The format of the entry is "key=value"
        // (According to the reference implementation, = is optional if there is no value,
        // and any equals signs after the first are part of the value.)
        // `ommittingEmptySubsequences` is necessary otherwise an empty string will crash the next line
        let keyValue = record.split(separator: "=", maxSplits: 1, omittingEmptySubsequences: false)
        let key = String(keyValue[0])
        // If there's no value, make the value the empty string
        switch keyValue.count {
        case 1:
            result[key] = ""
        case 2:
            result[key] = String(keyValue[1])
        default:
            fatalError()
        }
    }

    return result
}
spudwaffle
  • 2,905
  • 1
  • 22
  • 29
1

I'm still hoping there's something I'm missing here, but in the mean time, I ended up checking the data for correctness and only then calling Apple's own method.

Here's my workaround:

func dictionaryFromTXTRecordData(data: NSData) -> [String:NSData] {
    let buffer = UnsafeBufferPointer<UInt8>(start: UnsafePointer(data.bytes), count: data.length)
    var pos = 0
    while pos < buffer.count {
        let len = Int(buffer[pos])
        if len > (buffer.count - pos + 1) {
            return [:]
        }
        let subdata = data.subdataWithRange(NSRange(location: pos + 1, length: len))
        guard let substring = String(data: subdata, encoding: NSUTF8StringEncoding) else {
            return [:]
        }
        if !substring.containsString("=") {
            return [:]
        }
        pos = pos + len + 1
    }
    return NSNetService.dictionaryFromTXTRecordData(data)
}

I'm using Swift 2 here. All contributions are welcome. Swift 3 versions, Objective-C versions, improvements, corrections.

ateijelo
  • 280
  • 1
  • 8
1

I just ran into this one using Swift 3. In my case the problem only occurred when I used NetService.dictionary(fromTXTRecord:) but did not occur when I switched to Objective-C and called NSNetService dictionaryFromTXTRecord:. When the Objective-C call encounters an entry without an equal sign it creates a key containing the data and shoves it into the dictionary with an NSNull value. From what I can tell the Swift version then enumerates that dictionary and throws a fit when it sees the NSNull. My solution was to add an Objective-C file and a utility function that calls dictionaryFromTXTRecord: and cleans up the results before handing them back to my Swift code.

Belden Fox
  • 1,257
  • 1
  • 8
  • 6
  • Good call. I didn't think about trying from Objective-C. But, just to reiterate my question, isn't this an error on Apple's side? – ateijelo Dec 29 '16 at 18:23
  • The behavior renders the Swift version of this call useless so it seems like a bug. On the other hand it's not clear how Apple would fix it. The NSNull can't be carried over into a Swift dictionary of type [String:Data]. It can't be turned into a nil because that's equivalent to removing the key. It can't be mapped to an empty Data object without losing the distinction between "hello" and "hello="; in Objective-C the former returns a value of NSNull and the latter returns a value of an empty NSData object. So Apple just threw up their hands on this one. – Belden Fox Dec 30 '16 at 20:58