diff options
author | Edoardo La Greca | 2025-07-12 19:14:26 +0200 |
---|---|---|
committer | Edoardo La Greca | 2025-07-13 21:22:19 +0200 |
commit | 917599228501ae235ffaf01b515c8b06cf8595b0 (patch) | |
tree | f7dc4ff08bf44a9b029346aaaec39b23dd35321e | |
parent | c916a1c14813fbb96288b0c75efd29e01ee6a0df (diff) |
-rw-r--r-- | src/main/kotlin/NinePConnection.kt | 191 | ||||
-rw-r--r-- | src/main/kotlin/NinePMessageType.kt | 46 | ||||
-rw-r--r-- | src/main/kotlin/NinePTranslator.kt | 14 | ||||
-rw-r--r-- | src/main/kotlin/SizedMessageField.kt | 35 |
4 files changed, 260 insertions, 26 deletions
diff --git a/src/main/kotlin/NinePConnection.kt b/src/main/kotlin/NinePConnection.kt index ee9d5ba..bb26210 100644 --- a/src/main/kotlin/NinePConnection.kt +++ b/src/main/kotlin/NinePConnection.kt @@ -1,5 +1,6 @@ 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 @@ -21,6 +22,11 @@ class NinePConnection(netPackTrans: NetworkPacketTransporter) : NinePTranslator 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. @@ -64,17 +70,188 @@ class NinePConnection(netPackTrans: NetworkPacketTransporter) : NinePTranslator } /** - * Read the message size and type. + * Read a message size, type, and tag. * - * @return A pair in which the first element is the message size in bytes and the second is the message type as a - * [NinePMessageType] constant. + * @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 readMessageSizeType(): Pair<UInt, NinePMessageType> { - return Pair( + private fun readSizeTypeTag(): Triple<UInt, NinePMessageType, UInt> { + return Triple( readInteger(4).toInt().toUInt(), - NinePMessageType.fromByte(readInteger(1).toByte()) + 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") } - // TODO: implement methods from NinePTranslator }
\ No newline at end of file diff --git a/src/main/kotlin/NinePMessageType.kt b/src/main/kotlin/NinePMessageType.kt index 017fe2e..c18e7e9 100644 --- a/src/main/kotlin/NinePMessageType.kt +++ b/src/main/kotlin/NinePMessageType.kt @@ -1,22 +1,32 @@ -/* -TODO: - - add correct values -*/ - enum class NinePMessageType(val value: Byte) { - VERSION(1), - AUTH(2), - FLUSH(3), - ATTACH(4), - WALK(5), - OPEN(6), - CREATE(7), - READ(8), - WRITE(9), - CLUNK(10), - REMOVE(11), - STAT(12), - WSTAT(13); + TVERSION(100), + RVERSION(101), + TAUTH(102), + RAUTH(103), + TATTACH(104), + RATTACH(105), + //TERROR(106), <--- illegal + RERROR(107), + TFLUSH(108), + RFLUSH(109), + TWALK(110), + RWALK(111), + TOPEN(112), + ROPEN(113), + TCREATE(114), + RCREATE(115), + TREAD(116), + RREAD(117), + TWRITE(118), + RWRITE(119), + TCLUNK(120), + RCLUNK(121), + TREMOVE(122), + RREMOVE(123), + TSTAT(124), + RSTAT(125), + TWSTAT(126), + RWSTAT(127); companion object { fun fromByte(value: Byte) = NinePMessageType.entries.first { it.value == value } diff --git a/src/main/kotlin/NinePTranslator.kt b/src/main/kotlin/NinePTranslator.kt index c976cd9..51b722e 100644 --- a/src/main/kotlin/NinePTranslator.kt +++ b/src/main/kotlin/NinePTranslator.kt @@ -1,3 +1,5 @@ +import java.util.Optional + /* TODO: - add arguments to methods @@ -12,8 +14,18 @@ TODO: interface NinePTranslator { /** * Negotiate protocol version. + * + * This must be the first message sent on the 9P connection and no other requests can be issued until a response has + * been received. + * Tag should be NOTAG ((ushort)~0). + * + * @param tag Should be NOTAG ((ushort)~0). + * @param msize The maximum length, in bytes, that the client will ever generate or expect to receive in a single + * 9P message. + * @param version Should be "9P2000", which is the only defined value. + * @return a possible error. */ - fun version() + fun version(tag: SizedMessageField, msize: SizedMessageField, version: String): String? /** * Perform authentication. diff --git a/src/main/kotlin/SizedMessageField.kt b/src/main/kotlin/SizedMessageField.kt new file mode 100644 index 0000000..c40a2a6 --- /dev/null +++ b/src/main/kotlin/SizedMessageField.kt @@ -0,0 +1,35 @@ +import java.math.BigInteger + +/** + * [SizedMessageField] represents an unsigned integer number stored in a message field (i.e. a contiguous region of + * memory) of fixed size. + * + * @param size The size of the field, measured in bytes. + * @param value The value of the field. + * + * @throws IllegalArgumentException if the size required to store the provided value is bigger than the provided size. + */ +class SizedMessageField(val size: Int, value: BigInteger) { + init { + if (!this.sizeOk(size, value)) { + throw IllegalArgumentException() + } + } + + /** + * @throws IllegalStateException on set if the declared size is not enough for the new value. + */ + var value = value + set(value) { + if (!this.sizeOk(value)) { + throw IllegalStateException() + } + } + + private fun sizeOk(size: Int, value: BigInteger): Boolean { + val requiredSize = value.toByteArray().size + return requiredSize <= size + } + private fun sizeOk(value: BigInteger): Boolean = sizeOk(this.size, value) + private fun sizeOk(): Boolean = sizeOk(this.size, this.value) +}
\ No newline at end of file |