Swift4 displaying Parser result in an NSOutlineView

419 Views Asked by At

I wrote a TLV parser that returns results like the following tag length value:

 <e1> 53 <9f1e0831 36303231 343337ef…> 
     <9f1e> 8 <31363032 31343337> 
     <ef> 18 <df0d084d 3030302d 4d5049df 7f04312d 3232> 
         <df0d> 8 <4d303030 2d4d5049> 
         <df7f> 4 <312d3232> 
     <ef> 20 <df0d0b4d 3030302d 54455354 4f53df7f 03362d35> 
         <df0d> 11 <4d303030 2d544553 544f53> 
         <df7f> 3 <362d35>

I want to display this in an OutlineView, but I'm not familiar how the store object should look like and how to fill it up. Somehow it needs to be something like below:

class Node: NSObject {
    var isConstructed = false
    var tag = „Tag“
    var length = 0
    var value = „Value“
    var children = [Node]()
    weak var parent: Node?

    override init() {
       super.init()
    }


    init(tag: String) {
      self.tag = tag
    }
    init(length: Int) {
      self.length = length
    }
    init(value: String) {
      self.value = value
    }
    init(isConstructed: Bool) {
      self.isConstructed = isConstructed
    }

    func isLeaf() -> Bool {
      return children.isEmpty
    }
}

The TLV parser demo TLVparser

Should look like this: TLV parse result in NSOutlineView

1

There are 1 best solutions below

9
On BEST ANSWER

Finally got some time to work on this question today. Putting this on an NSOutlineView is the easy part. The hard part is to define a data model for your TLV data and parse the Data into it.

TLVNode.swift

This class stores the TLV data. Your code looks like converted C and then it's not very good C either.

import Foundation

// Since we use Cocoa Bindings, all properties need to be
// dynamically dispatch hence @objCMembers declaration
// Its ability to handle erroneous data is not tested. That
// is left for the OP as an exercise.
@objcMembers class TLVNode: NSObject {
    var tag: Data
    var length: Int
    var value: Data
    var isConstructed: Bool
    var children = [TLVNode]()

    // Convert `tag` from Data to a string of hex for display
    var displayTag: String {
        // Pad the hex value with 0 so it outputs `0d` instead of just `d`
        return tag.map { ("0" + String($0, radix: 16)).suffix(2) }.joined()
    }

    // Convert `value` from Data to string of hex
    var displayValue: String {
        let hexValues = value.map { ("0" + String($0, radix: 16)).suffix(2) }

        var str = ""
        for (index, hex) in hexValues.enumerated() {
            if index > 0 && index % 4 == 0 {
                str += " " + hex
            } else {
                str += hex
            }
        }
        return str
    }

    convenience init?(dataStream: Data) {
        var size = 0
        self.init(dataStream: dataStream, offset: 0, size: &size)
    }

    static func create(from dataStream: Data) -> [TLVNode] {
        var size = 0
        var offset = 0
        var nodes = [TLVNode]()

        while let node = TLVNode(dataStream: dataStream, offset: offset, size: &size) {
            nodes.append(node)
            offset += size
        }
        return nodes
    }

    /// Intialize a TLVNode object from a data stream
    ///
    /// - Parameters:
    ///   - dataStream: The serialized data stream, in TLV encoding
    ///   - offset: The location from which parsing of the data stream starts
    ///   - size: Upon return, the number of bytes that the node occupies
    private init?(dataStream: Data, offset: Int, size: inout Int) {
        // A node must have at least 3 bytes
        guard offset < dataStream.count - 3 else { return nil }

        // The number of bytes that `tag` occupies
        let m = dataStream[offset] & 0x1F == 0x1F ?
            2 + dataStream[(offset + 1)...].prefix(10).prefix(while: { $0 & 0x80 == 0x80 }).count : 1

        // The number of bytes that `length` occupies
        let n = dataStream[offset + m] & 0x80 == 0x80 ? Int(dataStream[offset + m] & 0x7f) : 1
        guard n <= 3 else { return nil }

        self.tag           = Data(dataStream[offset ..< (offset + m)])
        self.length        = dataStream[(offset + m) ..< (offset + m + n)].map { Int($0) }.reduce(0) { result, element in result * 0x100 + element }
        self.value         = Data(dataStream[(offset + m + n) ..< (offset + m + n + length)])
        self.isConstructed = dataStream[offset] & 0x20 == 0x20

        size = m + n + length
        if self.isConstructed {
            var childOffset = 0
            var childNodeSize = 0

            while let childNode = TLVNode(dataStream: self.value, offset: childOffset, size: &childNodeSize) {
                self.children.append(childNode)
                childOffset += childNodeSize
            }
        }
    }

