summaryrefslogtreecommitdiff
path: root/source/storage/Codec.mc
blob: 469ba8274db1566ccc5f2f44a1bbb4e75492ddeb (plain) (blame)
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;
    }
}