Program Listing for File ndef-record.cpp

Return to documentation for file (src/ndef-record.cpp)

#include <cassert>
#include <codecvt>
#include <deque>
#include <iostream>
#include <locale>
#include <string>

#include "ndef-lite/encoding.hpp"
#include "ndef-lite/exceptions.hpp"
#include "ndef-lite/record-header.hpp"
#include "ndef-lite/record.hpp"
#include "ndef-lite/util.hpp"

#define BOM_BE_1ST static_cast<char>('\xef')
#define BOM_LE_2ND static_cast<char>('\xff')

using namespace std;
using namespace util;

NDEFRecord::NDEFRecord()
{
  this->record_type = NDEFRecordType{};
  this->id_field = "";
  this->payload_data = vector<uint8_t>{};
}

NDEFRecord::NDEFRecord(const vector<uint8_t>& payload, const NDEFRecordType& type, const string& id, size_t offset,
                       bool chunked)
    : record_type(type), id_field(id), chunked(chunked)
{
  // Set payload from payload bytes, skipping bytes as specified by offset
  this->set_payload(vector<uint8_t>{ payload.begin() + offset, payload.end() });
}

NDEFRecord::NDEFRecord(const vector<uint8_t>& payload, const NDEFRecordType& type, size_t offset, bool chunked)
    : record_type(type), chunked(chunked)
{
  // Set payload from payload bytes, skipping bytes as specified by offset
  this->set_payload(vector<uint8_t>{ payload.begin() + offset, payload.end() });
}

uint8_t NDEFRecord::header() const
{
  uint8_t flags = 0x00;

  // Set Type Name Format field
  flags |= static_cast<uint8_t>(this->record_type.id());

  // If record < 256 bytes then this is a short record
  flags |= this->is_short() ? static_cast<uint8_t>(RecordFlag::SR) : 0;

  // If ID field has any value, set ID_LENGTH field
  flags |= (this->id_field.size() > 0) ? static_cast<uint8_t>(RecordFlag::IL) : 0;

  // Check if record is chunked
  flags |= this->is_chunked() ? static_cast<uint8_t>(RecordFlag::CF) : 0;

  return flags;
}

NDEFRecord NDEFRecord::from_bytes(uint8_t bytes[], size_t size, size_t offset)
{
  // Create vector from byte array
  vector<uint8_t> bytesVec(bytes, bytes + size);

  return from_bytes(bytesVec, offset);
}

NDEFRecord NDEFRecord::from_bytes(vector<uint8_t> bytes, size_t offset, size_t& bytes_used)
{
  // Keep track of number of bytes used
  bytes_used = 0;

  if (bytes.size() < 4) {
    // There are at least 4 required octets (field)
    throw NDEFException("Invalid number of octets, must have at least 4");
  }

  // Generate type field from first byte
  auto record_type = NDEFRecordType::from_bytes(bytes, offset);

  // If the bytes were invalid then return early without further parsing (indicates lack of bytes)
  if (record_type.id() == NDEFRecordType::TypeID::Invalid) {
    // No payload and not chunked, invalid payload
    return NDEFRecord{ vector<uint8_t>{}, record_type };
  }

  // Create deque to allow pop_front
  deque<uint8_t> vals(make_move_iterator(bytes.begin()), make_move_iterator(bytes.end()));

  // Pop off front byte (header already created)
  auto header = NDEFRecordHeader::from_byte(pop_front(vals));
  uint8_t type_length = pop_front(vals);

  // Header/type length bytes
  bytes_used += 2;

  uint32_t payload_length;
  if (header.sr) {
    // Payload is a short record, payload is at most 255 bytes long and length is contained in 1 byte
    payload_length = pop_front(vals);

    // One byte used for short record length
    bytes_used += 1;
  } else {
    assertHasValues(vals, 4, "payload length");

    // Create uint32 from next four bytes
    uint8_t payloadLenBytes[4]{ pop_front(vals), pop_front(vals), pop_front(vals), pop_front(vals) };
    payload_length = uint32FromBEBytes(payloadLenBytes);

    // Four bytes used for record length
    bytes_used += 4;
  }

  uint8_t id_length = 0;
  if (header.il) {
    assertHasValue(vals, "ID length");
    id_length = pop_front(vals);

    // One byte used for ID field
    bytes_used += 1;
  }

  // Confirm there are enough bytes to complete type field then populate type characters from bytes
  assertHasValues(vals, type_length, "type length");
  vector<uint8_t> type_bytes = drain_deque(vals, type_length);

  // Create the type field from the bytes, converting them into ASCII characters after validating them
  string type_field;
  for (auto&& chr : type_bytes) {
    if (chr <= 31 || chr == 127) {
      // Invalid character, no ASCII characters [0-31] or 127
      throw NDEFException("Invalid character code " + to_string(chr) + " found in type field");
    }

    // Append valid character to type string
    type_field += chr;
  }

  // type_field length # of bytes used to create type field string
  bytes_used += type_field.length();

  string id_field;

  // Only attempt to extract ID field if the length is > 0
  if (id_length > 0) {
    // Check if there are enough bytes to pull out id field
    assertHasValues(vals, id_length, "ID");

    // Checked that the bytes we require are available, now collect them
    auto id_bytes = drain_deque(vals, id_length);

    // Create string from UTF-8 bytes
    for (auto&& chr : id_bytes) {
      id_field += chr;
    }
  }

  // id_field length # of bytes used to create ID field string
  bytes_used += id_field.length();

  // Collect remaining data as payload after validating length
  assertHasValues(vals, payload_length, "payload");
  auto payload = drain_deque(vals, payload_length);

  // payload # of bytes used as the payload
  bytes_used += payload.size();

  // Successfully built Record object from uint8_t array
  return NDEFRecord{ payload, record_type, id_field, header.cf };
}

