Writing a TLV Parser in Dart

ยท

5 min read

I experienced the challenges of working with TLV data when setting up an MPOS (Mobile Point of Sale) device. During the process, I struggled to find a suitable TLV parser in Dart that met my specific requirements. This led me to develop my own TLV parser, which I have shared in this article.

A TLV (Tag-Length-Value) parser is a fundamental component used to process data in various communication protocols and data formats. In this article, we will dive into the logic behind a TLV parser implemented in Dart. The parser efficiently extracts TLV elements from a given hexadecimal string and constructs a list of TLV objects representing each parsed element. Let's explore the code and understand its inner workings.

import 'dart:typed_data';

class TLVParser {
  static List<TLV> parse(String tlv) {
    final tlvList = <TLV>[];
    final data = hexToByteArray(tlv);
    getTLVList(data, tlvList);
    return tlvList;
  }

  static void getTLVList(Uint8List data, List<TLV> tlvList) {
    var index = 0;

    while (index < data.length) {
      var isNested = false;

      if ((data[index] & 0x20) == 0x20) {
        isNested = true;
      } else {
        isNested = false;
      }

      Uint8List tag;
      Uint8List length;
      Uint8List value;
      TLV? tlv;

      if ((data[index] & 0x1F) == 0x1F) {
        var lastByte = index + 1;
        while ((data[lastByte] & 0x80) == 0x80) {
          ++lastByte;
        }
        tag = Uint8List.sublistView(data, index, lastByte + 1);
        index += tag.length;
      } else {
        tag = Uint8List(1);
        tag[0] = data[index];
        ++index;

        if (tag[0] == 0x00) {
          break;
        }
      }

      if ((data[index] & 0x80) == 0x80) {
        final n = (data[index] & 0x7F) + 1;
        length = Uint8List.sublistView(data, index, index + n);
        index += length.length;
      } else {
        length = Uint8List(1);
        length[0] = data[index];
        ++index;
      }

      final n = getLengthInt(length);
      value = Uint8List.sublistView(data, index, index + n);
      index += value.length;

      if (isNested) {
        getTLVList(value, tlvList);
      } else {
        tlv = TLV();
        tlv.tag = toHexString(tag);
        tlv.length = toHexString(length);
        tlv.value = toHexString(value);
        tlv.isNested = isNested;
        tlvList.add(tlv);
      }
    }
  }
  static int getLengthInt(Uint8List data) {
    if ((data[0] & 0x80) == 0x80) {
      final n = data[0] & 0x7F;
      var length = 0;
      for (var i = 1; i < n + 1; ++i) {
        length <<= 8;
        length |= (data[i] & 0xFF);
      }
      return length;
    } else {
      return data[0] & 0xFF;
    }
  }
}

  String toHexString(Uint8List data) {
    return data.map((byte) => byte.toRadixString(16).padLeft(2, '0')).join();
  }

 Uint8List hexToByteArray(String hexStr) {
    final result = Uint8List(hexStr.length ~/ 2);
    for (var i = 0; i < hexStr.length ~/ 2; i++) {
      final high = int.parse(hexStr.substring(i * 2, i * 2 + 1), radix: 16);
      final low = int.parse(hexStr.substring(i * 2 + 1, i * 2 + 2), radix: 16);
      result[i] = (high * 16 + low).toUnsigned(8);
    }
    return result;
  }

class TLV {
  late String tag;
  late String length;
  late String value;
  late bool isNested;

  @override
  String toString() {
    return 'Tag: $tag, Length: $length, Value: $value, IsNested: $isNested';
  }
}

The TLVParser class serves as the entry point for parsing TLV data. It provides a static method, parse, which takes a TLV string as input and returns a list of TLV objects. Here's an overview of the parsing process:

  1. Converting Hexadecimal String to Byte Array: The hexToByteArray method is responsible for converting the input hexadecimal string into a Uint8List byte array, which is easier to manipulate. The method iterates over the input string, converts each pair of characters into their respective byte value, and stores them in the resulting byte array.

  2. Parsing TLV Elements: The getTLVList method is the core logic for parsing TLV elements from the byte array. It iterates over the byte array using an index pointer (index) and extracts each TLV element.

  3. Parsing TLV Tag: The TLV tag is extracted from the byte array using the following logic:

  • If the most significant bit (MSB) of the current byte is set (1), indicating an extended tag, the method reads subsequent bytes until it encounters a byte with the MSB unset (0). It constructs a Uint8List tag to store the tag bytes.

  • If the MSB is unset (0), indicating a short tag, the method creates a Uint8List tag with a single byte value and increments the index pointer.

  1. Parsing TLV Length: The TLV length is extracted from the byte array using the following logic:
  • If the MSB of the current byte is set to (1), indicating an extended length, the method reads the subsequent bytes to determine the length value. It constructs a Uint8List length to store the length bytes.

  • If the MSB is unset (0), indicating a short length, the method creates a Uint8List length with a single byte value and increments the index pointer.

  1. Parsing TLV Value: The TLV value is extracted from the byte array based on the determined length. The method constructs a Uint8List value by taking a subarray of the byte array from the current index to the index plus the length.

  2. Handling Nested TLV Elements: If the TLV element is nested (indicated by the fifth bit of the tag byte being set), the method recursively calls itself with the value as the new byte array. This allows the parsing of nested TLV elements within the current TLV element.

  3. Creating TLV Objects: If the TLV element is not nested, a TLV object is created. The toHexString method is used to convert the tag, length, and value byte arrays into hexadecimal strings. The TLV object is then added to the tlvList.

  4. Repeating the Process: The parsing process continues until the index pointer exceeds the length of the byte array.

Helper Methods
The getLengthInt method is responsible for extracting the integer value of a TLV length byte array. It handles both short and extended lengths, performing the necessary bitwise operations to construct the final length value.

The toHexString method converts a Uint8List byte array into a hexadecimal string representation. It uses the map function to convert each byte into a two-character hexadecimal representation and then joins them together.

Conclusion
The TLV parser implemented in Dart provides a reliable way to parse TLV elements from a hexadecimal string. By following the logical steps described above, the parser extracts the tag, length, and value of each TLV element and constructs a list of TLV objects. This parser can be used in various applications and scenarios where TLV-based data formats need to be processed and analyzed. I'd like to hear your thoughts was this helpful ๐Ÿค—