summaryrefslogtreecommitdiff
path: root/src/main/kotlin/InMessage.kt
blob: c111d623adc6f0f28d6259404d4c74151607b87b (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
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
import java.math.BigInteger

/**
 * An incoming 9P message. Upon instancing this class only one message is read, and it's represented in a way similar to
 * that of [OutMessage]. This class is supposed to be complementary, and opposite, to [OutMessage].
 *
 * @param tl The transport layer API.
 * @param maxSize The maximum message size negotiated with the remote part.
 * @param reqTag The required tag.
 * @throws InvalidMessageException if the message that is currently being read is invalid.
 */
class InMessage(val tl: TransportLayer, maxSize: UInt, val reqTag: UShort) {
    /**
     * The total size of the message.
     */
    val size: UInt

    /**
     * The message type.
     */
    val type: NinePMessageType

    /**
     * The message tag.
     */
    val tag: UShort

    /**
     * A map of each integer field's name to its value.
     */
    var fieldsInt: MutableMap<String, BigInteger> = mutableMapOf()
        private set

    /**
     * A map of each string field's name to its value.
     */
    var fieldsStr: MutableMap<String, String> = mutableMapOf()
        private set

    /**
     * A map of each raw field's name to its value.
     */
    var fieldsRaw: MutableMap<String, Array<UByte>> = mutableMapOf()
        private set

    /**
     * An ordered collection of raw bytes that still need to be interpreted as values.
     */
    private var rawData: List<UByte>

    init {
        size = convInteger(this.tl.receiver(), 0, 4).toInt().toUInt()
        if (this.size > maxSize) {
            throw InvalidMessageException("Size greater than maximum size (${this.size} > ${maxSize}).")
        }
        try {
            this.type = NinePMessageType.fromByte(convInteger(this.tl.receiver(), 0, 1).toInt().toUByte())
        } catch (_: NoSuchElementException) {
            throw InvalidMessageException("Invalid 9P message type.")
        }
        tag = convInteger(this.tl.receiver(), 0, 2).toInt().toUShort()
        if (tag != reqTag) {
            // TODO: what do we do now?
        }
        this.rawData = this.tl.receive((size - (4u + 1u + 2u)).toULong()).toList()
    }

    /**
     * Field of an incoming 9P message. An ordered collection of fields makes a schema.
     *
     * @param name The field's name. It's typically the same you can find in the manual pages.
     * @param type The field's type.
     * @param size The field's size in bytes. If the type is [Type.STRING], this parameter is ignored.
     */
    data class Field(val name: String, val type: Type, val size: UInt) {

        enum class Type {
            INTEGER,
            STRING,
            RAW
        }
    }

    /**
     * Convert an [len] bytes long unsigned integer number from raw bytes.
     *
     * In 9P, binary numbers (non-textual) are specified in little-endian order (least significant byte first).
     *
     * @param len The length of the integer number in bytes. If zero, nothing is read.
     * @return the number's value.
     * @throws IllegalArgumentException if either [offset] or [len] are negative.
     */
    fun convInteger(bytes: Iterable<UByte>, offset: Int, len: Int): BigInteger {
        val bytes = bytes.drop(offset).take(len)
        var value = 0.toBigInteger()
        for (i in 0..<bytes.size) {
            value += bytes[i].toInt().toBigInteger().shl(i*8)
        }
        return value
    }

    /**
     * Convert a string from raw bytes.
     *
     * 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.
     *
     * @return the string.
     * @throws IllegalArgumentException if either [offset] is negative.
     */
    fun convString(bytes: Iterable<UByte>, offset: Int): String {
        val length = convInteger(bytes, 0, 2).toInt()
        val bytes = bytes.drop(offset).take(length)
        return String(ByteArray(bytes.size) { i -> bytes[i].toByte() })
    }

    /**
     * Apply the given field to the raw data and put it in one of [fieldsInt], [fieldsStr], or [fieldsRaw]. Fields must
     * be applied strictly in order, as their application is not commutative.
     *
     * Each time a field is applied, the initial part of raw data that coincides with that field is removed.
     *
     * @param field The given field.
     */
    fun applyField(field: Field) {
        val size: Int
        when (field.type) {
            Field.Type.STRING -> {
                val str = convString(this.rawData.toList(), 0)
                size = 2 + str.length
                this.fieldsStr[field.name] = str
            }
            Field.Type.INTEGER -> {
                size = field.size.toInt()
                this.fieldsInt[field.name] = convInteger(this.rawData.toList(), 0, size)
            }
            Field.Type.RAW -> {
                size = field.size.toInt()
                this.fieldsRaw[field.name] = this.rawData.take(size).toTypedArray()
            }
        }
        this.rawData = this.rawData.drop(size)
    }

    /**
     * Apply the given message schema to the raw data and fill [fieldsInt], [fieldsStr], and [fieldsRaw].
     *
     * Note: This method could have been avoided by making a giant `when` block in the class constructor. However, I'd
     * rather let the caller, which is usually a method that makes a request and reads its response, decide the schema.
     * In this way, each method that needs to read a response of a specific type (and there is usually one method per
     * response type) declares its own schema, while those which cannot be easily represented by a schema (e.g. `Rwalk`)
     * are simply going to be read in a field-by-field fashion.
     *
     * @param schema The desired ordered collection of fields.
     */
    fun applySchema(schema: Iterable<Field>) {
        for (field in schema) {
            applyField(field)
        }
    }
}