diff options
author | Edoardo La Greca | 2025-08-06 18:44:59 +0200 |
---|---|---|
committer | Edoardo La Greca | 2025-08-06 18:44:59 +0200 |
commit | ff5ab181327baed8b937856305b28d8ac2700cc8 (patch) | |
tree | 77e7d77ca0e6e035600df212ce9ec5d42a2cdf1a /src | |
parent | 5578eefca662e9cf32f5a39ac846a5aa74ca00f0 (diff) |
add InMessage
Diffstat (limited to 'src')
-rw-r--r-- | src/main/kotlin/InMessage.kt | 161 |
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 |