nandi/frqpublic Fork 0
3975b4bc9fe9fd59fc0e81ca2bb7ffdba986934a
Commits
Clone
git clone https://git.rickub.com/nandi/frq.git
git clone ssh://git@rickub.com/nandi/frq.git

Host key fingerprint (ed25519): SHA256:iycHnxEyq0Q7uyVpB7JlznP0G7JrTPXLYRcAU5CSLhc — verify it before your first connect.

One frontend where there were three, and a core that is not Clojure 438b247 · on 3975b4bc9fe9fd59fc0e81ca2bb7ffdba986934a · nandi · 21h ago
tircparse.nim · 137 lines · 4.8 KBNim Blame HistoryRaw
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
## The IRC wire format, tested against the cases that cost something.
##
## These are not a transcription of the Clojure's tests, because it had none —
## which is half the argument for the move. Every case here is either a rule
## IRCv3 states or a bug the comments in `common/frq/irc/parse.cljc` record
## having been paid for once already.

import std/unittest
import frq/ircparse

suite "parseLine":
  test "a bare line":
    let p = parseLine("PING :12345")
    check p.command == "PING"
    check p.params == @["12345"]
    check not p.hasPrefix
    check not p.hasTags

  test "a prefix is split off and the command upcased":
    let p = parseLine(":nick!user@host privmsg #chan :hello there")
    check p.prefix == "nick!user@host"
    check p.hasPrefix
    check p.command == "PRIVMSG"
    check p.params == @["#chan", "hello there"]

  test "the trailing parameter keeps its spaces and its colons":
    let p = parseLine(":a!b@c PRIVMSG #chan :look: a b  c")
    check p.params == @["#chan", "look: a b  c"]

  test "no trailing parameter at all":
    let p = parseLine(":a!b@c JOIN #chan")
    check p.params == @["#chan"]

  test "an empty trailing parameter is a parameter":
    # `:` with nothing after it is how a client sends an empty topic, and
    # dropping it turns a clear into a no-op.
    let p = parseLine(":a!b@c TOPIC #chan :")
    check p.params == @["#chan", ""]

  test "tags are taken off the front":
    let p = parseLine("@time=2026-01-01T00:00:00Z;account=alice :a!b@c PRIVMSG #chan :hi")
    check p.hasTags
    check p.tags == "time=2026-01-01T00:00:00Z;account=alice"
    check p.account == "alice"
    check p.command == "PRIVMSG"
    check p.params == @["#chan", "hi"]

  test "a line with tags and no account":
    let p = parseLine("@time=x :a!b@c PRIVMSG #chan :hi")
    check p.hasTags
    check not p.hasAccount

  test "trailing whitespace is trimmed but leading structure is not":
    let p = parseLine("PING :12345\r\n")
    check p.raw == "PING :12345"
    check p.params == @["12345"]

  test "raw is what arrived, not what we made of it":
    let p = parseLine("@a=1 :n!u@h PRIVMSG #c :x  ")
    check p.raw == "@a=1 :n!u@h PRIVMSG #c :x"

  test "runs of spaces in the head do not become empty parameters":
    let p = parseLine(":a!b@c PRIVMSG   #chan   :hi")
    check p.params == @["#chan", "hi"]

  test "a line that is only tags does not throw":
    # The Clojure indexes with `subs` on a nil `index-of` here and dies. It
    # cannot arrive from a conforming server, but a parser reading a socket
    # answers malformed input with a value rather than an exception.
    let p = parseLine("@only=tags")
    check p.hasTags
    check p.tags == "only=tags"
    check p.command == ""

  test "an empty line":
    let p = parseLine("")
    check p.command == ""
    check p.params.len == 0

suite "unescapeTag":
  test "the five escapes":
    check unescapeTag("a\\:b") == "a;b"
    check unescapeTag("a\\sb") == "a b"
    check unescapeTag("a\\\\b") == "a\\b"
    check unescapeTag("a\\rb") == "a\rb"
    check unescapeTag("a\\nb") == "a\nb"

  test "an unknown escape is the character itself":
    check unescapeTag("a\\qb") == "aqb"

  test "a trailing backslash is kept rather than eating the terminator":
    check unescapeTag("ab\\") == "ab\\"

  test "a reaction tally survives the trip":
    # The bug this function exists for: three tallies, not one.
    check unescapeTag("x\\:alice\\:bob") == "x;alice;bob"

suite "escapeTagValue":
  test "round-trips everything unescapeTag undoes":
    for s in ["plain", "a;b", "a b", "a\\b", "a\r\nb", "", "😀", "a;;b"]:
      check unescapeTag(escapeTagValue(s)) == s

  test "a backslash is not double-escaped":
    check escapeTagValue("a\\b") == "a\\\\b"

suite "tagValue":
  test "a present value":
    check tagValue("a=1;b=2", "b") == ("2", true)

  test "an absent tag":
    check tagValue("a=1", "b") == ("", false)

  test "an empty value and a bare key are both absent":
    # IRCv3 says `key` and `key=` mean the same thing, and a caller asking
    # `tagValue` is asking whether a fact is there. `+reply=` on a line
    # answering nothing used to put a reply chip above it.
    check tagValue("a=;b=2", "a") == ("", false)
    check tagValue("a;b=2", "a") == ("", false)

  test "the value is unescaped":
    check tagValue("t=a\\sb", "t") == ("a b", true)

  test "a key that is a prefix of another does not match it":
    check tagValue("account-x=1;account=2", "account") == ("2", true)

  test "a value containing = keeps the rest of it":
    check tagValue("t=a=b", "t") == ("a=b", true)

suite "nickOf":
  test "a full prefix":
    check nickOf("nick!user@host") == "nick"

  test "a prefix that is only a nick":
    check nickOf("nick") == "nick"

  test "a server prefix has no bang and comes back whole":
    check nickOf("irc.freeq.at") == "irc.freeq.at"