You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

MessageParser.kt 3.9KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121
  1. package com.dmdirc.ktirc.io
  2. /**
  3. * Parses a message received from an IRC server.
  4. *
  5. * IRC messages consist of:
  6. *
  7. * - Optionally, IRCv3 message tags. Identified with an '@' character
  8. * - Optionally, a prefix, identified with an ':' character
  9. * - A command in the form of a consecutive sequence of letters or exactly three numbers
  10. * - Some number of space-separated parameters
  11. * - Optionally, a final 'trailing' parameter prefixed with a ':' character
  12. *
  13. * For example:
  14. *
  15. * @aaa=bbb;ccc;example.com/ddd=eee :nick!ident@host.com PRIVMSG #someChannel :This is a test message
  16. * ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ ^^^^^^^^^^^^^^^^^^^^ ^^^^^^^ ^^^^^^^^^^^^ ^^^^^^^^^^^^^^^^^^^^^^^
  17. * IRCv3 tags Prefix Cmd Param #1 Trailing parameter
  18. *
  19. * or:
  20. *
  21. * PING 12345678
  22. * ^^^^ ^^^^^^^^
  23. * Cmd Param #1
  24. */
  25. class MessageParser {
  26. companion object {
  27. private const val AT = '@'.toByte()
  28. private const val COLON = ':'.toByte()
  29. }
  30. fun parse(message: ByteArray) = CursorByteArray(message).run {
  31. IrcMessage(takeTags(), takePrefix(), String(takeWord()), takeParams().toList())
  32. }
  33. /**
  34. * Attempts to read IRCv3 tags from the message.
  35. */
  36. private fun CursorByteArray.takeTags() = takeOptionalPrefixedSection(AT)
  37. /**
  38. * Attempts to read a prefix from the message.
  39. */
  40. private fun CursorByteArray.takePrefix() = takeOptionalPrefixedSection(COLON)
  41. /**
  42. * Read a single parameter from the message. If the parameter is a trailing one, the entire message will be
  43. * consumed.
  44. */
  45. private fun CursorByteArray.takeParam() = when (peek()) {
  46. COLON -> takeRemaining(skip = 1)
  47. else -> takeWord()
  48. }
  49. /**
  50. * Reads all remaining parameters from the message.
  51. */
  52. private fun CursorByteArray.takeParams() = sequence {
  53. while (!exhausted()) {
  54. yield(takeParam())
  55. }
  56. }
  57. private fun CursorByteArray.takeOptionalPrefixedSection(prefix: Byte) = when {
  58. exhausted() -> null
  59. peek() == prefix -> takeWord(skip = 1)
  60. else -> null
  61. }
  62. }
  63. class IrcMessage(val tags: ByteArray?, val prefix: ByteArray?, val command: String, val params: List<ByteArray>)
  64. /**
  65. * A ByteArray with a 'cursor' that tracks the current read position.
  66. */
  67. internal class CursorByteArray(private val data: ByteArray, var cursor: Int = 0) {
  68. companion object {
  69. private const val SPACE = ' '.toByte()
  70. }
  71. /**
  72. * Returns whether or not the cursor has reached the end of the array.
  73. */
  74. fun exhausted() = cursor >= data.size
  75. /**
  76. * Returns the next byte in the array without advancing the cursor.
  77. *
  78. * @throws ArrayIndexOutOfBoundsException If the array is [exhausted].
  79. */
  80. @Throws(ArrayIndexOutOfBoundsException::class)
  81. fun peek() = data[cursor]
  82. /**
  83. * Returns the next "word" in the byte array - that is, all non-space characters up until the next space.
  84. *
  85. * After calling this method, the cursor will be advanced to the start of the next word (i.e., it will skip over
  86. * any number of space characters).
  87. *
  88. * @param skip Number of bytes to omit from the start of the word
  89. */
  90. fun takeWord(skip: Int = 0) = data.sliceArray(cursor + skip until seekTo { it == SPACE }).apply { seekTo { it != SPACE } }
  91. /**
  92. * Takes all remaining bytes from the cursor until the end of the array.
  93. *
  94. * @param skip Number of bytes to omit from the start of the remainder
  95. */
  96. fun takeRemaining(skip: Int = 0) = data.sliceArray(cursor + skip until data.size).apply { cursor = data.size }
  97. private fun seekTo(matcher: (Byte) -> Boolean): Int {
  98. while (!exhausted() && !matcher(peek())) {
  99. cursor++
  100. }
  101. return cursor
  102. }
  103. }