summaryrefslogtreecommitdiff
path: root/lib/src/main/kotlin
diff options
context:
space:
mode:
authorEdoardo La Greca2025-08-18 21:09:11 +0200
committerEdoardo La Greca2025-08-18 21:09:11 +0200
commit7341ead2aade10ea1b833e94275277658741883a (patch)
tree46495f24c54278d50aa0da5046822fbe502f3f14 /lib/src/main/kotlin
parent1e50cf9c224d03896f176f3718ff80ef1659e9c2 (diff)
switch to multi-module project structure
Diffstat (limited to 'lib/src/main/kotlin')
-rw-r--r--lib/src/main/kotlin/Authenticator.kt20
-rw-r--r--lib/src/main/kotlin/Authenticator9PAnyV2DP9IK.kt12
-rw-r--r--lib/src/main/kotlin/Connection.kt319
-rw-r--r--lib/src/main/kotlin/FileMode.kt32
-rw-r--r--lib/src/main/kotlin/FilePermissions.kt92
-rw-r--r--lib/src/main/kotlin/IPAddress.kt73
-rw-r--r--lib/src/main/kotlin/NinePMacros.kt8
-rw-r--r--lib/src/main/kotlin/NinePMessageType.kt39
-rw-r--r--lib/src/main/kotlin/PathInfo.kt93
-rw-r--r--lib/src/main/kotlin/ProtocolTranslator.kt167
-rw-r--r--lib/src/main/kotlin/QID.kt105
-rw-r--r--lib/src/main/kotlin/SizedInteger.kt36
-rw-r--r--lib/src/main/kotlin/Stat.kt151
-rw-r--r--lib/src/main/kotlin/TagGenerator.kt63
-rw-r--r--lib/src/main/kotlin/Utils.kt38
-rw-r--r--lib/src/main/kotlin/except/FailedAuthenticationException.kt8
-rw-r--r--lib/src/main/kotlin/except/InvalidMessageException.kt8
-rw-r--r--lib/src/main/kotlin/except/MsizeValueTooBigException.kt10
-rw-r--r--lib/src/main/kotlin/except/RErrorException.kt8
-rw-r--r--lib/src/main/kotlin/except/UnaccessibleFileException.kt11
-rw-r--r--lib/src/main/kotlin/except/UnknownVersionException.kt9
-rw-r--r--lib/src/main/kotlin/except/UnresolvableHostException.kt8
-rw-r--r--lib/src/main/kotlin/net/InMessage.kt167
-rw-r--r--lib/src/main/kotlin/net/OutMessage.kt127
-rw-r--r--lib/src/main/kotlin/net/TransportLayer.kt59
-rw-r--r--lib/src/main/kotlin/net/TransportLayerJavaNet.kt95
26 files changed, 1758 insertions, 0 deletions
diff --git a/lib/src/main/kotlin/Authenticator.kt b/lib/src/main/kotlin/Authenticator.kt
new file mode 100644
index 0000000..2d501ae
--- /dev/null
+++ b/lib/src/main/kotlin/Authenticator.kt
@@ -0,0 +1,20 @@
+/**
+ * The Authenticator interface provides methods for authenticating a user over an established protocol connection.
+ */
+interface Authenticator {
+ /**
+ * Authenticate a user identified by the given [username] and whose authenticity is confirmed by the given [key].
+ * The authentication protocol can read and write data within the underlying connection using [readFun] and
+ * [writeFun].
+ *
+ * @param username The name the user goes by.
+ * @param key The confirmation of the user's identity and authenticity. It could be any public, private, or derived
+ * (e.g. mixed with something else or hashed) datum, possibly shared between the server and the client, such as a
+ * UTF-8-encoded password or a raw public key.
+ * @param readFun A function to read incoming data from the underlying connection.
+ * @param writeFun A function to write outgoing data into the underlying connection.
+ * @throws except.FailedAuthenticationException if the authentication could not be performed. A human-readable
+ * reason for the failure can be provided if necessary.
+ */
+ fun authenticate(username: String, key: List<UByte>, readFun: () -> List<UByte>, writeFun: (b: List<UByte>) -> Unit)
+} \ No newline at end of file
diff --git a/lib/src/main/kotlin/Authenticator9PAnyV2DP9IK.kt b/lib/src/main/kotlin/Authenticator9PAnyV2DP9IK.kt
new file mode 100644
index 0000000..a6495b1
--- /dev/null
+++ b/lib/src/main/kotlin/Authenticator9PAnyV2DP9IK.kt
@@ -0,0 +1,12 @@
+/**
+ * This class (with an ugly ass name) implements the authentication procedure for the p9any meta-protocol version 2,
+ * hinting at the usage of dp9ik during negotiation and failing if it's unavailable.
+ *
+ * The 9P protocol does not provide a default authentication method. However, since NineKt must work with 9front's
+ * default authenticated 9P service, it must implement the p9any meta-protocol, preferably version 2.
+ */
+class Authenticator9PAnyV2DP9IK : Authenticator {
+ override fun authenticate(username: String, key: List<UByte>, readFun: () -> List<UByte>, writeFun: (List<UByte>) -> Unit) {
+
+ }
+} \ No newline at end of file
diff --git a/lib/src/main/kotlin/Connection.kt b/lib/src/main/kotlin/Connection.kt
new file mode 100644
index 0000000..7f2e505
--- /dev/null
+++ b/lib/src/main/kotlin/Connection.kt
@@ -0,0 +1,319 @@
+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
+
+/**
+ * This class represents a 9P connection. It provides a practical implementation with networking to the 9P methods
+ * 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 [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 {
+ /**
+ * Networking API.
+ */
+ private val tl: TransportLayer = transLay
+
+ /**
+ * Tag generator.
+ */
+ private val tagGen = TagGenerator(TagGenerator.TagGenerationMethod.INCREMENTAL, 1u)
+
+ /**
+ * Maximum size for messages negotiated between the client and the server.
+ */
+ private var maxSize: UInt = 0u
+
+ // 9P constants.
+ val DEFAULT_VERSION = "9P2000"
+ val NOTAG = 0.toUShort().inv()
+ val NOFID = 0.toUInt().inv()
+
+
+ /**
+ * Disconnect from the remote host,
+ *
+ * @throws IOException if an I/O error occurred while closing the socket.
+ */
+ fun disconnect() {
+ this.tl.close()
+ }
+
+ /**
+ * 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 [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 [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.
+ */
+ private fun checkedInMessage(reqTag: UShort): InMessage {
+ val imsg = InMessage(this.tl, this.maxSize, reqTag)
+ if (imsg.type == NinePMessageType.RERROR) {
+ imsg.applyField(InMessage.Field("ename", InMessage.Field.Type.STRING, 0u))
+ throw RErrorException(imsg.fieldsStr["ename"])
+ }
+ return imsg
+ }
+
+ override fun version(msize: UInt, version: String) {
+ val omsg = OutMessage(NinePMessageType.TVERSION, this.NOTAG, listOf("msize", "version"),
+ mapOf(
+ "msize" to Pair(BigInteger(msize.toString()), 4u)
+ ),
+ mapOf(
+ "version" to version
+ ),
+ emptyMap(),
+ this.maxSize
+ )
+ omsg.write(this.tl)
+ val imsg = checkedInMessage(omsg.tag)
+ imsg.applySchema(listOf(
+ InMessage.Field("msize", InMessage.Field.Type.INTEGER, 4u),
+ InMessage.Field("version", InMessage.Field.Type.STRING, 0u)
+ ))
+ val remoteMaxSize = imsg.fieldsInt["msize"]!!.toInt().toUInt()
+ if (remoteMaxSize > this.maxSize) {
+ throw MsizeValueTooBigException(msize, remoteMaxSize)
+ }
+ this.maxSize = remoteMaxSize
+ if (!imsg.fieldsStr["version"]!!.startsWith("9P2000")) {
+ throw UnknownVersionException(imsg.fieldsStr["version"]!!)
+ }
+ }
+
+ override fun auth(afid: UInt, uname: String, aname: String) {
+ }
+
+ override fun flush(oldtag: UShort) {
+ val omsg = OutMessage(NinePMessageType.TFLUSH, this.tagGen.generate(), listOf("oldtag"),
+ mapOf(
+ "oldtag" to Pair(BigInteger(oldtag.toString()), 2u)
+ ),
+ emptyMap(),
+ emptyMap(),
+ this.maxSize
+ )
+ omsg.write(this.tl)
+ val imsg = checkedInMessage(omsg.tag)
+ }
+
+ override fun attach(fid: UInt, afid: UInt, uname: String, aname: String): QID {
+ val omsg = OutMessage(NinePMessageType.TATTACH, this.tagGen.generate(), listOf("fid", "afid", "uname", "aname"),
+ mapOf(
+ "fid" to Pair(BigInteger(fid.toString()), 4u),
+ "afid" to Pair(BigInteger(afid.toString()), 4u)
+ ),
+ mapOf(
+ "uname" to uname,
+ "aname" to aname
+ ),
+ emptyMap(),
+ this.maxSize
+ )
+ omsg.write(this.tl)
+ val imsg = checkedInMessage(omsg.tag)
+
+ imsg.applyField(InMessage.Field("qid", InMessage.Field.Type.RAW, 13u))
+ val qid = QID(imsg.fieldsRaw["qid"]!!.toList())
+ return qid
+ }
+
+ override fun walk(fid: UInt, newfid: UInt, wname: List<String>): List<QID> {
+ val nwname = wname.size
+ val omsg = OutMessage(NinePMessageType.TWALK, this.tagGen.generate(), listOf("fid", "newfid", "nwname"),
+ mapOf(
+ "fid" to Pair(BigInteger(fid.toString()), 4u),
+ "newfid" to Pair(BigInteger(newfid.toString()), 4u),
+ "nwname" to Pair(BigInteger(nwname.toString()), 2u)
+ ),
+ emptyMap(),
+ emptyMap(),
+ this.maxSize
+ )
+ omsg.write(this.tl)
+ for (wn in wname) { // write wname elements
+ this.tl.transmit(OutMessage.convStringToBytes(wn))
+ }
+ val imsg = checkedInMessage(omsg.tag)
+
+ imsg.applyField(InMessage.Field("nwqid", InMessage.Field.Type.INTEGER, 2u))
+ val nwqid = imsg.fieldsInt["nwqid"]!!.toInt()
+ if (nwqid < nwname) {
+ throw UnaccessibleFileException(wname.slice(0..nwqid))
+ }
+ imsg.applyField(InMessage.Field("qids", InMessage.Field.Type.RAW, (nwqid * 13).toUInt()))
+ val rawQids = imsg.fieldsRaw["qids"]!!
+ val qids: MutableList<QID> = mutableListOf()
+ for (i in 0..<nwqid/13) {
+ val start = i * 13
+ val end = ((i+1) * 13) - 1
+ val rawQid = rawQids.slice(start..end)
+ qids += QID(rawQid)
+ }
+ return qids.toList()
+ }
+
+ override fun open(fid: UInt, mode: FileMode): Pair<QID, UInt> {
+ val omsg = OutMessage(NinePMessageType.TOPEN, this.tagGen.generate(), listOf("fid", "mode"),
+ mapOf(
+ "fid" to Pair(BigInteger(fid.toString()), 4u),
+ "mode" to Pair(BigInteger(mode.toModeByte().toString()), 1u)
+ ),
+ emptyMap(),
+ emptyMap(),
+ this.maxSize
+ )
+ omsg.write(this.tl)
+ val imsg = checkedInMessage(omsg.tag)
+ imsg.applySchema(listOf(
+ InMessage.Field("qid", InMessage.Field.Type.RAW, 13u),
+ InMessage.Field("iounit", InMessage.Field.Type.INTEGER, 4u)
+ ))
+ val qid = QID(imsg.fieldsRaw["qid"]!!.toList())
+ val iounit = imsg.fieldsInt["iounit"]!!.toInt().toUInt()
+ return Pair(qid, iounit)
+ }
+
+ override fun create(fid: UInt, name: String, perm: FilePermissions, mode: FileMode): Pair<QID, UInt> {
+ val omsg = OutMessage(NinePMessageType.TCREATE, this.tagGen.generate(), listOf("fid", "name", "perm", "mode"),
+ mapOf(
+ "fid" to Pair(BigInteger(fid.toString()), 4u),
+ "perm" to Pair(BigInteger(perm.toPermissionInt().toString()), 4u),
+ "mode" to Pair(BigInteger(mode.toModeByte().toString()), 1u)
+ ),
+ mapOf(
+ "name" to name
+ ),
+ emptyMap(),
+ this.maxSize
+ )
+ omsg.write(this.tl)
+ val imsg = checkedInMessage(omsg.tag)
+ imsg.applySchema(listOf(
+ InMessage.Field("qid", InMessage.Field.Type.RAW, 13u),
+ InMessage.Field("iounit", InMessage.Field.Type.INTEGER, 4u)
+ ))
+ val qid = QID(imsg.fieldsRaw["qid"]!!.toList())
+ val iounit = imsg.fieldsInt["iounit"]!!.toInt().toUInt()
+ return Pair(qid, iounit)
+ }
+
+ override fun read(fid: UInt, offset: ULong, count: UInt): Array<UByte> {
+ val omsg = OutMessage(NinePMessageType.TREAD, this.tagGen.generate(), listOf("fid", "offset", "count"),
+ mapOf(
+ "fid" to Pair(BigInteger(fid.toString()), 4u),
+ "offset" to Pair(BigInteger(offset.toString()), 8u),
+ "count" to Pair(BigInteger(count.toString()), 4u)
+ ),
+ emptyMap(),
+ emptyMap(),
+ this.maxSize
+ )
+ omsg.write(this.tl)
+ val imsg = checkedInMessage(omsg.tag)
+ imsg.applyField(InMessage.Field("count", InMessage.Field.Type.INTEGER, 4u))
+ val count = imsg.fieldsInt["count"]!!.toInt().toUInt()
+ imsg.applyField(InMessage.Field("data", InMessage.Field.Type.RAW, count))
+
+ return imsg.fieldsRaw["data"]!!
+ }
+
+ override fun write(fid: UInt, offset: ULong, count: UInt, data: Iterable<UByte>): UInt {
+ val data = data.take(count.toInt()).toTypedArray()
+ val omsg = OutMessage(NinePMessageType.TWRITE, this.tagGen.generate(), listOf("offset", "count", "data"),
+ mapOf(
+ "offset" to Pair(BigInteger(offset.toString()), 8u),
+ "count" to Pair(BigInteger(count.toString()), 4u)
+ ),
+ emptyMap(),
+ mapOf(
+ "data" to data.toList()
+ ),
+ this.maxSize
+ )
+ omsg.write(this.tl)
+ val imsg = checkedInMessage(omsg.tag)
+ imsg.applyField(InMessage.Field("count", InMessage.Field.Type.INTEGER, 4u))
+ val count = imsg.fieldsInt["count"]!!
+
+ return count.toInt().toUInt()
+ }
+
+ override fun clunk(fid: UInt) {
+ val omsg = OutMessage(NinePMessageType.TCLUNK, this.tagGen.generate(), listOf("fid"),
+ mapOf(
+ "fid" to Pair(BigInteger(fid.toString()), 4u)
+ ),
+ emptyMap(),
+ emptyMap(),
+ this.maxSize
+ )
+ omsg.write(this.tl)
+ val imsg = checkedInMessage(omsg.tag)
+ }
+
+ override fun remove(fid: UInt) {
+ val omsg = OutMessage(NinePMessageType.TREMOVE, this.tagGen.generate(), listOf("fid"),
+ mapOf(
+ "fid" to Pair(BigInteger(fid.toString()), 4u)
+ ),
+ emptyMap(),
+ emptyMap(),
+ this.maxSize
+ )
+ omsg.write(this.tl)
+ val imsg = checkedInMessage(omsg.tag)
+ }
+
+ override fun stat(fid: UInt): Stat {
+ val omsg = OutMessage(NinePMessageType.TSTAT, this.tagGen.generate(), listOf("fid"),
+ mapOf(
+ "fid" to Pair(BigInteger(fid.toString()), 4u)
+ ),
+ emptyMap(),
+ emptyMap(),
+ this.maxSize
+ )
+ omsg.write(this.tl)
+ val imsg = checkedInMessage(omsg.tag)
+ imsg.applyField(InMessage.Field("nstat", InMessage.Field.Type.INTEGER, 2u))
+ val nstat = imsg.fieldsInt["nstat"]!!.toInt().toUInt()
+ imsg.applyField(InMessage.Field("stat", InMessage.Field.Type.RAW, nstat))
+ val rawStat = imsg.fieldsRaw["stat"]!!.toList()
+ return Stat(fid, rawStat)
+ }
+
+ override fun wstat(fid: UInt, stat: Stat) {
+ val omsg = OutMessage(NinePMessageType.TWSTAT, this.tagGen.generate(), listOf("fid", "stat"),
+ mapOf(
+ "fid" to Pair(BigInteger(fid.toString()), 4u)
+ ),
+ emptyMap(),
+ mapOf(
+ "stat" to stat.toRaw()
+ ),
+ this.maxSize
+ )
+ omsg.write(this.tl)
+ val imsg = checkedInMessage(omsg.tag)
+ }
+} \ No newline at end of file
diff --git a/lib/src/main/kotlin/FileMode.kt b/lib/src/main/kotlin/FileMode.kt
new file mode 100644
index 0000000..81712e6
--- /dev/null
+++ b/lib/src/main/kotlin/FileMode.kt
@@ -0,0 +1,32 @@
+/**
+ * The mode in which to open or create a file. For directories, it's illegal for directories to be written, truncated,
+ * or removed on close.
+ *
+ * @param mode The actual mode, as described by the [Mode] enum class.
+ * @param truncate Set or unset the truncation bit. It requires the write permission.
+ * @param removeClose Set or unset the "remove on close" bit. It requires the remove permission in the parent directory.
+ */
+data class FileMode(val mode: Mode, val truncate: Boolean, val removeClose: Boolean) {
+ enum class Mode(val value: UByte) {
+ READ(0u),
+ WRITE(1u),
+ READ_WRITE(2u),
+ EXECUTE(3u)
+ }
+
+ /**
+ * Turn the mode described by the [FileMode] fields into a mode byte.
+ */
+ fun toModeByte(): UByte {
+ var byte: UByte = 0u
+ byte = byte.or(this.mode.value)
+ if (truncate) {
+ byte = byte.or(0x10u)
+ }
+ if (removeClose) {
+ byte = byte.or(0x40u)
+ }
+
+ return byte
+ }
+} \ No newline at end of file
diff --git a/lib/src/main/kotlin/FilePermissions.kt b/lib/src/main/kotlin/FilePermissions.kt
new file mode 100644
index 0000000..84a7944
--- /dev/null
+++ b/lib/src/main/kotlin/FilePermissions.kt
@@ -0,0 +1,92 @@
+/**
+ * The permissions of a newly created file.
+ */
+class FilePermissions {
+
+ /**
+ * The permissions for the file's owning user.
+ */
+ val userPerms: Permissions
+
+ /**
+ * The permissions for the file's owning group.
+ */
+ val groupPerms: Permissions
+
+ /**
+ * The permissions for everyone else.
+ */
+ val elsePerms: Permissions
+
+ /**
+ * Is the file a directory? If not, it's a regular file.
+ */
+ val isDirectory: Boolean
+
+ private val DMDIR: UInt = 0x80000000u
+
+ /**
+ * Constructor for file permissions with separate fields.
+ *
+ * @param userPerms The permissions for the file's owning user.
+ * @param groupPerms The permissions for the file's owning group.
+ * @param elsePerms The permissions for everyone else.
+ * @param isDirectory Is the file a directory? If not, it's a regular file.
+ */
+ constructor(userPerms: Permissions, groupPerms: Permissions, elsePerms: Permissions, isDirectory: Boolean) {
+ this.userPerms = userPerms
+ this.groupPerms = groupPerms
+ this.elsePerms = elsePerms
+ this.isDirectory = isDirectory
+ }
+
+ /**
+ * Constructor for raw file permission data. Only the first 4 elements are read.
+ *
+ * @param raw The raw file permission data.
+ * @throws IllegalArgumentException if [raw] does not have at least 4 elements.
+ */
+ constructor(raw: List<UByte>) {
+ require(raw.size >= 4)
+ val raw = raw.slice(0..4)
+ val dirValue = raw[0].toUInt().xor(this.DMDIR)
+ this.isDirectory = dirValue > 0u
+ this.userPerms = Permissions.fromByte(raw[1])
+ this.groupPerms = Permissions.fromByte(raw[2])
+ this.elsePerms = Permissions.fromByte(raw[3])
+ }
+
+ enum class Permissions(val bits: UByte) {
+ READ(0x4u),
+ WRITE(0x2u),
+ EXECUTE(0x1u),
+ READ_WRITE(READ.bits.or(WRITE.bits)),
+ READ_EXECUTE(READ.bits.or(EXECUTE.bits)),
+ WRITE_EXECUTE(WRITE.bits.or(EXECUTE.bits)),
+ READ_WRITE_EXECUTE(READ.bits.or(WRITE.bits.or(EXECUTE.bits)));
+
+ companion object {
+ /**
+ * Obtain a [Permissions] instance by matching its value.
+ *
+ * @throws NoSuchElementException if no such element has the provided value.
+ */
+ fun fromByte(bits: UByte) = Permissions.entries.first { it.bits == bits }
+ }
+ }
+
+ /**
+ * Turn the permissions described by the [FilePermissions] fields into a permission integer (4 bytes).
+ */
+ fun toPermissionInt(): UInt {
+ val permFileds = listOf(userPerms, groupPerms, elsePerms)
+ val perms: UInt = 0u
+ for (i in 0..permFileds.size) {
+ perms.or(permFileds[i].bits.toUInt().shl(8 * (permFileds.size - 1 - i)))
+ }
+ if (isDirectory) {
+ perms.or(this.DMDIR)
+ }
+ return perms
+ }
+} \ No newline at end of file
diff --git a/lib/src/main/kotlin/IPAddress.kt b/lib/src/main/kotlin/IPAddress.kt
new file mode 100644
index 0000000..8eb7414
--- /dev/null
+++ b/lib/src/main/kotlin/IPAddress.kt
@@ -0,0 +1,73 @@
+/**
+ * An IP address (v4 or v6).
+ */
+class IPAddress {
+ private var address: Array<UByte>
+ private var is4: Boolean
+
+ /**
+ * @throws NumberFormatException if the IP address follows neither the IPv4 syntax, nor the IPv6 syntax.
+ */
+ constructor(address: String) {
+ if (isAddressV4(address)) {
+ this.is4 = true
+ } else if (isAddressV6(address)) {
+ this.is4 = false
+ } else {
+ throw NumberFormatException()
+ }
+
+ val split4 = address.split('.')
+ val split6 = address.split(':')
+ val bytes: List<UByte>
+ if (this.is4) {
+ bytes = split4.map { it.toUByte(10) }
+ } else {
+ val shorts = split6.map { it.toShort(16) }
+ bytes = shorts.flatMap {
+ listOf(
+ it.toInt().and(0xFF00).shr(0x08).toUByte(),
+ it.toInt().and(0x00FF).toUByte()
+ )
+ }
+ }
+ this.address = bytes.toTypedArray()
+ }
+
+ private fun isAddressV4(address: String): Boolean {
+ val split4 = address.split('.')
+ var isOK = split4.size == 4
+ isOK = isOK && split4.size == split4.filter { it.matches(Regex("^[0-9]+$")) }.size
+ return isOK
+ }
+
+ private fun isAddressV6(address: String): Boolean {
+ val split6 = address.split(':')
+ var isOK = split6.size == 8
+ isOK = isOK && split6.size == split6.filter { it.contains(Regex("^[0-9][a-f]+$")) }.size
+ if (isOK) {
+ return true
+ }
+
+ // try with "::"
+ val split6Double = address.split("::")
+ if (split6Double.size != 2) {
+ return false
+ }
+ val omitted = 8 - split6Double[0].split(':').size - split6Double[1].split(':').size
+ if (omitted < 0) {
+ return false
+ }
+ return true
+ }
+
+ override fun toString(): String {
+ val str: String
+ if (this.is4) {
+ str = this.address.joinToString(separator = ".") { it.toString() }
+ } else {
+ str = this.address.joinToString(separator = ":") { it.toString() }
+ }
+ return str
+ }
+} \ No newline at end of file
diff --git a/lib/src/main/kotlin/NinePMacros.kt b/lib/src/main/kotlin/NinePMacros.kt
new file mode 100644
index 0000000..d5aa83e
--- /dev/null
+++ b/lib/src/main/kotlin/NinePMacros.kt
@@ -0,0 +1,8 @@
+/**
+ * Handy function that initializes the 9P connection.
+ */
+fun initNineP(npconn: Connection) {
+ // TODO: implement
+}
+
+fun readAll(npconn: Connection, ) \ No newline at end of file
diff --git a/lib/src/main/kotlin/NinePMessageType.kt b/lib/src/main/kotlin/NinePMessageType.kt
new file mode 100644
index 0000000..4c8c60f
--- /dev/null
+++ b/lib/src/main/kotlin/NinePMessageType.kt
@@ -0,0 +1,39 @@
+enum class NinePMessageType(val value: UByte) {
+ TVERSION(100u),
+ RVERSION(101u),
+ TAUTH(102u),
+ RAUTH(103u),
+ TATTACH(104u),
+ RATTACH(105u),
+ //TERROR(106), <--- illegal
+ RERROR(107u),
+ TFLUSH(108u),
+ RFLUSH(109u),
+ TWALK(110u),
+ RWALK(111u),
+ TOPEN(112u),
+ ROPEN(113u),
+ TCREATE(114u),
+ RCREATE(115u),
+ TREAD(116u),
+ RREAD(117u),
+ TWRITE(118u),
+ RWRITE(119u),
+ TCLUNK(120u),
+ RCLUNK(121u),
+ TREMOVE(122u),
+ RREMOVE(123u),
+ TSTAT(124u),
+ RSTAT(125u),
+ TWSTAT(126u),
+ RWSTAT(127u);
+
+ companion object {
+ /**
+ * Obtain a [NinePMessageType] instance by matching its value.
+ *
+ * @throws NoSuchElementException if no such element has the provided value.
+ */
+ fun fromByte(value: UByte) = NinePMessageType.entries.first { it.value == value }
+ }
+} \ No newline at end of file
diff --git a/lib/src/main/kotlin/PathInfo.kt b/lib/src/main/kotlin/PathInfo.kt
new file mode 100644
index 0000000..2aee406
--- /dev/null
+++ b/lib/src/main/kotlin/PathInfo.kt
@@ -0,0 +1,93 @@
+/**
+ * This class holds all info about paths, their FIDs, and their QIDs.
+ */
+class PathInfo() {
+ private val paths: MutableSet<Path> = mutableSetOf()
+
+ /**
+ * Information about a path. Each FID maps to one path but not all paths have FIDs (injective). On the other hand,
+ * one QID can be shared with different paths and FIDs.
+ *
+ * FIDs are optional because some R-messages only return QIDs for certain files (e.g. walk).
+ *
+ * @param path The path.
+ * @param fid The optional FID associated with the path.
+ * @param qid The QID associated with the path.
+ */
+ data class Path(val path: List<String>, val fid: UInt?, val qid: QID)
+
+ /**
+ * Add a path.
+ */
+ fun addPath(path: Path) {
+ this.paths.add(path)
+ }
+
+ /**
+ * Add a QID to an existing FID. Do nothing if the given FID does not exist.
+ */
+ fun addQIDToFID(fid: UInt, qid: QID) {
+ findByFID(fid)?.qid = qid
+ }
+
+ /**
+ * Remove a FID.
+ */
+ fun removeFID(fid: UInt) {
+ this.paths.removeIf { x -> x.fid == fid }
+ }
+
+ fun find(predicate: (Path) -> Boolean) = this.paths.find(predicate)
+
+ /**
+ * Find [Path] object by path.
+ *
+ * @param path The path to search for.
+ * @return A path object if [path] exists, or null otherwise.
+ */
+ fun findByPath(path: List<String>): Path? {
+ return this.paths.find { x -> x.path == path }
+ }
+
+ /**
+ * Find [Path] object by FID.
+ *
+ * @param fid The FID to search for.
+ * @return A path object if [fid] exists, or null otherwise.
+ */
+ fun findByFID(fid: UInt): Path? {
+ return this.paths.find { x -> x.fid == fid }
+ }
+
+ /**
+ * Find [Path] object by QID.
+ *
+ * @param qid The path to search for.
+ * @return A path object if [qid] exists, or null otherwise.
+ */
+ fun findByQID(qid: QID): Path? {
+ return this.paths.find { x -> x.qid == qid }
+ }
+
+ /**
+ * Retrieve all paths
+ */
+ fun getAllPaths(): Set<Path> {
+ return this.paths.toSet()
+ }
+
+ /**
+ * Generate a new FID which is not already in use.
+ *
+ * @return The new FID.
+ * @throws IllegalStateException if there is no available FID.
+ */
+ fun genFID(): UInt {
+ for (newFid in 0u..UInt.MAX_VALUE) {
+ if (findByFID(newFid) == null) {
+ return newFid
+ }
+ }
+ throw IllegalStateException()
+ }
+} \ No newline at end of file
diff --git a/lib/src/main/kotlin/ProtocolTranslator.kt b/lib/src/main/kotlin/ProtocolTranslator.kt
new file mode 100644
index 0000000..fee52f4
--- /dev/null
+++ b/lib/src/main/kotlin/ProtocolTranslator.kt
@@ -0,0 +1,167 @@
+/*
+TODO:
+ - add arguments to methods
+ - switch from returned strings to exceptions
+*/
+
+/**
+ * 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 and the `msize` value are supposed to be managed internally by the implementing class.
+ *
+ * When compared to 9P's formal message descriptions, like those which can be read in Plan 9's manual pages, some of the
+ * methods might lack parameters. Those which can be inferred from the existing parameters are purposefully omitted. An
+ * example is [walk], which omits `nwname` because it can be obtained by calculating the size of `wname`.
+ *
+ * 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.
+ * @throws except.InvalidMessageException if the received message is invalid.
+ * @throws except.RErrorException if the received message is an R-error message.
+ * @throws except.MsizeValueTooBigException if the received `msize` value is bigger than what the client requested.
+ * @throws except.UnknownVersionException if the version negotiation failed.
+ */
+ fun version(msize: UInt, version: String)
+
+ /**
+ * Perform authentication.
+ */
+ fun auth(afid: UInt, uname: String, aname: String)
+
+ /**
+ * Abort a message.
+ *
+ * @param oldtag The tag of the message that needs to be purged.
+ * @throws except.InvalidMessageException if the received message is invalid.
+ * @throws except.RErrorException if the received message is an R-error message.
+ */
+ fun flush(oldtag: UShort)
+
+ /**
+ * Establish a connection.
+ *
+ * @param fid FID that represents the root directory of the desired file tree.
+ * @param afid A FID previously established by an auth message.
+ * @param uname A user identifier.
+ * @param aname The desired file tree to access.
+ * @return The QID of the file tree's root.
+ * @throws except.InvalidMessageException if the received message is invalid.
+ * @throws except.RErrorException if the received message is an R-error message.
+ */
+ fun attach(fid: UInt, afid: UInt, uname: String, aname: String): QID
+
+ /**
+ * Descend a directory hierarchy.
+ *
+ * @param fid The existing FID. It must represent a directory.
+ * @param newfid The proposed FID, which is going to be associated with the result of walking the hierarchy. It must
+ * be not in use, unless it is the same as [fid].
+ * @param wname The successive path name elements which describe the file to walk to.
+ * @return The QID of each path element that has been walked through.
+ * @throws except.InvalidMessageException if the received message is invalid.
+ * @throws except.RErrorException if the received message is an R-error message.
+ * @throws except.UnaccessibleFileException if [wname] contains one or more path elements that do not exist.
+ */
+ fun walk(fid: UInt, newfid: UInt, wname: List<String>): List<QID>
+
+ /**
+ * Prepare an FID for I/O on an existing file.
+ *
+ * @param fid The FID of the file to open.
+ * @param mode The mode in which the file is opened.
+ * @return A pair of: (1) the returned QID, and (2) a value called `iounit` that indicates, if non-zero, the maximum
+ * number of bytes that are guaranteed to be read from or written to the file without breaking the I/O transfer into
+ * multiple 9P messages.
+ * @throws except.InvalidMessageException if the received message is invalid.
+ * @throws except.RErrorException if the received message is an R-error message.
+ */
+ fun open(fid: UInt, mode: FileMode): Pair<QID, UInt>
+
+ /**
+ * Prepare an FID for I/O on a new file.
+ *
+ * @param fid The FID of the directory that is going to contain the file. The specified directory requires write
+ * permission.
+ * @param name The file name.
+ * @param perm The permissions of the new file.
+ * @param mode The open mode after successful creation.
+ * @return A pair of: (1) the QID of the newly created file, and (2) a value called `iounit` that indicates, if
+ * non-zero, the maximum number of bytes that are guaranteed to be read from or written to the file without breaking
+ * the I/O transfer into multiple 9P messages.
+ * @throws except.InvalidMessageException if the received message is invalid.
+ * @throws except.RErrorException if the received message is an R-error message.
+ */
+ fun create(fid: UInt, name: String, perm: FilePermissions, mode: FileMode): Pair<QID, UInt>
+
+ /**
+ * Transfer data from file. Due to the negotiated maximum size of 9P messages, called `msize`, one is supposed to
+ * call this method multiple times, unless the content is smaller than `msize`.
+ *
+ * @return The content read with the call just made.
+ * @throws except.InvalidMessageException if the received message is invalid.
+ * @throws except.RErrorException if the received message is an R-error message.
+ */
+ fun read(fid: UInt, offset: ULong, count: UInt): Array<UByte>
+
+ /**
+ * Transfer data to file. Due to the negotiated maximum size of 9P messages, called `msize`, this method is supposed
+ * to be called multiple times, unless the content is smaller than `msize`.
+ *
+ * @param fid The FID to write to.
+ * @param offset The distance between the beginning of the file and the first written byte.
+ * @param data The raw bytes that are going to be written.
+ * @return The amount of bytes written with the call just made.
+ * @throws except.InvalidMessageException if the received message is invalid.
+ * @throws except.RErrorException if the received message is an R-error message.
+ */
+ fun write(fid: UInt, offset: ULong, count: UInt, data: Iterable<UByte>): UInt
+
+ /**
+ * Forget about a FID.
+ *
+ * @param fid The FID to forget.
+ * @throws except.InvalidMessageException if the received message is invalid.
+ * @throws except.RErrorException if the received message is an R-error message.
+ */
+ fun clunk(fid: UInt)
+
+ /**
+ * Remove a file from a server.
+ *
+ * @param fid The FID of the file to remove.
+ * @throws except.InvalidMessageException if the received message is invalid.
+ * @throws except.RErrorException if the received message is an R-error message.
+ */
+ fun remove(fid: UInt)
+
+ /**
+ * Inquire file attributes.
+ *
+ * @param fid The FID of the file to inquire.
+ * @return All the file attributes of the file associated with [fid].
+ * @throws except.InvalidMessageException if the received message is invalid.
+ * @throws except.RErrorException if the received message is an R-error message.
+ */
+ fun stat(fid: UInt): Stat
+
+ /**
+ * Change file attributes.
+ *
+ * @param fid The FID of the file to set attributes of.
+ * @param stat The attributes to set.
+ * @throws except.InvalidMessageException if the received message is invalid.
+ * @throws except.RErrorException if the received message is an R-error message.
+ */
+ fun wstat(fid: UInt, stat: Stat)
+} \ No newline at end of file
diff --git a/lib/src/main/kotlin/QID.kt b/lib/src/main/kotlin/QID.kt
new file mode 100644
index 0000000..4d0bd6a
--- /dev/null
+++ b/lib/src/main/kotlin/QID.kt
@@ -0,0 +1,105 @@
+import net.InMessage
+import net.OutMessage
+import java.math.BigInteger
+
+/**
+ * This class holds information about a single QID.
+ */
+class QID {
+ /**
+ * Does the QID represent a directory?
+ */
+ val isDirectory get() = getIsDirectory()
+
+ /**
+ * Does the QID represent an append-only file?
+ */
+ val isAppendOnly get() = getIsAppendOnly()
+
+ /**
+ * Does the QID represent an exclusive-use file?
+ */
+ val isExclusive get() = getIsExclusive()
+
+ /**
+ * Does the QID represent a temporary file?
+ */
+ val isTemporary get() = getIsTemporary()
+
+ /**
+ * The QID type.
+ */
+ val type: UByte
+
+ /**
+ * The QID file version.
+ */
+ val version: UInt
+
+ /**
+ * The QID path.
+ */
+ val path: ULong
+
+ /**
+ * Constructor for a QID with separate fields. See the `stat(5)` manual page for more information about [type],
+ * [version], and [path].
+ *
+ * @param type The QID type.
+ * @param version The QID file version.
+ * @param path The QID path.
+ */
+ constructor(type: UByte, version: UInt, path: ULong) {
+ this.type = type
+ this.version = version
+ this.path = path
+ }
+
+ /**
+ * Constructor for raw QID data. Only the first 13 elements are read.
+ *
+ * @param raw The raw QID data.
+ * @throws IllegalArgumentException if [raw] does not have at least 13 elements.
+ */
+ constructor(raw: List<UByte>) {
+ require(raw.size >= 13)
+ this.type = raw.first()
+ val rawVersion = raw.slice(1..4)
+ val rawPath = raw.slice(5..12)
+ this.version = InMessage.convInteger(rawVersion, 0, rawVersion.size).toInt().toUInt()
+ this.path = InMessage.convInteger(rawPath, 0, rawPath.size).toLong().toULong()
+ }
+
+ /**
+ * Check bit values in [type]. In case of multiple bits, the method returns true if at least one of them is 1.
+ *
+ * @param bits A byte whose bits set to 1 are checked.
+ */
+ private fun checkTypeBits(bits: UByte): Boolean {
+ return this.type.and(bits) != 0u.toUByte()
+ }
+
+ private fun getIsDirectory(): Boolean {
+ return checkTypeBits(0x80u)
+ }
+
+ private fun getIsAppendOnly(): Boolean {
+ return checkTypeBits(0x40u)
+ }
+
+ private fun getIsExclusive(): Boolean {
+ return checkTypeBits(0x20u)
+ }
+
+ private fun getIsTemporary(): Boolean {
+ return checkTypeBits(0x04u)
+ }
+
+ fun toRaw(): List<UByte> {
+ var bytes: List<UByte> = emptyList()
+ bytes += this.type
+ bytes += OutMessage.convIntegerToBytes(BigInteger(this.version.toString()), 4u)
+ bytes += OutMessage.convIntegerToBytes(BigInteger(this.path.toString()), 8u)
+ return bytes
+ }
+} \ No newline at end of file
diff --git a/lib/src/main/kotlin/SizedInteger.kt b/lib/src/main/kotlin/SizedInteger.kt
new file mode 100644
index 0000000..48e4e25
--- /dev/null
+++ b/lib/src/main/kotlin/SizedInteger.kt
@@ -0,0 +1,36 @@
+import java.math.BigInteger
+
+/**
+ * [SizedInteger] represents an unsigned integer number of arbitrary yet fixed length. It's useful for storing integers
+ * got from a connection as message fields.
+ *
+ * @param size The size of the field, measured in bytes.
+ * @param value The value of the field.
+ *
+ * @throws IllegalArgumentException if the size required to store the provided value is bigger than the provided size.
+ */
+class SizedInteger(val size: ULong, value: BigInteger) {
+ init {
+ if (!this.sizeOk(size, value)) {
+ throw IllegalArgumentException()
+ }
+ }
+
+ constructor(size: ULong) : this(size, 0.toBigInteger())
+
+ /**
+ * @throws IllegalStateException on set if the declared size is not enough for the new value.
+ */
+ var value = value
+ set(value) {
+ if (!this.sizeOk(value)) {
+ throw IllegalStateException()
+ }
+ }
+
+ private fun sizeOk(size: ULong, value: BigInteger): Boolean {
+ val requiredSize = value.toByteArray().size.toUInt()
+ return requiredSize <= size
+ }
+ private fun sizeOk(value: BigInteger): Boolean = sizeOk(this.size, value)
+} \ No newline at end of file
diff --git a/lib/src/main/kotlin/Stat.kt b/lib/src/main/kotlin/Stat.kt
new file mode 100644
index 0000000..20692c5
--- /dev/null
+++ b/lib/src/main/kotlin/Stat.kt
@@ -0,0 +1,151 @@
+import net.InMessage
+import net.OutMessage
+import java.math.BigInteger
+
+// TODO: add time conversion methods
+/**
+ * File attributes. The `type` and `dev` attributes are ignored since they are for kernel use only. Time is measured in
+ * seconds, since the epoch (Jan 1 00:00 1970 GMT).
+ */
+class Stat {
+ /**
+ * The FID sent the T-stat message.
+ */
+ val fid: UInt
+
+ /**
+ * The QID of the file.
+ */
+ val qid: QID
+
+ /**
+ * Permissions and flags.
+ */
+ val mode: FilePermissions
+
+ /**
+ * Last acces time.
+ */
+ val atime: UInt
+
+ /**
+ * Last modification time.
+ */
+ val mtime: UInt
+
+ /**
+ * The length of the file in bytes.
+ */
+ val length: ULong
+
+ /**
+ * The file name, which is `/` if the file is the root directory.
+ */
+ val name: String
+
+ /**
+ * The owner's name.
+ */
+ val uid: String
+
+ /**
+ * The group's name.
+ */
+ val gid: String
+
+ /**
+ * The name of the user who last modified the file.
+ */
+ val muid: String
+
+ /**
+ * Make an instance of [Stat] from each of its fields.
+ *
+ * @param fid The FID sent the T-stat message.
+ * @param qid The QID of the file.
+ * @param mode Permissions and flags.
+ * @param atime Last acces time.
+ * @param mtime Last modification time.
+ * @param length The length of the file in bytes.
+ * @param name The file name, which is `/` if the file is the root directory.
+ * @param uid The owner's name.
+ * @param gid The group's name.
+ * @param muid The name of the user who last modified the file.
+ */
+ constructor(fid: UInt, qid: QID, mode: FilePermissions, atime: UInt, mtime: UInt, length: ULong, name: String, uid: String, gid: String, muid: String) {
+ this.fid = fid
+ this.qid = qid
+ this.mode = mode
+ this.atime = atime
+ this.mtime = mtime
+ this.length = length
+ this.name = name
+ this.uid = uid
+ this.gid = gid
+ this.muid = muid
+ }
+
+ /**
+ * Make an instance of [Stat] from raw data.
+ *
+ * @param fid The FID of the file associated with the stat instance.
+ * @param raw The raw stat data.
+ */
+ constructor(fid: UInt, raw: List<UByte>) {
+ var offset = 0
+ val qid = QID(raw.slice(0..<13))
+ offset += 13
+ val mode = FilePermissions(raw.slice(offset+0..<offset+4))
+ offset += 4
+
+ val intFielSizes = listOf(4, 4, 8)
+ val intFields: MutableList<BigInteger> = mutableListOf()
+ for (size in intFielSizes) {
+ intFields.add(InMessage.convInteger(raw, offset, size))
+ offset += size
+ }
+ val atime = intFields[0].toInt().toUInt()
+ val mtime = intFields[1].toInt().toUInt()
+ val length = intFields[2].toLong().toULong()
+
+ val strAmount = 4
+ val strFields: MutableList<String> = mutableListOf()
+ for (i in 0..strAmount) {
+ val str = InMessage.convString(raw, offset)
+ strFields.add(str)
+ offset += str.length
+ }
+ val name = strFields[0]
+ val uid = strFields[1]
+ val gid = strFields[2]
+ val muid = strFields[3]
+
+ this.fid = fid
+ this.qid = qid
+ this.mode = mode
+ this.atime = atime
+ this.mtime = mtime
+ this.length = length
+ this.name = name
+ this.uid = uid
+ this.gid = gid
+ this.muid = muid
+ }
+
+ /**
+ * Turn a [Stat] instance into raw data. This leaves out the [fid] field.
+ */
+ fun toRaw(): List<UByte> {
+ var bytes: List<UByte> = emptyList()
+ bytes += this.qid.toRaw()
+ bytes += OutMessage.convIntegerToBytes(BigInteger(this.mode.toPermissionInt().toString()), 4u)
+ bytes += OutMessage.convIntegerToBytes(BigInteger(this.atime.toString()), 4u)
+ bytes += OutMessage.convIntegerToBytes(BigInteger(this.mtime.toString()), 4u)
+ bytes += OutMessage.convIntegerToBytes(BigInteger(this.length.toString()), 8u)
+ bytes += OutMessage.convStringToBytes(this.name)
+ bytes += OutMessage.convStringToBytes(this.uid)
+ bytes += OutMessage.convStringToBytes(this.gid)
+ bytes += OutMessage.convStringToBytes(this.muid)
+ return bytes
+ }
+} \ No newline at end of file
diff --git a/lib/src/main/kotlin/TagGenerator.kt b/lib/src/main/kotlin/TagGenerator.kt
new file mode 100644
index 0000000..c386b17
--- /dev/null
+++ b/lib/src/main/kotlin/TagGenerator.kt
@@ -0,0 +1,63 @@
+import kotlin.random.Random
+
+/**
+ * Generate tags for 9P messages.
+ */
+class TagGenerator(val method: TagGenerationMethod, val initial: UShort) {
+
+ private var current: UShort = initial
+ private val rng: Random = Random(initial.toInt())
+ private val randomReturned: Set<UShort> = emptySet()
+
+ private val generationFunctions: Map<TagGenerationMethod, () -> UShort> = mapOf(
+ TagGenerationMethod.INCREMENTAL to this::generateIncremental,
+ TagGenerationMethod.RANDOM to this::generateRandom,
+ TagGenerationMethod.RANDOM_CHECKED to this::generateRandomChecked
+ )
+
+ /**
+ * How are tags generated?
+ */
+ enum class TagGenerationMethod {
+ /**
+ * Return the initial value on the first generation. Increment the value on each generation.
+ */
+ INCREMENTAL,
+
+ /**
+ * Use the initial value as a seed and generate random values from it.
+ */
+ RANDOM,
+
+ /**
+ * Same as [RANDOM], but checks are added to avoid generating the same value twice.
+ */
+ RANDOM_CHECKED,
+ }
+
+ /**
+ * Generate a new tag.
+ */
+ fun generate(): UShort {
+ return this.generationFunctions.getValue(method).invoke()
+ }
+
+ private fun generateIncremental(): UShort {
+ val tmp = this.current
+ this.current++
+ return tmp
+ }
+
+ private fun generateRandom(): UShort {
+ return this.rng.nextBits(16).toUShort()
+ }
+
+ private fun generateRandomChecked(): UShort {
+ var v: UShort
+ do {
+ v = generateRandom()
+ } while (v in randomReturned)
+ randomReturned.plus(v)
+ return v
+ }
+} \ No newline at end of file
diff --git a/lib/src/main/kotlin/Utils.kt b/lib/src/main/kotlin/Utils.kt
new file mode 100644
index 0000000..45185de
--- /dev/null
+++ b/lib/src/main/kotlin/Utils.kt
@@ -0,0 +1,38 @@
+const val DEFAULT_9P_PORT: UShort = 564u
+const val DEFAULT_9P_TLS_PORT: UShort = 17020u
+
+/**
+ * The [nineAddressToValues] function translates an address from one of the dial(2) formats into the respective data.
+ * For the sake of my sanity, the following list highlights differences from the typical dial(2) formats:
+ * 1. The "network" part is ignored.
+ * 2. If nothing is specified in the place of "service", the default unencrypted 9P port is used: 564.
+ * 3. "Service" can only contain port numbers.
+ * All the rest is the same.
+ *
+ * @return A Pair (A, P) containing the host address (A) and the port (P).
+ *
+ * @throws IllegalArgumentException If the address is improperly formatted or has invalid values.
+ * @throws NumberFormatException If the port's value can't be parsed into an [UShort].
+ */
+fun nineAddressToValues(nineAddress: String): Pair<String, UShort> {
+ val parts = nineAddress.split('!', limit = 3)
+ val address: String
+ val port: UShort
+ when (parts.size) {
+ 1 -> {
+ address = parts[0]
+ port = DEFAULT_9P_PORT
+ }
+ 2 -> {
+ address = parts[1]
+ port = DEFAULT_9P_PORT
+ }
+ 3 -> {
+ address = parts[1]
+ port = parts[2].toUShort()
+ }
+ else -> throw IllegalArgumentException()
+ }
+
+ return Pair(address, port)
+} \ No newline at end of file
diff --git a/lib/src/main/kotlin/except/FailedAuthenticationException.kt b/lib/src/main/kotlin/except/FailedAuthenticationException.kt
new file mode 100644
index 0000000..94935c3
--- /dev/null
+++ b/lib/src/main/kotlin/except/FailedAuthenticationException.kt
@@ -0,0 +1,8 @@
+package except
+
+/**
+ * The authentication with the remote part failed.
+ *
+ * @param reason A human-readable reason for which the authentication failed.
+ */
+class FailedAuthenticationException(val reason: String) : Exception("Authentication with remote host failed: $reason") \ No newline at end of file
diff --git a/lib/src/main/kotlin/except/InvalidMessageException.kt b/lib/src/main/kotlin/except/InvalidMessageException.kt
new file mode 100644
index 0000000..02a3cc4
--- /dev/null
+++ b/lib/src/main/kotlin/except/InvalidMessageException.kt
@@ -0,0 +1,8 @@
+package except
+
+/**
+ * The packet that is currently being read is not valid.
+ *
+ * @param reason The reason for which the packet is invalid.
+ */
+class InvalidMessageException(val reason: String) : Exception("Invalid packet: $reason") \ No newline at end of file
diff --git a/lib/src/main/kotlin/except/MsizeValueTooBigException.kt b/lib/src/main/kotlin/except/MsizeValueTooBigException.kt
new file mode 100644
index 0000000..f1f6da1
--- /dev/null
+++ b/lib/src/main/kotlin/except/MsizeValueTooBigException.kt
@@ -0,0 +1,10 @@
+package except
+
+/**
+ * This exception is thrown when the `msize` value sent by the server is bigger than that sent by the client during the
+ * version transaction.
+ *
+ * @param maxClientValue The value requested by the client.
+ * @param receivedValue The value sent by the server.
+ */
+class MsizeValueTooBigException(val maxClientValue: UInt, val receivedValue: UInt) : Exception("Msize value too big: $receivedValue > $maxClientValue") \ No newline at end of file
diff --git a/lib/src/main/kotlin/except/RErrorException.kt b/lib/src/main/kotlin/except/RErrorException.kt
new file mode 100644
index 0000000..c15cbc2
--- /dev/null
+++ b/lib/src/main/kotlin/except/RErrorException.kt
@@ -0,0 +1,8 @@
+package except
+
+/**
+ * This exception represents an error sent by the remote server as an R-error message.
+ *
+ * @param message The message sent by the server.
+ */
+class RErrorException(val rErrorMessage: String?) : Exception("R-error message received: $rErrorMessage") \ No newline at end of file
diff --git a/lib/src/main/kotlin/except/UnaccessibleFileException.kt b/lib/src/main/kotlin/except/UnaccessibleFileException.kt
new file mode 100644
index 0000000..07d5d13
--- /dev/null
+++ b/lib/src/main/kotlin/except/UnaccessibleFileException.kt
@@ -0,0 +1,11 @@
+package except
+
+/**
+ * This exception is thrown when the file that the client is trying to open (or walk through, in case of a directory)
+ * cannot be accessed.
+ *
+ * @param path The path, as a list of path elements, that the client tried to access, up to and including the first
+ * element that cannot be accessed (e.g. if the path the user wants to access is `["usr", "foo", "bar", "zib"]` but
+ * `bar` does not exist, then [path] must be `["usr", "foo", "bar"]`).
+ */
+class UnaccessibleFileException(val path: List<String>) : Exception("Could not walk to file ${path.joinToString(separator = "/")}.") \ No newline at end of file
diff --git a/lib/src/main/kotlin/except/UnknownVersionException.kt b/lib/src/main/kotlin/except/UnknownVersionException.kt
new file mode 100644
index 0000000..5023bc5
--- /dev/null
+++ b/lib/src/main/kotlin/except/UnknownVersionException.kt
@@ -0,0 +1,9 @@
+package except
+
+/**
+ * This exception is thrown when the remote server sent either an "unknown" version back during the version negotiation
+ * procedure or a version unknown to this client implementation.
+ *
+ * @param version The version sent by the server.
+ */
+class UnknownVersionException(val version: String) : Exception("Unknown version: $version") \ No newline at end of file
diff --git a/lib/src/main/kotlin/except/UnresolvableHostException.kt b/lib/src/main/kotlin/except/UnresolvableHostException.kt
new file mode 100644
index 0000000..19dd3b6
--- /dev/null
+++ b/lib/src/main/kotlin/except/UnresolvableHostException.kt
@@ -0,0 +1,8 @@
+package except
+
+/**
+ * The specified domain, which identifies the host's address, could not be resolved.
+ *
+ * @param address The unresolvable address.
+ */
+class UnresolvableHostException(val address: String) : Exception("Hostname $address unresolvable.") \ No newline at end of file
diff --git a/lib/src/main/kotlin/net/InMessage.kt b/lib/src/main/kotlin/net/InMessage.kt
new file mode 100644
index 0000000..771c670
--- /dev/null
+++ b/lib/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/lib/src/main/kotlin/net/OutMessage.kt b/lib/src/main/kotlin/net/OutMessage.kt
new file mode 100644
index 0000000..c9ae879
--- /dev/null
+++ b/lib/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/lib/src/main/kotlin/net/TransportLayer.kt b/lib/src/main/kotlin/net/TransportLayer.kt
new file mode 100644
index 0000000..90dcd11
--- /dev/null
+++ b/lib/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/lib/src/main/kotlin/net/TransportLayerJavaNet.kt b/lib/src/main/kotlin/net/TransportLayerJavaNet.kt
new file mode 100644
index 0000000..3d2867a
--- /dev/null
+++ b/lib/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