1

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

Fritz
  • 95
  • 7
  • For those of us who don't know what TLV is, can you explain what the data at different levels in the screenshot mean? I took a look at your GitHub code, it parses a `Data` into a `String`. You will have to define a better data model to display it on the `NSOutlineView` – Code Different May 19 '18 at 23:45
  • Just a short explanation of TLV: It consists of a tag/type, a length, and a value. In the tag there can be a special marker that tells you that the value also consists of TLV coded elements (constructed) or is just a simple value. the length also contains a special marker that tell you how many "bytes" are used for coding the length ... See also: https://www.emvlab.org/tlvutils/ or https://www.openscdp.org/scripts/tutorial/emv/TLV.html – Fritz May 20 '18 at 19:38
  • I want to store the result of parseTLVStream into an array of Node. What is the challange is to set parent and children correctly ... https://github.com/frcocoatst/OutlineTLV – Fritz May 20 '18 at 20:12
  • Putting this on an `NSOutlineView` isn't hard. The hard part is to understand your data structure. For example, the first line has `tag = e1, length = 35` so `value` is the next 53 (`0x35` bytes). Both tag and value is 1-byte long. But while does the second line has a `tag` that lasts 2 bytes, and the third go back to 1 byte? And you put everything into a string, which I would hate to analyze to recapture the data model – Code Different May 21 '18 at 01:48
  • The first E1 tag is a constructed tag, meaning that the following 53(0x35) bytes also contain TLV coded dated. So the first one in this block is a primitive tag 9F1E with 8 bytes. The second one is EF with 18 bytes and it is also a constructed tag, which consists of primitive tag df0d and df7f.... – Fritz May 21 '18 at 06:43
  • Tags and Length can be more than one byte. (But that is already solved in extension of Data func extract()) ... I do think I need something like an array of trees to keep the decoded data, but I have no experience with this – Fritz May 21 '18 at 06:59

1 Answers1

0

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

Code Different
  • 90,614
  • 16
  • 144
  • 163
  • wow! What an excellent compact solution! Thanks a lot. But how would I handle a byte streams that consists of 2 trees? eg.: Just copying the demo data and concatenate them, without wrapping them into another TLV stream … – Fritz May 23 '18 at 13:41
  • I haven't thought of that scenario since I haven't read the TLV specs. Not near my Mac for the next 12 hours or so.Play with the code as much as you can, I'll look at it when I come home tonight – Code Different May 23 '18 at 13:44
  • See my updated answer. I have changed the argument order for `TLVNode.init` and updated the binding info. You should bind the `Content Array` instead of `Content Object` property of the Tree Controller. – Code Different May 24 '18 at 04:03
  • Thanks a lot! Would be nice if Apple would offer similar sample code ... – Fritz May 24 '18 at 18:31
  • I'd like to put a demo onto GitHub (https://github.com/frcocoatst/TLVParsingToOutlineView) Is this okay? If so, which references do I have to put there? – Fritz Jun 08 '18 at 09:56
  • Feel free to put it on Github. You can add a link to this answer and "With help from Code Different" – Code Different Jun 08 '18 at 12:35
  • Anybody a good idea how I can handle insert and remove? If I have a constructed tag and insert another tag or remove a tag, I have to update the length of the constructed tag and also the length of the constructed parent tag and so on. Anybody an elegant solution? – Fritz Oct 29 '18 at 11:04
  • @Fritz modify the `tlvNodes` property as needed, remember to wrap your changes between `willChangeValue` and `didChangeValue` like in the `viewDidLoad` function above so that the tree controller knows it's time to update the outline view. – Code Different Oct 30 '18 at 12:33
  • I think I have to add parent to TLVNode and whenever I add/remove/edit have have to change the length of every parent, till there is no one left. So far I added a Button getSelected, but I done't know how to get the corresponding TLVNode. Maybe you could have a look at https://github.com/frcocoatst/TLVParsingToOutlineViewEdit – Fritz Nov 05 '18 at 10:28