1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
|
// 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<Number, ByteArray or String or Long or Boolean>? {
var fields = {} as Dictionary<Number, ByteArray or String or Long or Boolean>;
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;
}
}
|