summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorEdoardo La Greca2025-07-12 19:14:26 +0200
committerEdoardo La Greca2025-07-13 21:22:19 +0200
commit917599228501ae235ffaf01b515c8b06cf8595b0 (patch)
treef7dc4ff08bf44a9b029346aaaec39b23dd35321e
parentc916a1c14813fbb96288b0c75efd29e01ee6a0df (diff)
-rw-r--r--src/main/kotlin/NinePConnection.kt191
-rw-r--r--src/main/kotlin/NinePMessageType.kt46
-rw-r--r--src/main/kotlin/NinePTranslator.kt14
-rw-r--r--src/main/kotlin/SizedMessageField.kt35
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