summaryrefslogtreecommitdiff
path: root/src/main/kotlin/net/OutMessage.kt
blob: c9ae879eb6dcacedd28e6d8a2dac0c2a69cead4f (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
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
package net

import NinePMessageType
import java.math.BigInteger
import kotlin.math.pow

/**
 * An outgoing 9P message with the given type, tag, and fields. The message size is calculated automatically.
 *
 * Important note: the field names in [fieldValuesInt], [fieldValuesStr], and [fieldValuesRaw] (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 integer field's name into its value and size in bytes.
 * @param fieldValuesStr A map of each string field's name into its value.
 * @param fieldValuesRaw A map of each raw field's name into its value.
 * @param maxSize The maximum message size.
 */
class OutMessage(val type: NinePMessageType, val tag: UShort, val fieldNames: List<String>, val fieldValuesInt: Map<String, Pair<BigInteger, UInt>>, val fieldValuesStr: Map<String, String>, val fieldValuesRaw: Map<String, List<UByte>>, val maxSize: UInt) {
    /**
     * Intersection between [fieldNames] and [fieldValuesInt]. In other words: the integer fields that are going to be
     * used when writing the message.
     */
    private val insecInts = fieldNames.intersect(fieldValuesInt.keys)

    /**
     * Intersection between [fieldNames] and [fieldValuesStr]. In other words: the string fields that are going to be
     * used when writing the message.
     */
    private val insecStrs = fieldNames.intersect(fieldValuesStr.keys)

    /**
     * Intersection between [fieldNames] and [fieldValuesRaw]. In other words: the raw fields that are going to be used
     * when writing the message.
     */
    private val insecRaws = fieldNames.intersect(fieldValuesRaw.keys)

    /**
     * Send the message using the given networking API.
     *
     * @param tl The networking API.
     * @throws IllegalArgumentException if [fieldNames], [fieldValuesInt], and [fieldValuesStr] are incoherent or the
     *         final size of the message exceeds the negotiated value.
     */
    fun write(tl: TransportLayer) {
        // check that names in fieldNames exist as keys in either fieldValuesInt or fieldValuesStr but not both
        require(fieldNames.size == insecInts.size + insecStrs.size + insecRaws.size)

        val totalSize = size()
        if (totalSize > this.maxSize) {
            throw IllegalArgumentException("Message size exceeded.")
        }
        writeMessageSizeTypeTag(tl, totalSize, type, tag)
        for (field in fieldNames) {
            tl.transmit(
                if (field in insecInts) {
                    val valsize = fieldValuesInt[field]!!
                    convIntegerToBytes(valsize.first, valsize.second)
                } else if (field in insecStrs) {
                    convStringToBytes(fieldValuesStr[field]!!)
                } else {
                    fieldValuesRaw[field]!!.toList()
                }
            )
        }
    }

    /**
     * Write the message size and type.
     *
     * @param tl 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.
     * @param tag The 9P message tag.
     */
    private fun writeMessageSizeTypeTag(tl: TransportLayer, size: UInt, type: NinePMessageType, tag: UShort) {
        var bytes: List<UByte> = emptyList()
        bytes += convIntegerToBytes(BigInteger(size.toString()), 4u)
        bytes += convIntegerToBytes(BigInteger(type.value.toString()), 1u)
        bytes += convIntegerToBytes(BigInteger(tag.toString()), 2u)
        tl.transmit(bytes)
    }

    /**
     * Calculate the expected size of the message.
     */
    fun size(): UInt {
        return 4u + 1u + 2u + this.insecInts.sumOf { this.fieldValuesInt[it]!!.second } + this.insecStrs.sumOf { 2u + this.fieldValuesStr[it]!!.length.toUInt() } + this.insecRaws.sumOf { this.fieldValuesRaw[it]!!.size.toUInt() }
    }

    companion object {
        // TODO: Add size that the value is required to fit in

        /**
         * Convert an integer number to its byte representation.
         *
         * In 9P, binary numbers (non-textual) are specified in little-endian order (least significant byte first).
         *
         * @param value The number's value.
         * @param size The number's size in bytes.
         */
        fun convIntegerToBytes(value: BigInteger, size: UInt): List<UByte> {
            var bytes: List<UByte> = value.toByteArray().toList().map { x -> x.toUByte() }
            bytes += List(size.toInt() - bytes.size, {0u}) // add padding for missing bytes
            return bytes
        }

        /**
         * 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 value The string.
         * @throws IllegalArgumentException if the value of the string's size does not fit into 2 bytes.
         */
        fun convStringToBytes(value: String): List<UByte> {
            require(value.length <= 2.0.pow(16.0) - 1)
            var bytes = convIntegerToBytes(value.length.toBigInteger(), 2u)
            bytes += value.toByteArray().toList().map { x -> x.toUByte() }
            return bytes
        }
    }
}