    private func generateDescription(indentation: Int) -> String {
        return "\(String(repeating: " ", count: indentation))\(tag as NSData) \(length) \(value as NSData)\n"
            + children.map { $0.generateDescription(indentation: indentation + 4) }.joined()
    }

    // Use this property when you need to quickly dump something to the debug console
    override var description: String {
        return self.generateDescription(indentation: 0)
    }

    // A more detailed view on the properties of the current instance
    // Does not include child nodes.
    override var debugDescription: String {
        return """
        TAG         = \(tag as NSData)
        LENGTH      = \(length)
        VALUE       = \(value as NSData)
        CONSTRUCTED = \(isConstructed)
        """
    }
}

View Controller

import Cocoa

class ViewController: NSViewController {
    @objc var tlvNodes: [TLVNode]!

    override func viewDidLoad() {
        super.viewDidLoad()

        let data = Data(bytes:
            [   0xe1,0x35,
                0x9f,0x1e,0x08,0x31,0x36,0x30,0x32,0x31,0x34,0x33,0x37,
                0xef,0x12,
                0xdf,0x0d,0x08,0x4d,0x30,0x30,0x30,0x2d,0x4d,0x50,0x49,
                0xdf,0x7f,0x04,0x31,0x2d,0x32,0x32,
                0xef,0x14,
                0xdf,0x0d,0x0b,0x4d,0x30,0x30,0x30,0x2d,0x54,0x45,0x53,0x54,0x4f,0x53,
                0xdf,0x7f,0x03,0x36,0x2d,0x35,
                // A repeat of the data above
                0xe1,0x35,
                0x9f,0x1e,0x08,0x31,0x36,0x30,0x32,0x31,0x34,0x33,0x37,
                0xef,0x12,
                0xdf,0x0d,0x08,0x4d,0x30,0x30,0x30,0x2d,0x4d,0x50,0x49,
                0xdf,0x7f,0x04,0x31,0x2d,0x32,0x32,
                0xef,0x14,
                0xdf,0x0d,0x0b,0x4d,0x30,0x30,0x30,0x2d,0x54,0x45,0x53,0x54,0x4f,0x53,
                0xdf,0x7f,0x03,0x36,0x2d,0x35
            ])

        // The Tree Controller won't know when we assign `tlvNode` to
        // an entirely new object. So we need to give it a notification
        let nodes = TLVNode.create(from: data)
        self.willChangeValue(forKey: "tlvNodes")
        self.tlvNodes = nodes
        self.didChangeValue(forKey: "tlvNodes")
    }
}

Interface Builder Setup

We will use Cocoa Bindings since populating an Outline View manually can be quite tedious (see my other answer) and your example screenshot look like you are already heading in that direction. A word of caution: while Cocoa Binding is very convenient, it should be considered an advanced topic since it's rather hard to troubleshoot. On your storyboard:

  1. From the Object library on the right, add a Tree Controller to your scene
  2. Select the Tree Controller, in the Attributes inspector, set Children = children
  3. Drag out an Outline View and configure it with 3 columns. We will name them Tag, Length and Value

Set the Children keypath for the Tree Controller

Open the Bindings Inspector, for the 5 highlight objects, set their bindings as follow:

| IB Object       | Property          | Bind To         | Controller Key  | Model Key Path           |
|-----------------|-------------------|-----------------|-----------------|--------------------------|
| Tree Controller | Controller Array  | View Controller |                 | self.tlvNodes            |
| Outline View    | Content           | Tree Controller | arrangedObjects |                          |
| Table View Cell | Value             | Table Cell View |                 | objectValue.displayTag   |
| (Tag column)    |                   |                 |                 |                          |
| Table View Cell | Value             | Table Cell View |                 | objectValue.length       |
| (Length column) |                   |                 |                 |                          |
| Table View Cell | Value             | Table Cell View |                 | objectValue.displayValue |
| (Value column)  |                   |                 |                 |                          |

set Bindings for the highlighted objects in the scene hierarchy

Result:

Showing multiple root nodes in an outline view