import java.math.BigInteger import kotlin.math.pow /** * An outgoing 9P message with the given type, tag, and fields. The message size is calculated automatically. * * Important note: the field names in [fieldValuesInt], [fieldValuesStr], and [fieldValuesRaw] (i.e. the keys of their * maps) must be mutually exclusive and the union of these two maps' keys must result in a subset of (or a set equal to) * [fieldNames]. Calling [write] when these conditions are not met throws an exception. * * @param type The 9P message type. * @param tag The tag given to the message. * @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. */ class OutMessage(val type: NinePMessageType, val tag: UShort, val fieldNames: List, val fieldValuesInt: Map, val fieldValuesStr: Map, val fieldValuesRaw: Map>, val maxSize: UInt) { /** * Intersection between [fieldNames] and [fieldValuesInt]. In other words: the integer fields that are going to be * used when writing the message. */ private val insecInts = fieldNames.intersect(fieldValuesInt.keys) /** * Intersection between [fieldNames] and [fieldValuesStr]. In other words: the string fields that are going to be * used when writing the message. */ private val insecStrs = fieldNames.intersect(fieldValuesStr.keys) /** * Intersection between [fieldNames] and [fieldValuesRaw]. In other words: the raw fields that are going to be used * when writing the message. */ private val insecRaws = fieldNames.intersect(fieldValuesRaw.keys) /** * Send the message using the given networking API. * * @param tl The networking API. * @throws IllegalArgumentException if [fieldNames], [fieldValuesInt], and [fieldValuesStr] are incoherent or the * final size of the message exceeds the negotiated value. */ fun write(tl: TransportLayer) { // check that names in fieldNames exist as keys in either fieldValuesInt or fieldValuesStr but not both require(fieldNames.size == insecInts.size + insecStrs.size + insecRaws.size) val totalSize = size() if (totalSize > this.maxSize) { throw IllegalArgumentException("Message size exceeded.") } writeMessageSizeType(tl, totalSize, type) writeInteger(tl, tag.toInt().toBigInteger()) for (field in fieldNames) { if (field in insecInts) { writeInteger(tl, fieldValuesInt[field]!!) } else if (field in insecStrs) { writeString(tl, fieldValuesStr[field]!!) } else { tl.transmit(fieldValuesRaw[field]!!) } } } /** * Write the message size and type. * * @param tl The networking API. * @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(tl: TransportLayer, size: UInt, type: NinePMessageType) { writeInteger(tl, size.toInt().toBigInteger()) writeInteger(tl, type.value.toInt().toBigInteger()) } /** * Calculate the expected size of the message. */ fun size(): UInt { return 4u + 1u + 2u + this.insecInts.sumOf { this.fieldValuesInt[it]!!.bitLength().toUInt() } + this.insecStrs.sumOf { 2u + this.fieldValuesStr[it]!!.length.toUInt() } + this.insecRaws.sumOf { this.fieldValuesRaw[it]!!.size.toUInt() } } companion object { // TODO: Add size that the value is required to fit in /** * Write an integer number to the connection. * * In 9P, binary numbers (non-textual) are specified in little-endian order (least significant byte first). * * @param tl The networking API. * @param value The number's value. */ fun writeInteger(tl: TransportLayer, value: BigInteger) { val bytes = value.toByteArray() tl.transmit(Array(bytes.size) { i -> bytes[i].toUByte() }) } /** * 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 tl The networking API. * @param value The string. * @throws IllegalArgumentException if the value of the string's size does not fit into 2 bytes. */ fun writeString(tl: TransportLayer, value: String) { require(value.length <= 2.0.pow(16.0) - 1) writeInteger(tl, value.length.toBigInteger()) val bytes = value.toByteArray() tl.transmit(Array(bytes.size) { i -> bytes[i].toUByte() }) } } }