summaryrefslogtreecommitdiff
path: root/src/main/kotlin/net/OutMessage.kt
diff options
context:
space:
mode:
authorEdoardo La Greca2025-08-12 18:02:04 +0200
committerEdoardo La Greca2025-08-12 18:08:23 +0200
commit1e50cf9c224d03896f176f3718ff80ef1659e9c2 (patch)
treea4eff2d5acf5db1780e34f49daba566ed2ce3fa4 /src/main/kotlin/net/OutMessage.kt
parentc03cb8abf4cb48e9816d8f2642e6e60823627689 (diff)
move InMessage, OutMessage, TransportLayer, and TransportLayerJavaNet to net package
Diffstat (limited to 'src/main/kotlin/net/OutMessage.kt')
-rw-r--r--src/main/kotlin/net/OutMessage.kt127
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