diff options
Diffstat (limited to 'src/main/kotlin/net/OutMessage.kt')
-rw-r--r-- | src/main/kotlin/net/OutMessage.kt | 127 |
1 files changed, 127 insertions, 0 deletions
diff --git a/src/main/kotlin/net/OutMessage.kt b/src/main/kotlin/net/OutMessage.kt new file mode 100644 index 0000000..c9ae879 --- /dev/null +++ b/src/main/kotlin/net/OutMessage.kt @@ -0,0 +1,127 @@ +package net + +import NinePMessageType +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 integer field's name into its value and size in bytes. + * @param fieldValuesStr A map of each string field's name into its value. + * @param fieldValuesRaw A map of each raw field's name into its value. + * @param maxSize The maximum message size. + */ +class OutMessage(val type: NinePMessageType, val tag: UShort, val fieldNames: List<String>, val fieldValuesInt: Map<String, Pair<BigInteger, UInt>>, val fieldValuesStr: Map<String, String>, val fieldValuesRaw: Map<String, List<UByte>>, 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.") + } + writeMessageSizeTypeTag(tl, totalSize, type, tag) + for (field in fieldNames) { + tl.transmit( + if (field in insecInts) { + val valsize = fieldValuesInt[field]!! + convIntegerToBytes(valsize.first, valsize.second) + } else if (field in insecStrs) { + convStringToBytes(fieldValuesStr[field]!!) + } else { + fieldValuesRaw[field]!!.toList() + } + ) + } + } + + /** + * 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. + * @param tag The 9P message tag. + */ + private fun writeMessageSizeTypeTag(tl: TransportLayer, size: UInt, type: NinePMessageType, tag: UShort) { + var bytes: List<UByte> = emptyList() + bytes += convIntegerToBytes(BigInteger(size.toString()), 4u) + bytes += convIntegerToBytes(BigInteger(type.value.toString()), 1u) + bytes += convIntegerToBytes(BigInteger(tag.toString()), 2u) + tl.transmit(bytes) + } + + /** + * Calculate the expected size of the message. + */ + fun size(): UInt { + return 4u + 1u + 2u + this.insecInts.sumOf { this.fieldValuesInt[it]!!.second } + 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 + + /** + * Convert an integer number to its byte representation. + * + * In 9P, binary numbers (non-textual) are specified in little-endian order (least significant byte first). + * + * @param value The number's value. + * @param size The number's size in bytes. + */ + fun convIntegerToBytes(value: BigInteger, size: UInt): List<UByte> { + var bytes: List<UByte> = value.toByteArray().toList().map { x -> x.toUByte() } + bytes += List(size.toInt() - bytes.size, {0u}) // add padding for missing bytes + return bytes + } + + /** + * 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. + */ + fun convStringToBytes(value: String): List<UByte> { + require(value.length <= 2.0.pow(16.0) - 1) + var bytes = convIntegerToBytes(value.length.toBigInteger(), 2u) + bytes += value.toByteArray().toList().map { x -> x.toUByte() } + return bytes + } + } +}
\ No newline at end of file |