Vous ne pouvez pas sélectionner plus de 25 sujets Les noms de sujets doivent commencer par une lettre ou un nombre, peuvent contenir des tirets ('-') et peuvent comporter jusqu'à 35 caractères.

MessageParser.kt 5.5KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174
  1. package com.dmdirc.ktirc.io
  2. import com.dmdirc.ktirc.model.IrcMessage
  3. import com.dmdirc.ktirc.model.messageTags
  4. import com.dmdirc.ktirc.util.logger
  5. /**
  6. * Parses a message received from an IRC server.
  7. *
  8. * IRC messages consist of:
  9. *
  10. * - Optionally, IRCv3 message tags. Identified with an '@' character
  11. * - Optionally, a prefix, identified with an ':' character
  12. * - A command in the form of a consecutive sequence of letters or exactly three numbers
  13. * - Some number of space-separated parameters
  14. * - Optionally, a final 'trailing' parameter prefixed with a ':' character
  15. *
  16. * For example:
  17. *
  18. * @aaa=bbb;ccc;example.com/ddd=eee :nick!ident@host.com PRIVMSG #someChannel :This is a test message
  19. * ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ ^^^^^^^^^^^^^^^^^^^^ ^^^^^^^ ^^^^^^^^^^^^ ^^^^^^^^^^^^^^^^^^^^^^^
  20. * IRCv3 tags Prefix Cmd Param #1 Trailing parameter
  21. *
  22. * or:
  23. *
  24. * PING 12345678
  25. * ^^^^ ^^^^^^^^
  26. * Cmd Param #1
  27. */
  28. class MessageParser {
  29. companion object {
  30. private const val AT = '@'.toByte()
  31. private const val COLON = ':'.toByte()
  32. }
  33. private val log by logger()
  34. fun parse(message: ByteArray) = CursorByteArray(message).run {
  35. IrcMessage(takeTags(), takePrefix(), String(takeWord()), takeParams())
  36. }
  37. /**
  38. * Attempts to read IRCv3 tags from the message.
  39. */
  40. private fun CursorByteArray.takeTags() = takeOptionalPrefixedSection(AT).toTagMap()
  41. /**
  42. * Attempts to read a prefix from the message.
  43. */
  44. private fun CursorByteArray.takePrefix() = takeOptionalPrefixedSection(COLON)
  45. /**
  46. * Read a single parameter from the message. If the parameter is a trailing one, the entire message will be
  47. * consumed.
  48. */
  49. private fun CursorByteArray.takeParam() = when (peek()) {
  50. COLON -> takeRemaining(skip = 1)
  51. else -> takeWord()
  52. }
  53. /**
  54. * Reads all remaining parameters from the message.
  55. */
  56. private fun CursorByteArray.takeParams() = sequence {
  57. while (!exhausted()) {
  58. yield(takeParam())
  59. }
  60. }.toList()
  61. /**
  62. * If the next word starts with the given prefix, takes and returns it, otherwise returns null.
  63. */
  64. private fun CursorByteArray.takeOptionalPrefixedSection(prefix: Byte) = when {
  65. exhausted() -> null
  66. peek() == prefix -> takeWord(skip = 1)
  67. else -> null
  68. }
  69. /**
  70. * Parses the bytes as a list of message tags. Unknown tags are discarded.
  71. */
  72. private fun ByteArray?.toTagMap() = sequence {
  73. forEachPart(';') { tag ->
  74. val index = tag.indexOf('=')
  75. val name = if (index == -1) tag else tag.substring(0 until index)
  76. messageTags[name]?.let {
  77. yield(Pair(it, if (index == -1) "" else tag.substring(index + 1).unescapeTagValue()))
  78. } ?: log.severe { "Unknown message tag: $name"}
  79. }
  80. }.toMap()
  81. /**
  82. * Resolves any backslash escaped characters in a tag value.
  83. */
  84. private fun String.unescapeTagValue() = String(sequence {
  85. var escaped = false
  86. forEach { char ->
  87. when {
  88. escaped -> {
  89. char.unescaped()?.let { yield(it) }
  90. escaped = false
  91. }
  92. char == '\\' -> escaped = true
  93. else -> yield(char)
  94. }
  95. }
  96. }.toList().toCharArray())
  97. /**
  98. * Maps an escaped character in a tag value back to its real form. Returns null if the sequence is invalid.
  99. */
  100. private fun Char.unescaped() = when (this) {
  101. ':' -> ';'
  102. 'n' -> '\n'
  103. 'r' -> '\r'
  104. 's' -> ' '
  105. '\\' -> '\\'
  106. else -> null
  107. }
  108. private inline fun ByteArray?.forEachPart(delimiter: Char, action: (String) -> Unit) = this?.let {
  109. String(it).split(delimiter).forEach(action)
  110. }
  111. }
  112. /**
  113. * A ByteArray with a 'cursor' that tracks the current read position.
  114. */
  115. internal class CursorByteArray(private val data: ByteArray, var cursor: Int = 0) {
  116. companion object {
  117. private const val SPACE = ' '.toByte()
  118. }
  119. /**
  120. * Returns whether or not the cursor has reached the end of the array.
  121. */
  122. fun exhausted() = cursor >= data.size
  123. /**
  124. * Returns the next byte in the array without advancing the cursor.
  125. *
  126. * @throws ArrayIndexOutOfBoundsException If the array is [exhausted].
  127. */
  128. @Throws(ArrayIndexOutOfBoundsException::class)
  129. fun peek() = data[cursor]
  130. /**
  131. * Returns the next "word" in the byte array - that is, all non-space characters up until the next space.
  132. *
  133. * After calling this method, the cursor will be advanced to the start of the next word (i.e., it will skip over
  134. * any number of space characters).
  135. *
  136. * @param skip Number of bytes to omit from the start of the word
  137. */
  138. fun takeWord(skip: Int = 0) = data.sliceArray(cursor + skip until seekTo { it == SPACE }).apply { seekTo { it != SPACE } }
  139. /**
  140. * Takes all remaining bytes from the cursor until the end of the array.
  141. *
  142. * @param skip Number of bytes to omit from the start of the remainder
  143. */
  144. fun takeRemaining(skip: Int = 0) = data.sliceArray(cursor + skip until data.size).apply { cursor = data.size }
  145. private fun seekTo(matcher: (Byte) -> Boolean): Int {
  146. while (!exhausted() && !matcher(peek())) {
  147. cursor++
  148. }
  149. return cursor
  150. }
  151. }