From eec68974cabb51e1a8136772e0cad38eaa7d3620 Mon Sep 17 00:00:00 2001 From: Edoardo La Greca Date: Fri, 1 Aug 2025 19:19:15 +0200 Subject: change class names and do some refactoring --- src/main/kotlin/Message.kt | 104 +++++++++++++++++++++ src/main/kotlin/NetworkPacketTransporter.kt | 74 --------------- src/main/kotlin/NetworkPacketTransporterJavaNet.kt | 78 ---------------- src/main/kotlin/NinePMessage.kt | 104 --------------------- src/main/kotlin/NinePTranslator.kt | 88 ----------------- src/main/kotlin/ProtocolTranslator.kt | 88 +++++++++++++++++ src/main/kotlin/TransportLayer.kt | 74 +++++++++++++++ src/main/kotlin/TransportLayerJavaNet.kt | 78 ++++++++++++++++ 8 files changed, 344 insertions(+), 344 deletions(-) create mode 100644 src/main/kotlin/Message.kt delete mode 100644 src/main/kotlin/NetworkPacketTransporter.kt delete mode 100644 src/main/kotlin/NetworkPacketTransporterJavaNet.kt delete mode 100644 src/main/kotlin/NinePMessage.kt delete mode 100644 src/main/kotlin/NinePTranslator.kt create mode 100644 src/main/kotlin/ProtocolTranslator.kt create mode 100644 src/main/kotlin/TransportLayer.kt create mode 100644 src/main/kotlin/TransportLayerJavaNet.kt diff --git a/src/main/kotlin/Message.kt b/src/main/kotlin/Message.kt new file mode 100644 index 0000000..5007c26 --- /dev/null +++ b/src/main/kotlin/Message.kt @@ -0,0 +1,104 @@ +import java.math.BigInteger +import kotlin.math.pow + +/** + * A 9P message with the given type, tag, and fields. The message size is calculated automatically. + * + * Important note: the field names in [fieldValuesInt] and [fieldValuesStr] (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 Message(val type: NinePMessageType, val tag: UShort, val fieldNames: List, val fieldValuesInt: Map, val fieldValuesStr: Map, val maxSize: UInt) { + /** + * Intersection between [fieldNames] and [fieldValuesInt]. In other words: the amount of 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 amount of string fields that are + * going to be used when writing the message. + */ + private val insecStrs = fieldNames.intersect(fieldValuesStr.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(insecInts.size == fieldNames.size - insecStrs.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 { + writeString(tl, fieldValuesStr[field]!!) + } + } + } + + // 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. + */ + private 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. + */ + private 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() }) + } + + /** + * 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() } + } +} \ No newline at end of file diff --git a/src/main/kotlin/NetworkPacketTransporter.kt b/src/main/kotlin/NetworkPacketTransporter.kt deleted file mode 100644 index eef0aea..0000000 --- a/src/main/kotlin/NetworkPacketTransporter.kt +++ /dev/null @@ -1,74 +0,0 @@ -import java.io.Closeable - -/** - * [NetworkPacketTransporter] is an abstract class for network transport-layer operations. A class that implements this - * class' abstract 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 [UnresolvableHostException] - * if the remote address is a domain name, but it cannot be resolved. - * - * Depending on the specific given implementation, the constructor of this class might throw other exceptions (e.g. the - * [java.net.Socket] constructor in [NetworkPacketTransporterJavaNet]). - */ -abstract class NetworkPacketTransporter : Closeable { - val address: String - val port: UShort - - /** - * Classic constructor. - * - * @throws UnresolvableHostException if the remote address is a domain name, but it cannot be resolved. - */ - constructor(address: String, port: UShort) { - this.address = address - this.port = port - } - - /** - * This constructor allows addresses specified using one of the styles specified in dial(2). See - * [nineAddressToValues]. - * - * @throws UnresolvableHostException if the remote address is a domain name, but it cannot be resolved. - */ - constructor(fullAddress: String) { - val ap = nineAddressToValues(fullAddress) - this.address = ap.first - this.port = ap.second - } - - /** - * Close the connection. - */ - abstract override fun close() - - /** - * Transmit a payload. - * - * @throws java.io.IOException if the message could not be correctly transmitted. - */ - abstract fun transmit(payload: Array) -/* - /** - * 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. - */ - abstract fun receive(length: ULong): Array -} \ No newline at end of file diff --git a/src/main/kotlin/NetworkPacketTransporterJavaNet.kt b/src/main/kotlin/NetworkPacketTransporterJavaNet.kt deleted file mode 100644 index fe2c87c..0000000 --- a/src/main/kotlin/NetworkPacketTransporterJavaNet.kt +++ /dev/null @@ -1,78 +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 [NetworkPacketTransporter] written using the [java.net] package. - */ -class NetworkPacketTransporterJavaNet : NetworkPacketTransporter { - /** - * The connection's socket. - */ - private val socket: Socket = Socket(address, 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(address: String, port: UShort) : super(address, port) - constructor(fullAddress: String) : super(fullAddress) - - override fun close() { - if (this.socket.isClosed) { - return - } - this.socket.close() - } - - override fun transmit(payload: Array) { - 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() } - } -} \ No newline at end of file diff --git a/src/main/kotlin/NinePMessage.kt b/src/main/kotlin/NinePMessage.kt deleted file mode 100644 index 1d77807..0000000 --- a/src/main/kotlin/NinePMessage.kt +++ /dev/null @@ -1,104 +0,0 @@ -import java.math.BigInteger -import kotlin.math.pow - -/** - * A 9P message with the given type, tag, and fields. The message size is calculated automatically. - * - * Important note: the field names in [fieldValuesInt] and [fieldValuesStr] (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 NinePMessage(val type: NinePMessageType, val tag: UShort, val fieldNames: List, val fieldValuesInt: Map, val fieldValuesStr: Map, val maxSize: UInt) { - /** - * Intersection between [fieldNames] and [fieldValuesInt]. In other words: the amount of 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 amount of string fields that are - * going to be used when writing the message. - */ - private val insecStrs = fieldNames.intersect(fieldValuesStr.keys) - - /** - * Send the message using the given networking API. - * - * @param npt 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(npt: NetworkPacketTransporter) { - // check that names in fieldNames exist as keys in either fieldValuesInt or fieldValuesStr but not both - require(insecInts.size == fieldNames.size - insecStrs.size) - - val totalSize = size() - if (totalSize > this.maxSize) { - throw IllegalArgumentException("Message size exceeded.") - } - writeMessageSizeType(npt, totalSize, type) - writeInteger(npt, tag.toInt().toBigInteger()) - for (field in fieldNames) { - if (field in insecInts) { - writeInteger(npt, fieldValuesInt[field]!!) - } else { - writeString(npt, fieldValuesStr[field]!!) - } - } - } - - // 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 npt The networking API. - * @param value The number's value. - */ - private fun writeInteger(npt: NetworkPacketTransporter, value: BigInteger) { - val bytes = value.toByteArray() - npt.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 npt The networking API. - * @param value The string. - * @throws IllegalArgumentException if the value of the string's size does not fit into 2 bytes. - */ - private fun writeString(npt: NetworkPacketTransporter, value: String) { - require(value.length <= 2.0.pow(16.0) - 1) - writeInteger(npt, value.length.toBigInteger()) - val bytes = value.toByteArray() - npt.transmit(Array(bytes.size) { i -> bytes[i].toUByte() }) - } - - /** - * Write the message size and type. - * - * @param npt 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(npt: NetworkPacketTransporter, size: UInt, type: NinePMessageType) { - writeInteger(npt, size.toInt().toBigInteger()) - writeInteger(npt, 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() } - } -} \ No newline at end of file diff --git a/src/main/kotlin/NinePTranslator.kt b/src/main/kotlin/NinePTranslator.kt deleted file mode 100644 index ef76cc6..0000000 --- a/src/main/kotlin/NinePTranslator.kt +++ /dev/null @@ -1,88 +0,0 @@ -/* -TODO: - - add arguments to methods -*/ - -/** - * The [NinePTranslator] interface provides methods that coincide 1:1 with each request type in the 9P protocol. Every - * method that can fail, that is, every request that can receive a response with `Rerror` type instead of the same type - * as itself, returns a non-null `String` that contains the error message received in the response. - * - * Tags are supposed to be managed internally by the class that provides the implementation. - * - * Trivia: comments for each method are taken from each message type's manual page in section 5. - */ -interface NinePTranslator { - /** - * Negotiate protocol version. - * - * This must be the first message sent on the 9P connection and no other requests can be issued until a response has - * been received. - * - * @param msize The maximum length, in bytes, that the client will ever generate or expect to receive in a single - * 9P message. - * @param version Should be "9P2000", which is the only defined value. - * @return a possible error. - */ - fun version(msize: UInt, version: String): String? - - /** - * Perform authentication. - */ - fun auth(afid: UInt, uname: String, aname: String) - - /** - * Abort a message. - */ - fun flush() - - /** - * Establish a connection. - */ - fun attach() - - /** - * Descend a directory hierarchy. - */ - fun walk(path: String) - - /** - * Prepare an FID for I/O on an existing file. - */ - fun open(path: String) - - /** - * Prepare an FID for I/O on a new file. - */ - fun create(path: String) - - /** - * Transfer data from file. - */ - fun read(fid: UInt, offset: ULong, count: UInt): String? - - /** - * Transfer data to file. - */ - fun write(fid: UInt, offset: ULong, count: UInt, data: Iterable): String? - - /** - * Forget about an FID. - */ - fun clunk(path: String) - - /** - * Remove a file from a server. - */ - fun remove(path: String) - - /** - * Inquire file attributes. - */ - fun stat(path: String) - - /** - * Change file attributes. - */ - fun wstat(path: String) -} \ No newline at end of file diff --git a/src/main/kotlin/ProtocolTranslator.kt b/src/main/kotlin/ProtocolTranslator.kt new file mode 100644 index 0000000..bc64002 --- /dev/null +++ b/src/main/kotlin/ProtocolTranslator.kt @@ -0,0 +1,88 @@ +/* +TODO: + - add arguments to methods +*/ + +/** + * The [ProtocolTranslator] interface provides methods that coincide 1:1 with each request type in the 9P protocol. Every + * method that can fail, that is, every request that can receive a response with `Rerror` type instead of the same type + * as itself, returns a non-null `String` that contains the error message received in the response. + * + * Tags are supposed to be managed internally by the class that provides the implementation. + * + * Trivia: comments for each method are taken from each message type's manual page in section 5. + */ +interface ProtocolTranslator { + /** + * Negotiate protocol version. + * + * This must be the first message sent on the 9P connection and no other requests can be issued until a response has + * been received. + * + * @param msize The maximum length, in bytes, that the client will ever generate or expect to receive in a single + * 9P message. + * @param version Should be "9P2000", which is the only defined value. + * @return a possible error. + */ + fun version(msize: UInt, version: String): String? + + /** + * Perform authentication. + */ + fun auth(afid: UInt, uname: String, aname: String) + + /** + * Abort a message. + */ + fun flush() + + /** + * Establish a connection. + */ + fun attach() + + /** + * Descend a directory hierarchy. + */ + fun walk(path: String) + + /** + * Prepare an FID for I/O on an existing file. + */ + fun open(path: String) + + /** + * Prepare an FID for I/O on a new file. + */ + fun create(path: String) + + /** + * Transfer data from file. + */ + fun read(fid: UInt, offset: ULong, count: UInt): String? + + /** + * Transfer data to file. + */ + fun write(fid: UInt, offset: ULong, count: UInt, data: Iterable): String? + + /** + * Forget about an FID. + */ + fun clunk(path: String) + + /** + * Remove a file from a server. + */ + fun remove(path: String) + + /** + * Inquire file attributes. + */ + fun stat(path: String) + + /** + * Change file attributes. + */ + fun wstat(path: String) +} \ No newline at end of file diff --git a/src/main/kotlin/TransportLayer.kt b/src/main/kotlin/TransportLayer.kt new file mode 100644 index 0000000..514f78b --- /dev/null +++ b/src/main/kotlin/TransportLayer.kt @@ -0,0 +1,74 @@ +import java.io.Closeable + +/** + * [TransportLayer] is an abstract class for network transport-layer operations. A class that implements this + * class' abstract 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 [UnresolvableHostException] + * if the remote address is a domain name, but it cannot be resolved. + * + * Depending on the specific given implementation, the constructor of this class might throw other exceptions (e.g. the + * [java.net.Socket] constructor in [TransportLayerJavaNet]). + */ +abstract class TransportLayer : Closeable { + val address: String + val port: UShort + + /** + * Classic constructor. + * + * @throws UnresolvableHostException if the remote address is a domain name, but it cannot be resolved. + */ + constructor(address: String, port: UShort) { + this.address = address + this.port = port + } + + /** + * This constructor allows addresses specified using one of the styles specified in dial(2). See + * [nineAddressToValues]. + * + * @throws UnresolvableHostException if the remote address is a domain name, but it cannot be resolved. + */ + constructor(fullAddress: String) { + val ap = nineAddressToValues(fullAddress) + this.address = ap.first + this.port = ap.second + } + + /** + * Close the connection. + */ + abstract override fun close() + + /** + * Transmit a payload. + * + * @throws java.io.IOException if the message could not be correctly transmitted. + */ + abstract fun transmit(payload: Array) +/* + /** + * 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. + */ + abstract fun receive(length: ULong): Array +} \ No newline at end of file diff --git a/src/main/kotlin/TransportLayerJavaNet.kt b/src/main/kotlin/TransportLayerJavaNet.kt new file mode 100644 index 0000000..c40b72b --- /dev/null +++ b/src/main/kotlin/TransportLayerJavaNet.kt @@ -0,0 +1,78 @@ +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 : TransportLayer { + /** + * The connection's socket. + */ + private val socket: Socket = Socket(address, 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(address: String, port: UShort) : super(address, port) + constructor(fullAddress: String) : super(fullAddress) + + override fun close() { + if (this.socket.isClosed) { + return + } + this.socket.close() + } + + override fun transmit(payload: Array) { + 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() } + } +} \ No newline at end of file -- cgit v1.2.3