vector<uint8_t> NDEFRecord::as_bytes(uint8_t flags) const
{
  // Vector to create record byte array from
  vector<uint8_t> bytes;

  NDEFRecordHeader header{ .tnf = this->record_type.id(),
                           .il = (this->id().length() > 0),
                           .sr = this->is_short(),
                           .cf = this->is_chunked(),
                           // Assume this record is only record, set message end/message begin. NDEFMessage may change
                           .me = true,
                           .mb = true };

  // Add Record Header byte
  bytes.push_back(header.asByte() | flags);

  // Add type length field
  bytes.push_back(record_type.name().length());

  // Add payload length, dependant on the Short Record flag
  if (this->is_short()) {
    assert(payload_data.size() < 256);
    bytes.push_back(static_cast<uint8_t>(payload_data.size()));
  } else {
    uint8_t payloadLen[4] = {
      static_cast<uint8_t>(payload_data.size() >> 24),
      static_cast<uint8_t>(payload_data.size() >> 16),
      static_cast<uint8_t>(payload_data.size() >> 8),
      static_cast<uint8_t>(payload_data.size() >> 0),
    };
    // Not a short record, append all 4 bytes in big endian order
    bytes.insert(bytes.end(), { payloadLen[0], payloadLen[1], payloadLen[2], payloadLen[3] });
  }

  // Add ID length if applicable
  if (header.il && id_field.size() > 0) {
    bytes.push_back(id_field.size());
  }

  // Add type field
  for (auto&& byte : record_type.name()) {
    // Validate each character is valid ASCII
    if (byte <= 31 || byte >= 127) {
      throw NDEFException("Invalid type field character with code " + to_string(byte));
    }

    // Valid ASCII, add it to the output vector
    bytes.emplace_back(static_cast<uint8_t>(byte));
  }

  // Add ID field if present
  if (this->id().length() > 0) {
    bytes.insert(bytes.end(), id_field.begin(), id_field.end());
  }

  // Add payload bytes
  bytes.insert(bytes.end(), payload_data.begin(), payload_data.end());

  // Return span pointing to location of vector in memory with number of bytes in vector
  return bytes;
}

void NDEFRecord::set_payload(const vector<uint8_t>& data)
{
  payload_data = data;

  // Validate the record type is still valid
  this->validate();
}

void NDEFRecord::validate()
{
  // Not sure how this would happen, but reflecting their
  if (payload_data.size() > 0 && (record_type.id() == NDEFRecordType::TypeID::Empty)) {
    record_type = NDEFRecordType(NDEFRecordType::TypeID::Unknown);
  }
}