summaryrefslogtreecommitdiff
path: root/src/main/kotlin/net
diff options
context:
space:
mode:
Diffstat (limited to 'src/main/kotlin/net')
-rw-r--r--src/main/kotlin/net/InMessage.kt167
-rw-r--r--src/main/kotlin/net/OutMessage.kt127
-rw-r--r--src/main/kotlin/net/TransportLayer.kt59
-rw-r--r--src/main/kotlin/net/TransportLayerJavaNet.kt95
4 files changed, 448 insertions, 0 deletions
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<String, BigInteger> = mutableMapOf()
+ private set
+
+ /**
+ * A map of each string field's name to its value.
+ */
+ var fieldsStr: MutableMap<String, String> = mutableMapOf()
+ private set
+
+ /**
+ * A map of each raw field's name to its value.
+ */
+ var fieldsRaw: MutableMap<String, Array<UByte>> = mutableMapOf()
+ private set
+
+ /**
+ * An ordered collection of raw bytes that still need to be interpreted as values.
+ */
+ private var rawData: List<UByte>
+
+ 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<Field>) {
+ 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<UByte>, offset: Int, len: Int): BigInteger {
+ val bytes = bytes.drop(offset).take(len)
+ var value = 0.toBigInteger()
+ for (i in 0..<bytes.size) {
+ value += bytes[i].toInt().toBigInteger().shl(i*8)
+ }
+ return value
+ }
+
+ /**
+ * Convert a string from raw bytes.
+ *
+ * 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.
+ *
+ * @return the string.
+ * @throws IllegalArgumentException if either [offset] is negative.
+ */
+ fun convString(bytes: Iterable<UByte>, 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<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
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<UByte>)
+/*
+ /**
+ * 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<UByte>
+*/
+ /**
+ * 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<UByte>
+
+ /**
+ * 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<UByte>
+} \ 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<UByte> {
+ 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<UByte>) {
+ val payload = payload.toList()
+ val bytes = ByteArray(payload.size, { i -> payload[i].toByte() })
+ this.outStream.write(bytes)
+ }
+
+/*
+ override fun receiveUntil(untilByte: UByte): Array<UByte> {
+ var stop = false
+ val payload: Array<UByte> = 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<UByte> {
+ var length = length
+ val intMax = Int.MAX_VALUE.toULong()
+ val bytes: MutableList<Byte> = 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<UByte> {
+ return Iterable { InStreamIterator(this.inStream) }
+ }
+} \ No newline at end of file