From 1e50cf9c224d03896f176f3718ff80ef1659e9c2 Mon Sep 17 00:00:00 2001 From: Edoardo La Greca Date: Tue, 12 Aug 2025 18:02:04 +0200 Subject: move InMessage, OutMessage, TransportLayer, and TransportLayerJavaNet to net package --- src/main/kotlin/Connection.kt | 18 +-- src/main/kotlin/InMessage.kt | 164 -------------------------- src/main/kotlin/OutMessage.kt | 124 -------------------- src/main/kotlin/QID.kt | 2 + src/main/kotlin/Stat.kt | 2 + src/main/kotlin/TransportLayer.kt | 57 --------- src/main/kotlin/TransportLayerJavaNet.kt | 92 --------------- src/main/kotlin/net/InMessage.kt | 167 +++++++++++++++++++++++++++ src/main/kotlin/net/OutMessage.kt | 127 ++++++++++++++++++++ src/main/kotlin/net/TransportLayer.kt | 59 ++++++++++ src/main/kotlin/net/TransportLayerJavaNet.kt | 95 +++++++++++++++ 11 files changed, 462 insertions(+), 445 deletions(-) delete mode 100644 src/main/kotlin/InMessage.kt delete mode 100644 src/main/kotlin/OutMessage.kt delete mode 100644 src/main/kotlin/TransportLayer.kt delete mode 100644 src/main/kotlin/TransportLayerJavaNet.kt create mode 100644 src/main/kotlin/net/InMessage.kt create mode 100644 src/main/kotlin/net/OutMessage.kt create mode 100644 src/main/kotlin/net/TransportLayer.kt create mode 100644 src/main/kotlin/net/TransportLayerJavaNet.kt (limited to 'src/main/kotlin') diff --git a/src/main/kotlin/Connection.kt b/src/main/kotlin/Connection.kt index ff12656..f2cdd15 100644 --- a/src/main/kotlin/Connection.kt +++ b/src/main/kotlin/Connection.kt @@ -2,6 +2,9 @@ import except.MsizeValueTooBigException import except.RErrorException import except.UnaccessibleFileException import except.UnknownVersionException +import net.InMessage +import net.OutMessage +import net.TransportLayer import java.io.IOException import java.math.BigInteger @@ -10,12 +13,11 @@ import java.math.BigInteger * described in [ProtocolTranslator]. Details about methods related to 9P can be found in [ProtocolTranslator]. Remember * to disconnect using [disconnect] after use. * - * Details about network-related topics can be found in [TransportLayer] and the implementation of choice. + * Details about network-related topics can be found in [net.TransportLayer] and the implementation of choice. * * Details about 9P messages and methods can be found in [ProtocolTranslator]. * * @param transLay The networking API backend of choice. - * * @throws except.UnresolvableHostException if the host resolution made by [transLay] failed. */ class Connection(transLay: TransportLayer) : ProtocolTranslator { @@ -50,15 +52,15 @@ class Connection(transLay: TransportLayer) : ProtocolTranslator { } /** - * Handy function to create an [InMessage] instance and check for errors. After successfully using this function, it - * is guaranteed that both no error occurred while reading the incoming message and the message is not of type - * R-error. + * Handy function to create an [net.InMessage] instance and check for errors. After successfully using this + * function, it is guaranteed that both no error occurred while reading the incoming message and the message is not + * of type R-error. * - * It uses [tl] and [maxSize] for instancing the [InMessage] class. + * It uses [tl] and [maxSize] for instancing the [net.InMessage] class. * * @return A pair of: (1) a nullable string (which can be: `null` if no error occurred, empty if an error occurred - * with no message, or non-empty with the error message) and (2) the optional [InMessage] instance (null if an error - * occurred). + * with no message, or non-empty with the error message) and (2) the optional [net.InMessage] instance (null if an + * error occurred). * @throws except.InvalidMessageException if the received message is invalid. * @throws except.RErrorException if the received message is an R-error message. */ diff --git a/src/main/kotlin/InMessage.kt b/src/main/kotlin/InMessage.kt deleted file mode 100644 index bf76ef9..0000000 --- a/src/main/kotlin/InMessage.kt +++ /dev/null @@ -1,164 +0,0 @@ -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() }) - } - } -} \ No newline at end of file diff --git a/src/main/kotlin/OutMessage.kt b/src/main/kotlin/OutMessage.kt deleted file mode 100644 index dc16016..0000000 --- a/src/main/kotlin/OutMessage.kt +++ /dev/null @@ -1,124 +0,0 @@ -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, 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.") - } - 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 = 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 { - var bytes: List = 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 { - 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 diff --git a/src/main/kotlin/QID.kt b/src/main/kotlin/QID.kt index 1e03fe8..4d0bd6a 100644 --- a/src/main/kotlin/QID.kt +++ b/src/main/kotlin/QID.kt @@ -1,3 +1,5 @@ +import net.InMessage +import net.OutMessage import java.math.BigInteger /** diff --git a/src/main/kotlin/Stat.kt b/src/main/kotlin/Stat.kt index e609b4c..20692c5 100644 --- a/src/main/kotlin/Stat.kt +++ b/src/main/kotlin/Stat.kt @@ -1,3 +1,5 @@ +import net.InMessage +import net.OutMessage import java.math.BigInteger // TODO: add time conversion methods diff --git a/src/main/kotlin/TransportLayer.kt b/src/main/kotlin/TransportLayer.kt deleted file mode 100644 index 8b31d20..0000000 --- a/src/main/kotlin/TransportLayer.kt +++ /dev/null @@ -1,57 +0,0 @@ -import java.io.Closeable - -/** - * [TransportLayer] is an interface for network transport-layer operations. A class that implements these methods, once - * instantiated, establishes and manages a connection with a remote endpoint defined by an address and a port and allows - * to send and receive network messages (also called "payloads"). - * - * The address of the remote endpoint can be an IP address (v4 or v6) or a domain name, in which case it is resolved to - * an IP address right before initializing the connection. Every constructor should throw an - * [except.UnresolvableHostException] if the remote address is formatted as an actual domain name, but it cannot be - * resolved (e.g. it doesn't exist, or it contains forbidden characters). - * - * Depending on the specific given implementation, the constructor of this class might throw other exceptions (e.g. the - * [java.net.Socket] constructor in [TransportLayerJavaNet]). - */ -interface TransportLayer : Closeable { - /** - * Close the connection. - */ - abstract override fun close() - - /** - * Transmit a payload. - * - * @throws java.io.IOException if the message could not be correctly transmitted. - */ - fun transmit(payload: Iterable) -/* - /** - * Receive a payload until a byte occurs, which marks the end of the message. The byte is discarded after being read - * and is not returned. - * - * If you know both which byte marks the end of the message and the message length, it is advised to use - * [receiveFixed] instead, which is usually more efficient. - * - * @param untilByte The byte that marks the end of the message. - * @return the received payload. - * @throws java.io.IOException if the message could not be correctly received. - */ - abstract fun receiveUntil(untilByte: UByte): Array -*/ - /** - * Receive a payload with fixed length. If zero, nothing is read. - * - * @param length The length of the message in bytes. - * @return the received payload. - * @throws java.io.IOException if the message could not be correctly received. - */ - fun receive(length: ULong): Array - - /** - * Gives the caller a "receiver" (i.e. an instance of Iterable) from which raw data of any length can be read. - * - * @return The receiver. - */ - fun receiver(): Iterable -} \ No newline at end of file diff --git a/src/main/kotlin/TransportLayerJavaNet.kt b/src/main/kotlin/TransportLayerJavaNet.kt deleted file mode 100644 index ee1c9f9..0000000 --- a/src/main/kotlin/TransportLayerJavaNet.kt +++ /dev/null @@ -1,92 +0,0 @@ -import java.io.InputStream -import java.io.OutputStream -import java.net.Socket -import kotlin.math.min - -/* -TODO: - - add TLS support -*/ - -/** - * An implementation of [TransportLayer] written using the [java.net] package. - */ -class TransportLayerJavaNet(val address: String, val port: UShort) : TransportLayer { - /** - * The connection's socket. - */ - private val socket: Socket = Socket(this.address, this.port.toInt()) - - /** - * The connection's input stream. - */ - private val inStream: InputStream = this.socket.inputStream - - /** - * The connection's output stream. - */ - private val outStream: OutputStream = this.socket.outputStream - - constructor(fullAddress: String) : this(nineAddressToValues(fullAddress).first, nineAddressToValues(fullAddress).second) - - private class InStreamIterator(val inStream: InputStream) : Iterator { - override fun next(): UByte { - return this.inStream.readNBytes(1).first().toUByte() - } - - override fun hasNext(): Boolean { - return this.inStream.available() > 0 - } - } - - override fun close() { - if (this.socket.isClosed) { - return - } - this.socket.close() - } - - override fun transmit(payload: Iterable) { - val payload = payload.toList() - val bytes = ByteArray(payload.size, { i -> payload[i].toByte() }) - this.outStream.write(bytes) - } - -/* - override fun receiveUntil(untilByte: UByte): Array { - var stop = false - val payload: Array = MutableList(0, { 0 }) - while (!stop) { - val b = this.inStream.readNBytes(1)[0] - if (b == untilByte) { - stop = true - continue - } else { - payload.add(b) - } - } - return payload - } -*/ - - override fun receive(length: ULong): Array { - var length = length - val intMax = Int.MAX_VALUE.toULong() - val bytes: MutableList = MutableList(0) { 0 } - // readNBytes only takes Int values, so it must be called multiple times if the length is greater than Int's - // maximum value - while (length > 0u) { - // the min function ensures that the value passed to readNBytes never exceeds Int's maximum value while also - // switching to the length variable when its value eventually becomes less than Int's maximum value, which - // avoids writing duplicated readNBytes calls in the code - val lenMin = min(length, intMax) - bytes += this.inStream.readNBytes(lenMin.toInt()).toMutableList() - length -= intMax - } - return Array(bytes.size) { i -> bytes[i].toUByte() } - } - - override fun receiver(): Iterable { - return Iterable { InStreamIterator(this.inStream) } - } -} \ No newline at end of file diff --git a/src/main/kotlin/net/InMessage.kt b/src/main/kotlin/net/InMessage.kt new file mode 100644 index 0000000..771c670 --- /dev/null +++ b/src/main/kotlin/net/InMessage.kt @@ -0,0 +1,167 @@ +package net + +import NinePMessageType +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 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() }) + } + } +} \ No newline at end of file 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, 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.") + } + 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 = 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 { + var bytes: List = 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 { + 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 diff --git a/src/main/kotlin/net/TransportLayer.kt b/src/main/kotlin/net/TransportLayer.kt new file mode 100644 index 0000000..90dcd11 --- /dev/null +++ b/src/main/kotlin/net/TransportLayer.kt @@ -0,0 +1,59 @@ +package net + +import java.io.Closeable + +/** + * [TransportLayer] is an interface for network transport-layer operations. A class that implements these methods, once + * instantiated, establishes and manages a connection with a remote endpoint defined by an address and a port and allows + * to send and receive network messages (also called "payloads"). + * + * The address of the remote endpoint can be an IP address (v4 or v6) or a domain name, in which case it is resolved to + * an IP address right before initializing the connection. Every constructor should throw an + * [except.UnresolvableHostException] if the remote address is formatted as an actual domain name, but it cannot be + * resolved (e.g. it doesn't exist, or it contains forbidden characters). + * + * Depending on the specific given implementation, the constructor of this class might throw other exceptions (e.g. the + * [java.net.Socket] constructor in [TransportLayerJavaNet]). + */ +interface TransportLayer : Closeable { + /** + * Close the connection. + */ + abstract override fun close() + + /** + * Transmit a payload. + * + * @throws java.io.IOException if the message could not be correctly transmitted. + */ + fun transmit(payload: Iterable) +/* + /** + * Receive a payload until a byte occurs, which marks the end of the message. The byte is discarded after being read + * and is not returned. + * + * If you know both which byte marks the end of the message and the message length, it is advised to use + * [receiveFixed] instead, which is usually more efficient. + * + * @param untilByte The byte that marks the end of the message. + * @return the received payload. + * @throws java.io.IOException if the message could not be correctly received. + */ + abstract fun receiveUntil(untilByte: UByte): Array +*/ + /** + * Receive a payload with fixed length. If zero, nothing is read. + * + * @param length The length of the message in bytes. + * @return the received payload. + * @throws java.io.IOException if the message could not be correctly received. + */ + fun receive(length: ULong): Array + + /** + * Gives the caller a "receiver" (i.e. an instance of Iterable) from which raw data of any length can be read. + * + * @return The receiver. + */ + fun receiver(): Iterable +} \ No newline at end of file diff --git a/src/main/kotlin/net/TransportLayerJavaNet.kt b/src/main/kotlin/net/TransportLayerJavaNet.kt new file mode 100644 index 0000000..3d2867a --- /dev/null +++ b/src/main/kotlin/net/TransportLayerJavaNet.kt @@ -0,0 +1,95 @@ +package net + +import nineAddressToValues +import java.io.InputStream +import java.io.OutputStream +import java.net.Socket +import kotlin.math.min + +/* +TODO: + - add TLS support +*/ + +/** + * An implementation of [TransportLayer] written using the [java.net] package. + */ +class TransportLayerJavaNet(val address: String, val port: UShort) : TransportLayer { + /** + * The connection's socket. + */ + private val socket: Socket = Socket(this.address, this.port.toInt()) + + /** + * The connection's input stream. + */ + private val inStream: InputStream = this.socket.inputStream + + /** + * The connection's output stream. + */ + private val outStream: OutputStream = this.socket.outputStream + + constructor(fullAddress: String) : this(nineAddressToValues(fullAddress).first, nineAddressToValues(fullAddress).second) + + private class InStreamIterator(val inStream: InputStream) : Iterator { + override fun next(): UByte { + return this.inStream.readNBytes(1).first().toUByte() + } + + override fun hasNext(): Boolean { + return this.inStream.available() > 0 + } + } + + override fun close() { + if (this.socket.isClosed) { + return + } + this.socket.close() + } + + override fun transmit(payload: Iterable) { + val payload = payload.toList() + val bytes = ByteArray(payload.size, { i -> payload[i].toByte() }) + this.outStream.write(bytes) + } + +/* + override fun receiveUntil(untilByte: UByte): Array { + var stop = false + val payload: Array = MutableList(0, { 0 }) + while (!stop) { + val b = this.inStream.readNBytes(1)[0] + if (b == untilByte) { + stop = true + continue + } else { + payload.add(b) + } + } + return payload + } +*/ + + override fun receive(length: ULong): Array { + var length = length + val intMax = Int.MAX_VALUE.toULong() + val bytes: MutableList = MutableList(0) { 0 } + // readNBytes only takes Int values, so it must be called multiple times if the length is greater than Int's + // maximum value + while (length > 0u) { + // the min function ensures that the value passed to readNBytes never exceeds Int's maximum value while also + // switching to the length variable when its value eventually becomes less than Int's maximum value, which + // avoids writing duplicated readNBytes calls in the code + val lenMin = min(length, intMax) + bytes += this.inStream.readNBytes(lenMin.toInt()).toMutableList() + length -= intMax + } + return Array(bytes.size) { i -> bytes[i].toUByte() } + } + + override fun receiver(): Iterable { + return Iterable { InStreamIterator(this.inStream) } + } +} \ No newline at end of file -- cgit v1.2.3