From 5df58e9d2866b836f50b8fd1200b534903229e53 Mon Sep 17 00:00:00 2001 From: Camden Dixie O'Brien Date: Sun, 23 Feb 2025 18:16:30 +0000 Subject: [PATCH] Implement DER decoding for BOOLEAN, INTEGER and UTF8String in Client --- client/src/der.vala | 134 ++++++++++++++++++++++++++++++++++++ client/tests/der_tests.vala | 129 ++++++++++++++++++++++++++++++++-- 2 files changed, 257 insertions(+), 6 deletions(-) diff --git a/client/src/der.vala b/client/src/der.vala index 15c4d5f..d572147 100644 --- a/client/src/der.vala +++ b/client/src/der.vala @@ -1,4 +1,64 @@ namespace StudySystemClient.Der { + public errordomain DecodeError { + INCOMPLETE, + INVALID_CONTENT, + UNKNOWN_TYPE, + } + + private const uint BASE_HEADER_SIZE = 2; + + public static Datum decode(uint8[] bytes) throws DecodeError { + if (bytes.length < BASE_HEADER_SIZE) { + throw new DecodeError.INCOMPLETE( + "Message is fewer than %u bytes", BASE_HEADER_SIZE); + } + + uint header_size = 0; + var length = decode_length(bytes, ref 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]; + return decode_datum(bytes[0], content); + } + + private static uint decode_length(uint8[] bytes, ref 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 { + 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); + default: + throw new DecodeError.UNKNOWN_TYPE("Unsupported type: %02x", + type); + } + } + public abstract class Datum { internal uint8 type; internal uint8[] content; @@ -33,20 +93,56 @@ namespace StudySystemClient.Der { 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; + private const uint8 MAX_BYTES = 8; + + 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); + } + private static uint64 twos_complement(uint64 x) { return ~x + 1; } @@ -77,14 +173,52 @@ namespace StudySystemClient.Der { return buffer.data; } + + private static int64 decode_int64(uint8[] bytes) throws DecodeError { + if (bytes.length > MAX_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_BYTES - length; ++i) + val = val << 8 | 0xff; + return val; + } } 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); + } + + public 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; } } } diff --git a/client/tests/der_tests.vala b/client/tests/der_tests.vala index 412b745..cf3d3ce 100644 --- a/client/tests/der_tests.vala +++ b/client/tests/der_tests.vala @@ -1,22 +1,19 @@ using StudySystemClient.Der; -static bool bytes_equal(uint8[] expected, uint8[] actual) -{ +static bool bytes_equal(uint8[] expected, uint8[] actual) { if (expected.length != actual.length) return false; return Memory.cmp(expected, actual, expected.length) == 0; } -static string bytes_to_string(uint8[] bytes) -{ +static string bytes_to_string(uint8[] bytes) { var s = ""; foreach (var byte in bytes) s += "%02x".printf(byte); return s; } -static void test_encode(Datum datum, uint8[] expected) -{ +static void test_encode(Datum datum, uint8[] expected) { var bytes = datum.encode(); if (!bytes_equal(expected, bytes)) { Test.message("Encoding is incorrect: expected %s got %s", @@ -26,6 +23,72 @@ static void test_encode(Datum datum, uint8[] expected) } } +static void test_decode_boolean(uint8[] bytes, bool expected) { + Boolean boolean; + try { + boolean = decode(bytes) as Boolean; + } catch (DecodeError err) { + Test.message("Decoding failed: %s", err.message); + Test.fail(); + return; + } + + if (boolean == null) { + Test.message("Bytes were not decoded as a BOOLEAN"); + Test.fail(); + return; + } + if (boolean.value != expected) { + Test.message(@"Expected $expected got $(boolean.value)"); + Test.fail(); + return; + } +} + +static void test_decode_integer(uint8[] bytes, int64 expected) { + Integer integer; + try { + integer = decode(bytes) as Integer; + } catch (DecodeError err) { + Test.message("Decoding failed: %s", err.message); + Test.fail(); + return; + } + + if (integer == null) { + Test.message("Bytes were not decoded as a INTEGER"); + Test.fail(); + return; + } + if (integer.value != expected) { + Test.message(@"Expected $expected got $(integer.value)"); + Test.fail(); + return; + } +} + +static void test_decode_utf8string(uint8[] bytes, string expected) { + Utf8String utf8string; + try { + utf8string = decode(bytes) as Utf8String; + } catch (DecodeError err) { + Test.message("Decoding failed: %s", err.message); + Test.fail(); + return; + } + + if (utf8string == null) { + Test.message("Bytes were not decoded as a UTF8String"); + Test.fail(); + return; + } + if (utf8string.value != expected) { + Test.message(@"Expected $expected got $(utf8string.value)"); + Test.fail(); + return; + } +} + void main(string[] args) { Test.init(ref args); @@ -90,6 +153,60 @@ void main(string[] args) { Memory.set(expected[3:], 0x78, 128); test_encode(new Utf8String(string.nfill(128, 'x')), expected); }); + + /* + * Decoding + */ + + Test.add_func("/decode/boolean/true", () => { + test_decode_boolean({0x01, 0x01, 0xff}, true); + }); + Test.add_func("/decode/boolean/false", () => { + test_decode_boolean({0x01, 0x01, 0x00}, false); + }); + Test.add_func("/decode/integer/small/42", () => { + test_decode_integer({0x02, 0x01, 0x2a}, 42); + }); + Test.add_func("/decode/integer/large/1337", () => { + test_decode_integer({0x02, 0x02, 0x05, 0x39}, 1337); + }); + Test.add_func("/decode/integer/sign/128", () => { + test_decode_integer({0x02, 0x02, 0x00, 0x80}, 128); + }); + Test.add_func("/decode/integer/sign/0xbeef", () => { + test_decode_integer({0x02, 0x03, 0x00, 0xbe, 0xef}, 0xbeef); + }); + Test.add_func("/decode/integer/sign/-128", () => { + test_decode_integer({0x02, 0x01, 0x80}, -128); + }); + Test.add_func("/decode/integer/sign/-1337", () => { + test_decode_integer({0x02, 0x02, 0xfa, 0xc7}, -1337); + }); + + Test.add_func("/decode/utf8string/short/foo", () => { + test_decode_utf8string({0x0c, 0x03, 0x66, 0x6f, 0x6f}, "foo"); + }); + Test.add_func("/decode/utf8string/short/bar", () => { + test_decode_utf8string({0x0c, 0x03, 0x62, 0x61, 0x72}, "bar"); + }); + Test.add_func("/decode/utf8string/long/x300", () => { + var bytes = new uint8[304]; + bytes[0] = 0x0c; + bytes[1] = 0x82; + bytes[2] = 0x01; + bytes[3] = 0x2c; + Memory.set(bytes[4:], 0x78, 300); + test_decode_utf8string(bytes, string.nfill(300, 'x')); + }); + Test.add_func("/decode/utf8string/long/x128", () => { + var bytes = new uint8[131]; + bytes[0] = 0x0c; + bytes[1] = 0x81; + bytes[2] = 0x80; + Memory.set(bytes[3:], 0x78, 128); + test_decode_utf8string(bytes, string.nfill(128, 'x')); + }); + Test.run(); }