import except.InvalidMessageException 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 except.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 = mutableMapOf() private set /** * A map of each string field's name to its value. */ var fieldsStr: MutableMap = mutableMapOf() private set /** * A map of each raw field's name to its value. */ var fieldsRaw: MutableMap> = mutableMapOf() private set /** * An ordered collection of raw bytes that still need to be interpreted as values. */ private var rawData: List 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 } } /** * 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) { for (field in schema) { applyField(field) } } companion object { /** * 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, offset: Int, len: Int): BigInteger { val bytes = bytes.drop(offset).take(len) var value = 0.toBigInteger() for (i in 0.., 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() }) } } }