summaryrefslogtreecommitdiff
path: root/src/main/kotlin/NinePConnection.kt
blob: bb26210e99902766d05c92a39596e6dfc9297a8d (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
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
import java.io.IOException
import java.math.BigInteger
import kotlin.math.pow

/**
 * This class represents a 9P connection. It provides a practical implementation with networking to the 9P methods
 * described in [NinePTranslator]. Details about methods related to 9P can be found in [NinePTranslator]. Remember to
 * disconnect using [disconnect] after use.
 *
 * Details about network-related topics can be found in [NetworkPacketTransporter] and the implementation of choice.
 *
 * Details about 9P messages and methods can be found in [NinePTranslator].
 *
 * @param netPackTrans The networking API backend of choice.
 *
 * @throws UnresolvableHostException if the host resolution made by [netPackTrans] failed.
 */
class NinePConnection(netPackTrans: NetworkPacketTransporter) : NinePTranslator {
    /**
     * Networking API.
     */
    val npt: NetworkPacketTransporter = netPackTrans

    /**
     * Has the 9P connection been initialized yet?
     */
    private var hasBeenInitialized = false

    /**
     * Disconnect from the remote host,
     *
     * @throws IOException if an I/O error occurred while closing the socket.
     */
    fun disconnect() {
        this.npt.close()
    }

    /**
     * Read an [nBytes]-long unsigned integer number from the connection.
     *
     * In 9P, binary numbers (non-textual) are specified in little-endian order (least significant byte first).
     *
     * @param nBytes The length of the integer number in bytes. It only works with unsigned numbers strictly greater
     *               than zero (zero is excluded).
     * @return The number's value.
     * @throws IllegalArgumentException if [nBytes] is zero or negative.
     */
    private fun readInteger(nBytes: Int): BigInteger {
        require(nBytes > 0)
        val bytes = this.npt.receiveFixed(nBytes.toULong())
        var number: BigInteger = BigInteger.valueOf(0)
        for (i in 0..<bytes.size) {
            number += bytes[i].toInt().toBigInteger().shl(i)
        }

        return number
    }

    /**
     * Read a string from 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.
     *
     * @return The string.
     */
    private fun readString(): String {
        val length = readInteger(2).toInt()
        return String(this.npt.receiveFixed(length.toULong()).toByteArray())
    }

    /**
     * Read a message size, type, and tag.
     *
     * @return A triple in which the first element is the message size in bytes, the second is the message type as a
     * [NinePMessageType] constant, and the third element is the message tag.
     */
    private fun readSizeTypeTag(): Triple<UInt, NinePMessageType, UInt> {
        return Triple(
            readInteger(4).toInt().toUInt(),
            NinePMessageType.fromByte(readInteger(1).toByte()),
            readInteger(2).toInt().toUInt()
        )
    }

    /**
     * Wait for a 9P message with the same tag from the server.
     *
     * All messages prior to the returned one are discarded.
     *
     * @param tag The tag to wait for.
     */
    private fun waitForTag(tag: UInt): Triple<UInt, NinePMessageType, UInt> {
        var s = 0u
        var ty: NinePMessageType?
        var ta = 0u
        var found = false
        while (!found) {
            val stt = readSizeTypeTag()
            s = stt.first
            ty = stt.second
            ta = stt.third

            if (ta == tag) {
                found = true
            }
        }
        return s
    }

    /**
     * Read a 9P message of type Rerror, after the message size and type have already been read.
     *
     * @return A pair of: (1) the message tag and (2) the error message
     */
    private fun readError(): Pair<SizedMessageField, String> {
        val tag = readInteger(2)
        val error = readString()
        return Pair(SizedMessageField(2, tag), error)
    }

    /**
     * Write an integer number to the connection.
     *
     * In 9P, binary numbers (non-textual) are specified in little-endian order (least significant byte first).
     *
     * @param value The number's value. [SizedMessageField] defines both its actual value and its size.
     */
    private fun writeInteger(value: SizedMessageField) {
        this.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 value The string.
     * @throws IllegalArgumentException if the value of the string's size does not fit into 2 bytes.
     */
    private fun writeString(value: String) {
        require(value.length <= 2.0.pow(16.0) - 1)
        writeInteger(SizedMessageField(2, value.length.toBigInteger()))
        this.npt.transmit(value.toByteArray().toList())
    }

    /**
     * Write the message size and type.
     *
     * @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(size: Int, type: NinePMessageType) {
        writeInteger(SizedMessageField(4, size.toBigInteger()))
        writeInteger(SizedMessageField(1, type.value.toInt().toBigInteger()))
    }

    /**
     * Write a message of the given fields.
     *
     * Important note: the field names in [fieldValuesInt] and [fieldValuesStr] (i.e. the keys of their maps) are
     * mutually exclusive and the union of these two sets must result exactly in the set of field names listed in
     * [fieldNames].
     *
     * @param type The 9P message type.
     * @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.
     * @throws IllegalArgumentException if [fieldNames], [fieldValuesInt], and [fieldValuesStr] are incoherent.
     */
    private fun writeMessage(type: NinePMessageType, fieldNames: List<String>, fieldValuesInt: Map<String, SizedMessageField>, fieldValuesStr: Map<String, String>) {
        // 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 + insecInts.sumOf { fieldValuesInt[it]!!.size } + insecStrs.sumOf { 2 + fieldValuesStr[it]!!.length }

        writeMessageSizeType(totalSize, type)
        for (field in fieldNames) {
            if (field in insecInts) {
                writeInteger(fieldValuesInt[field]!!)
            } else {
                writeString(fieldValuesStr[field]!!)
            }
        }
    }

    override fun version(tag: SizedMessageField, msize: SizedMessageField, version: String): String? {
        writeMessage(NinePMessageType.VERSION, listOf("tag", "msize", "version"),
            mapOf(
                "tag" to tag,
                "msize" to msize
            ),
            mapOf(
                "version" to version
            )
        )
        val se = ignoreUntilType(NinePMessageType.VERSION)
        if (se.second) {
            val error = readError()
            return error.second
        }

    }

    override fun auth() {
        TODO("Not yet implemented")
    }

    override fun flush() {
        TODO("Not yet implemented")
    }

    override fun attach() {
        TODO("Not yet implemented")
    }

    override fun walk(path: String) {
        TODO("Not yet implemented")
    }

    override fun open(path: String) {
        TODO("Not yet implemented")
    }

    override fun create(path: String) {
        TODO("Not yet implemented")
    }

    override fun read(path: String) {
        TODO("Not yet implemented")
    }

    override fun write(path: String) {
        TODO("Not yet implemented")
    }

    override fun clunk(path: String) {
        TODO("Not yet implemented")
    }

    override fun remove(path: String) {
        TODO("Not yet implemented")
    }

    override fun stat(path: String) {
        TODO("Not yet implemented")
    }

    override fun wstat(path: String) {
        TODO("Not yet implemented")
    }

}