I have a project (ldaplib) I am working on that needs to do ASN1-BER decoding and encoding. The function I have for the decoding portion is slightly more complex but neither are all that complicated. I would like to get some feedback on the overall approach as well as the code/exceptions/inline comments.
The decoding specifically uses a named tuple defined much higher in the code as a return type which is then used in many places throughout the code:
BER = namedtuple('BER', 'cls pc tag value')
The encoding portion:
def ber_encode(ber_cls, ber_pc, ber_tag, message):
""" Encode a message into ASN1-BER (TLV) bytes.
The ber_cls, ber_pc, and ber_tag should be numbers, the message can be
bytes, str, or int. """
if type(message) not in (str, int, bytes):
raise TypeError("Requires str, int, or bytes object.")
encoded = (ber_cls + ber_pc + ber_tag).to_bytes(1, byteorder='big')
if type(message) is int:
bytelength = (message.bit_length() // 8) + 1
message = message.to_bytes(bytelength, byteorder='big')
elif type(message) is str:
message = message.encode(encoding='ascii')
if len(message) == 0:
return encoded + int(0).to_bytes(1, byteorder='big')
# Short form or Long form?
length = len(message)
if length < 0x80:
encoded += length.to_bytes(1, byteorder='big')
else:
bytelength = (length.bit_length() // 8) + 1
encoded += (0x80 + bytelength).to_bytes(1, byteorder='big')
encoded += length.to_bytes(bytelength, byteorder='big')
# Add the message
encoded += message
return encoded
The decoding portion:
def ber_decode(message, result=None):
""" Decode a message from ASN1-BER (TLV) bytes into a list of byte values.
Values that contain embedded types will need to call this function to
break the objects into their individual type/value components. """
if type(message) is not bytes:
raise TypeError("Requires bytes object.")
if not result:
result = []
if len(message) == 0:
raise Exception("No message")
# Use masks to isolate the binary parts
ber_type = message[0] # NOQA
ber_cls = ber_type & 0b11000000
ber_pc = ber_type & 0b00100000
ber_tag = ber_type & 0b00011111
ber_len = message[1]
# Assume that the ber_value is offset 2 or is blank
offset = 2
# Only the definite form of length encoding is used.
if ber_len > 0x80:
nbytes = ber_len & 0x7f
ber_len = int.from_bytes(
message[offset:offset + nbytes], byteorder='big')
offset = offset + nbytes
ber_value = message[offset: offset + ber_len]
else:
ber_value = message[offset: offset + ber_len]
# Append the decoded message part.
result.append(BER(ber_cls, ber_pc, ber_tag, ber_value))
message = message[ber_len + offset:]
# If the message had only one type then we are done, if not recurse.
if len(message) == 0:
return result
else:
return ber_decode(message, result)