namespace StudySystemClient.Der { public errordomain DecodeError { INCOMPLETE, INVALID_CONTENT, UNKNOWN_TYPE, } private const uint BASE_HEADER_SIZE = 2; private const uint8 MAX_INT64_BYTES = 8; public static Datum decode(uint8[] bytes, out uint? size = null) throws DecodeError { if (bytes.length < BASE_HEADER_SIZE) { throw new DecodeError.INCOMPLETE( "Message is fewer than %u bytes", BASE_HEADER_SIZE); } uint header_size; var length = decode_length(bytes, out header_size); if (header_size + length > bytes.length) { throw new DecodeError.INCOMPLETE( "Length %u but only %u bytes available", length, bytes.length - header_size); } var content = bytes[header_size:header_size + length]; size = header_size + length; return decode_datum(bytes[0], content); } private static uint decode_length(uint8[] bytes, out uint header_size) throws DecodeError { if ((bytes[1] & 0x80) != 0) { var length_size = bytes[1] & 0x7f; if (BASE_HEADER_SIZE + length_size > bytes.length) { throw new DecodeError.INCOMPLETE( "Length with size %u but only %u bytes available", length_size, bytes.length - BASE_HEADER_SIZE); } var length = 0; for (int i = 0; i < length_size; ++i) length = length << 8 | bytes[BASE_HEADER_SIZE + i]; header_size = BASE_HEADER_SIZE + length_size; return length; } else { header_size = BASE_HEADER_SIZE; return bytes[1]; } } private static Datum decode_datum(uint8 type, uint8[] content) throws DecodeError { if ((type & ~Choice.ID_MASK) == Choice.BASE_TYPE) return new Choice.from_content(type, content); switch (type) { case Boolean.TYPE: return new Boolean.from_content(content); case Integer.TYPE: return new Integer.from_content(content); case Utf8String.TYPE: return new Utf8String.from_content(content); case Sequence.TYPE: return new Sequence.from_content(content); case Null.TYPE: return new Null.from_content(content); case Enumerated.TYPE: return new Enumerated.from_content(content); default: throw new DecodeError.UNKNOWN_TYPE("Unsupported type: %02x", type); } } public abstract class Datum { protected uint8 type; protected uint8[] content; public uint8[] encode() { var buffer = new ByteArray(); buffer.append({type}); if (content.length >= 0x80) { var length_bytes = encode_length(content.length); buffer.append({0x80 | (uint8)length_bytes.length}); buffer.append(length_bytes); } else { buffer.append({(uint8)content.length}); } buffer.append(content); return buffer.data; } private static uint8[] encode_length(uint length) { var buffer = new ByteArray(); int shift = 0; while (length >> (shift + 8) != 0) shift += 8; for (; shift >= 0; shift -= 8) buffer.append({(uint8)(length >> shift)}); return buffer.data; } } public class Boolean : Datum { internal const uint8 TYPE = 0x01; public bool value { get; private set; } public Boolean(bool val) { type = TYPE; content = new uint8[] { val ? 0xff : 0x00 }; value = val; } internal Boolean.from_content(uint8[] bytes) throws DecodeError { type = TYPE; content = bytes; value = decode_bool(content); } internal static bool decode_bool(uint8[] bytes) throws DecodeError { if (bytes.length != 1) { throw new DecodeError.INVALID_CONTENT( "Invalid length for boolean: %u", bytes.length); } switch (bytes[0]) { case 0xff: return true; case 0x00: return false; default: throw new DecodeError.INVALID_CONTENT( "Invalid value of boolean: %02x", bytes[0]); } } } public class Integer : Datum { internal const uint8 TYPE = 0x02; public int64 value { get; private set; } public Integer(int64 val) { type = TYPE; content = encode_int64(val); value = val; } internal Integer.from_content(uint8[] bytes) throws DecodeError { type = TYPE; content = bytes; value = decode_int64(content); } } public class Utf8String : Datum { internal const uint8 TYPE = 0x0c; public string value { get; private set; } public Utf8String(string val) { type = TYPE; content = val.data; value = val; } public Utf8String.from_content(uint8[] bytes) { type = TYPE; content = bytes; value = decode_string(bytes); } private static string decode_string(uint8[] bytes) { var buffer = new uint8[bytes.length + 1]; Memory.copy(buffer, bytes, bytes.length); buffer[bytes.length] = 0; return (string)buffer; } } public class Sequence : Datum { internal const uint8 TYPE = 0x30; public Datum[] value { get; private set; } public Sequence(Datum[] val) { type = TYPE; content = encode_array(val); value = val; } internal Sequence.from_content(uint8[] bytes) throws DecodeError { type = TYPE; content = bytes; value = decode_array(bytes); } private static uint8[] encode_array(Datum[] val) { var buffer = new ByteArray(); foreach (var datum in val) buffer.append(datum.encode()); return buffer.data; } private static Datum[] decode_array(uint8[] bytes) throws DecodeError { var elems = new GenericArray(); uint offset = 0; uint size; while (offset < bytes.length) { elems.add(decode(bytes[offset:], out size)); offset += size; } return elems.data; } } public class Choice : Datum { internal const uint8 BASE_TYPE = 0xa0; internal const uint8 ID_MASK = 0x1f; public int id { get; private set; } public Datum value { get; private set; } public Choice(int id, Datum val) { type = BASE_TYPE | id; content = val.encode(); this.id = id; value = val; } internal Choice.from_content(uint8 type, uint8[] bytes) throws DecodeError { this.type = type; content = bytes; id = type & ID_MASK; value = decode(bytes); } } public class Null : Datum { internal const uint8 TYPE = 0x05; public Null() { type = TYPE; content = new uint8[] {}; } internal Null.from_content(uint8[] bytes) throws DecodeError { if (bytes.length != 0) { throw new DecodeError.INVALID_CONTENT( "Non-empty content for NULL"); } type = TYPE; content = bytes; } } public class Enumerated : Datum { internal const uint8 TYPE = 0x0a; public int64 value { get; private set; } public Enumerated(int64 val) { type = TYPE; content = encode_int64(val); value = val; } internal Enumerated.from_content(uint8[] bytes) throws DecodeError { type = TYPE; content = bytes; value = decode_int64(content); } } private static uint64 twos_complement(uint64 x) { return ~x + 1; } private static int min_bits(bool negative, uint64 x) { int n = 0; if (negative) { while ((x >> (n + 8) & 0xff) != 0xff) n += 8; } else { while (x >> (n + 8) > 0) n += 8; } return n; } private static uint8[] encode_int64(int64 val) { var negative = val < 0; var uval = negative ? twos_complement(val.abs()) : val; var shift = min_bits(negative, uval); var buffer = new ByteArray(); for (; shift >= 0; shift -= 8) buffer.append({(uint8)(uval >> shift)}); if (!negative && (buffer.data[0] & 0x80) != 0) buffer.prepend({0x00}); return buffer.data; } private static int64 decode_int64(uint8[] bytes) throws DecodeError { if (bytes.length > MAX_INT64_BYTES) { throw new DecodeError.INVALID_CONTENT( "int64 too small for %u bytes", bytes.length); } var negative = (bytes[0] & 0x80) != 0; var val = decode_start_val(negative, bytes.length); foreach (var byte in bytes) val = val << 8 | byte; return negative ? -(int64)twos_complement(val) : (int64)val; } private static uint64 decode_start_val(bool negative, uint length) { if (!negative) return 0; var val = 0; for (uint i = 0; i < MAX_INT64_BYTES - length; ++i) val = val << 8 | 0xff; return val; } }