summaryrefslogtreecommitdiff
path: root/src/main/kotlin/NinePMessage.kt
blob: 9edf361659624ef5404df39ee29724a228eee1ef (plain)
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
import kotlin.math.pow

/**
 * A 9P message with the given type, tag, and fields. The message size is calculated automatically.
 *
 * Important note: the field names in [fieldValuesInt] and [fieldValuesStr] (i.e. the keys of their maps) must be
 * mutually exclusive and the union of these two maps' keys must result in a subset of (or a set equal to)
 * [fieldNames]. Calling [write] when these conditions are not met throws an exception.
 *
 * @param type The 9P message type.
 * @param tag The tag given to the message.
 * @param fieldNames The names of the message fields, in the same order they are expected to be sent.
 * @param fieldValuesInt A map of each field name into its value. This map only stores integer values.
 * @param fieldValuesStr A map of each field name into its value. This map only stores string values.
 */
class NinePMessage(val type: NinePMessageType, val tag: UShort, val fieldNames: List<String>, val fieldValuesInt: Map<String, SizedMessageField>, val fieldValuesStr: Map<String, String>) {
    /**
     * Send the message using the given networking API.
     *
     * @param npt The networking API.
     * @throws IllegalArgumentException if [fieldNames], [fieldValuesInt], and [fieldValuesStr] are incoherent.
     */
    fun write(npt: NetworkPacketTransporter) {
        // shorthands
        val insecInts = fieldNames.intersect(fieldValuesInt.keys)
        val insecStrs = fieldNames.intersect(fieldValuesStr.keys)
        // check that names in fieldNames exist as keys in either fieldValuesInt or fieldValuesStr but not both
        require(insecInts.size == fieldNames.size - insecStrs.size)

        val totalSize = 4 + 1 + 2 + insecInts.sumOf { fieldValuesInt[it]!!.size } + insecStrs.sumOf { 2 + fieldValuesStr[it]!!.length }

        writeMessageSizeType(npt, totalSize, type)
        writeInteger(npt, SizedMessageField(2, tag.toInt().toBigInteger()))
        for (field in fieldNames) {
            if (field in insecInts) {
                writeInteger(npt, fieldValuesInt[field]!!)
            } else {
                writeString(npt, fieldValuesStr[field]!!)
            }
        }
    }

    /**
     * Write an integer number to the connection.
     *
     * In 9P, binary numbers (non-textual) are specified in little-endian order (least significant byte first).
     *
     * @param npt The networking API.
     * @param value The number's value. [SizedMessageField] defines both its actual value and its size.
     */
    private fun writeInteger(npt: NetworkPacketTransporter, value: SizedMessageField) {
        npt.transmit(value.value.toByteArray().toList())
    }

    /**
     * Write a string to the connection.
     *
     * In 9P, strings are represented as a 2-byte integer (the string's size) followed by the actual UTF-8 string. The
     * null terminator is forbidden in 9P messages.
     *
     * @param npt The networking API.
     * @param value The string.
     * @throws IllegalArgumentException if the value of the string's size does not fit into 2 bytes.
     */
    private fun writeString(npt: NetworkPacketTransporter, value: String) {
        require(value.length <= 2.0.pow(16.0) - 1)
        writeInteger(npt, SizedMessageField(2, value.length.toBigInteger()))
        npt.transmit(value.toByteArray().toList())
    }

    /**
     * Write the message size and type.
     *
     * @param npt The networking API.
     * @param size The total message size, including the 4 bytes of this parameter and the type's byte.
     * @param type The 9P message type as a [NinePMessageType] constant.
     */
    private fun writeMessageSizeType(npt: NetworkPacketTransporter, size: Int, type: NinePMessageType) {
        writeInteger(npt, SizedMessageField(4, size.toBigInteger()))
        writeInteger(npt, SizedMessageField(1, type.value.toInt().toBigInteger()))
    }
}