// Author: cirakg & claude ai // Summary: Deterministic TLV byte encoding of record fields, so the same fields always give the same MAC input. // // A field is: tag (1 byte) | type (1 byte) | length (2 bytes, big-endian) | value. // Types: bytes, UTF-8 text, 64-bit integer (8 bytes big-endian), boolean // (1 byte). Fields are written in the order the caller gives, and Record.mc // always gives them in tag order, so encoding is canonical. // // decode() is strict: a truncated field, an unknown type, invalid UTF-8 or a // duplicate tag gives null, never a partial result. import Toybox.Lang; module Codec { const TYPE_BYTES = 1; const TYPE_TEXT = 2; const TYPE_INT = 3; const TYPE_BOOL = 4; const MAX_VALUE_BYTES = 65535; // Builds an encoding field by field. class Writer { var bytes as ByteArray = []b; function initialize() { } function header(tag as Number, type as Number, length as Number) as Void { if (length > MAX_VALUE_BYTES) { throw new Lang.InvalidValueException("Codec: field too long"); } bytes.add(tag); bytes.add(type); bytes.add((length >> 8) & 0xFF); bytes.add(length & 0xFF); } function addBytes(tag as Number, value as ByteArray) as Void { header(tag, TYPE_BYTES, value.size()); bytes.addAll(value); } function addText(tag as Number, value as String) as Void { var utf8 = Bytes.fromUtf8(value); header(tag, TYPE_TEXT, utf8.size()); bytes.addAll(utf8); } function addInt(tag as Number, value as Number or Long) as Void { header(tag, TYPE_INT, 8); bytes.addAll(Bytes.uint64BigEndian(value.toLong())); } function addBool(tag as Number, value as Boolean) as Void { header(tag, TYPE_BOOL, 1); bytes.add(value ? 1 : 0); } } // Tag -> value (ByteArray, String, Long or Boolean), or null when the // encoding is malformed. function decode(data as ByteArray) as Dictionary? { var fields = {} as Dictionary; var i = 0; while (i < data.size()) { if (i + 4 > data.size()) { return null; } var tag = data[i]; var type = data[i + 1]; var length = (data[i + 2] << 8) | data[i + 3]; i += 4; if (i + length > data.size() || fields.hasKey(tag)) { return null; } var value = data.slice(i, i + length); i += length; if (type == TYPE_BYTES) { fields[tag] = value; } else if (type == TYPE_TEXT) { var text = Bytes.toUtf8(value); if (text == null) { return null; } fields[tag] = text; } else if (type == TYPE_INT && length == 8) { var v = 0l; for (var k = 0; k < 8; k++) { v = (v << 8) | value[k].toLong(); } fields[tag] = v; } else if (type == TYPE_BOOL && length == 1 && value[0] <= 1) { fields[tag] = value[0] == 1; } else { return null; } } return fields; } }