aboutsummaryrefslogtreecommitdiff
path: root/tags.go
diff options
context:
space:
mode:
authorRunxi Yu <me@runxiyu.org>2024-12-07 21:13:28 +0800
committerRunxi Yu <me@runxiyu.org>2024-12-07 21:13:28 +0800
commit8c18330aa3976aa981bbe4f135960a92b5923fb6 (patch)
tree6b1548fce387c3d86f110732f971d85f64990c9a /tags.go
downloadmeseircd-8c18330aa3976aa981bbe4f135960a92b5923fb6.tar.gz
meseircd-8c18330aa3976aa981bbe4f135960a92b5923fb6.tar.zst
meseircd-8c18330aa3976aa981bbe4f135960a92b5923fb6.zip
Initial commit
Diffstat (limited to 'tags.go')
-rw-r--r--tags.go119
1 files changed, 119 insertions, 0 deletions
diff --git a/tags.go b/tags.go
new file mode 100644
index 0000000..43a2833
--- /dev/null
+++ b/tags.go
@@ -0,0 +1,119 @@
+// Almost everything in this file is adapted from Ergo IRCd
+// This is probably considered a derived work for copyright purposes
+//
+// SPDX-License-Identifier: MIT
+
+package main
+
+import (
+ "strings"
+ "unicode/utf8"
+)
+
+func parseTags(tagsString string) (tags map[string]string, err error) {
+ tags = make(map[string]string)
+ for 0 < len(tagsString) {
+ tagEnd := strings.IndexByte(tagsString, ';')
+ endPos := tagEnd
+ nextPos := tagEnd + 1
+ if tagEnd == -1 {
+ endPos = len(tagsString)
+ nextPos = len(tagsString)
+ }
+ tagPair := tagsString[:endPos]
+ equalsIndex := strings.IndexByte(tagPair, '=')
+ var tagName, tagValue string
+ if equalsIndex == -1 {
+ // Tag with no value
+ tagName = tagPair
+ } else {
+ tagName, tagValue = tagPair[:equalsIndex], tagPair[equalsIndex+1:]
+ }
+ // "Implementations [...] MUST NOT perform any validation that would
+ // reject the message if an invalid tag key name is used."
+ if validateTagName(tagName) {
+ if !validateTagValue(tagValue) {
+ err = ErrInvalidTagContent
+ return
+ }
+ tags[tagName] = UnescapeTagValue(tagValue)
+ }
+ tagsString = tagsString[nextPos:]
+ }
+ return
+}
+
+func UnescapeTagValue(inString string) string {
+ // buf.Len() == 0 is the fastpath where we have not needed to unescape any chars
+ var buf strings.Builder
+ remainder := inString
+ for {
+ backslashPos := strings.IndexByte(remainder, '\\')
+
+ if backslashPos == -1 {
+ if buf.Len() == 0 {
+ return inString
+ } else {
+ buf.WriteString(remainder)
+ break
+ }
+ } else if backslashPos == len(remainder)-1 {
+ // trailing backslash, which we strip
+ if buf.Len() == 0 {
+ return inString[:len(inString)-1]
+ } else {
+ buf.WriteString(remainder[:len(remainder)-1])
+ break
+ }
+ }
+
+ // Non-trailing backslash detected; we're now on the slowpath
+ // where we modify the string
+ if buf.Len() < len(inString) {
+ buf.Grow(len(inString))
+ }
+ buf.WriteString(remainder[:backslashPos])
+ buf.WriteByte(escapedCharLookupTable[remainder[backslashPos+1]])
+ remainder = remainder[backslashPos+2:]
+ }
+
+ return buf.String()
+}
+
+var escapedCharLookupTable [256]byte
+
+func init() {
+ for i := 0; i < 256; i += 1 {
+ escapedCharLookupTable[i] = byte(i)
+ }
+ escapedCharLookupTable[':'] = ';'
+ escapedCharLookupTable['s'] = ' '
+ escapedCharLookupTable['r'] = '\r'
+ escapedCharLookupTable['n'] = '\n'
+}
+
+// https://ircv3.net/specs/extensions/message-tags.html#rules-for-naming-message-tags
+func validateTagName(name string) bool {
+ if len(name) == 0 {
+ return false
+ }
+ if name[0] == '+' {
+ name = name[1:]
+ }
+ if len(name) == 0 {
+ return false
+ }
+ // Let's err on the side of leniency here; allow -./ (45-47) in any position
+ for i := 0; i < len(name); i++ {
+ c := name[i]
+ if !(('-' <= c && c <= '/') || ('0' <= c && c <= '9') || ('A' <= c && c <= 'Z') || ('a' <= c && c <= 'z')) {
+ return false
+ }
+ }
+ return true
+}
+
+// "Tag values MUST be encoded as UTF8."
+func validateTagValue(value string) bool {
+ return utf8.ValidString(value)
+}