I'm implementing a communications protocol in an embedded system and the protocol uses a subset of the ASN.1 BER (Basic Encoding Rules).
In a nutshell, this class simply represents a TLV triplet (Tag, Length, Value) in which the Tag is some label identifying the type of data, the Length is the length of the Value (in bytes) and the Value is simply a set of bytes.
Because it's going to be running on an embedded system, it will not be using any of the following:
- exceptions
- Runtime Type Identification (RTTI)
- most of the Standard Template Library (STL)
- any of the Boost library components
- C++14 (the compiler's not yet smart enough)
Additionally, there are some restrictions built into the project which are important for understanding the context of this class:
- all Tag values fit in a single 8-bit byte
- the maximum message size is 32k; the user of this class must assure this
- minimizing total memory footprint is very important
- assuring adequate performance (speed) is important
- the underlying microprocessor may be 16-bit, or 32-bit or 64-bit
- the underlying microprocessor may be little-endian or big-endian
- there is a macro (not shown) that correctly defines
SHORT_IS_MORE_THAN_TWO_BYTES
for the target architecture. - the
Ber
class will not be derived from (which is why the destructor is not virtual)
I'm interested in a general review, but particularly for clarity and performance of the code. The test code is only for convenience on a computer; the embedded system has no iostream
support.
bertest.cpp
#include <iostream> #include <iomanip> #include "ber.h" std::ostream& operator<<(std::ostream &out, const Ber &b) { unsigned short len = b.totallength(b.msglength()); out << "totallength = " << std::dec << len << '\n'; for (unsigned short i=0; i < len; ++i) { out << std::setfill('0') << std::setw(2) << std::hex << (unsigned short)b[i] << ' '; if (0xf == (i & 0xf)) out << '\n'; } return out; } int main() { uint8_t m1[]{ 71, 72, 73, 74, 75 }; Ber message(0x22, sizeof(m1), m1); std::cout << message << '\n'; Ber message2('e'); Ber message3 = Ber(0x60) + message; std::cout << message3 << '\n'; for (int i=0; i < 100; ++i) { message2 += message; std::cout << message2 << '\n'; } std::cout << "tag for message2 is " << (int)message2[0] << "\n"; std::cout << "out-of-bounds byte for message2 is " << (int)message2[1000] << "\n"; }
ber.h
#ifndef BER_H #define BER_H /*! * Basic Encoding Rules class for TLV (Tag, Length, Value) messages */ class Ber { public: //! constructor Ber(uint8_t tag, unsigned short length=0, const uint8_t *message=nullptr); //! copy constructor Ber(const Ber& b); //! move constructor Ber(Ber &&b); //! destructor ~Ber(); //! returns the number of bytes required to encode the length static unsigned short encodedlen(unsigned short n); //! returns the total number of bytes required to store a Ber object with the indicated message length static unsigned short totallength(unsigned short len); //! create a new Ber object from an existing one Ber operator=(const Ber& rhs); //! append the passed Ber data to the existing Ber object Ber &operator+=(const Ber& rhs); //! return the length of the message for this Ber object unsigned short msglength() const; //! return the byte at the indicated offset or NUL if out of range uint8_t operator[](size_t idx) const; private: //! encodes and stores the length at **ptr and updates and returns pointer static uint8_t *lencode(uint8_t **ptr, unsigned short length); //! internal data pointer uint8_t * _data; }; inline Ber operator+(Ber lhs, const Ber& rhs) { lhs += rhs; return lhs; } #endif
ber.cpp
#include <algorithm> #include "ber.h" Ber::Ber(uint8_t tag, unsigned short length, const uint8_t *message) : _data(new uint8_t[totallength(length)]) { uint8_t *ptr = _data; *ptr++ = tag; std::copy(message, message+length, lencode(&ptr, length)); } Ber::Ber(const Ber& b) : _data(new uint8_t[totallength(b.msglength())]) { std::copy(b._data, b._data+totallength(b.msglength()), _data); } Ber::Ber(Ber &&b) : _data(nullptr) { std::swap(_data, b._data); } Ber::~Ber() { delete _data; } uint8_t *Ber::lencode(uint8_t **ptr, unsigned short length) { switch(encodedlen(length)) { case 1: *(*ptr)++ = (length & 0x7fu); break; case 2: *(*ptr)++ = 0x81u; *(*ptr)++ = (length & 0xffu); break; case 3: *(*ptr)++ = 0x82u; *(*ptr)++ = ((length >> 8) & 0xffu); *(*ptr)++ = (length & 0xffu); break; } return *ptr; } unsigned short Ber::encodedlen(unsigned short n) { if (n <= 0x7fu) return 1; if (n <= 0xffu) return 2; #if SHORT_IS_MORE_THAN_TWO_BYTES // assert(sizeof(unsigned short)>2); if (n <= 0xffffu) return 3; return 0; // error -- encoded length can't be > 3 #else // assert(sizeof(unsigned short)==2); return 3; #endif } unsigned short Ber::totallength(unsigned short len) { return 1u+len+encodedlen(len); } Ber Ber::operator=(const Ber& rhs) { return Ber(rhs); } Ber& Ber::operator+=(const Ber& rhs) { unsigned short datalen = msglength(); unsigned short rhslen = totallength(rhs.msglength()); unsigned short mynewlen = totallength(datalen+rhslen); uint8_t *data_copy(new uint8_t[mynewlen]); uint8_t *ptr = data_copy; *ptr++ = *_data; uint8_t *dataptr = &_data[1+encodedlen(datalen)]; ptr = std::copy(dataptr, dataptr + datalen, lencode(&ptr, datalen+rhslen)); std::copy(rhs._data, rhs._data+rhslen, ptr); std::swap(data_copy, _data); delete data_copy; return *this; } unsigned short Ber::msglength() const { unsigned short len = _data[1] & 0x7f; if (_data[1] != len) { switch(len) { case 2: len = (_data[2] << 8) | _data[3]; break; case 1: len = _data[2]; } } return len; } uint8_t Ber::operator[](size_t idx) const { return (idx > totallength(msglength())) ? '\0' : _data[idx]; }
operator +=
is creating some packet chain? It seems to produce[tag1][len1][load1][tag2][len2][load2]
. Did I get it right?\$\endgroup\$Ber
instances. So it would be[tag1][len1+len2][msg1][tag2][len2][msg2]
. Typical use would be to start with an emptyBer
instance (just a tag) and then add multipleBer
instances to it.\$\endgroup\$