summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorEdoardo La Greca2025-08-06 18:44:59 +0200
committerEdoardo La Greca2025-08-06 18:44:59 +0200
commitff5ab181327baed8b937856305b28d8ac2700cc8 (patch)
tree77e7d77ca0e6e035600df212ce9ec5d42a2cdf1a /src
parent5578eefca662e9cf32f5a39ac846a5aa74ca00f0 (diff)
add InMessage
Diffstat (limited to 'src')
-rw-r--r--src/main/kotlin/InMessage.kt161
1 files changed, 161 insertions, 0 deletions
diff --git a/src/main/kotlin/InMessage.kt b/src/main/kotlin/InMessage.kt
new file mode 100644
index 0000000..c111d62
--- /dev/null
+++ b/src/main/kotlin/InMessage.kt
@@ -0,0 +1,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)
+ }
+ }
+} \ No newline at end of file