aboutsummaryrefslogtreecommitdiff
path: root/tags.go
blob: 31f115432582303c53985d87e31c1b3dcb26ffab (plain) (blame)
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
// 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) {
			// "Tag values MUST be encoded as UTF8."
			if !utf8.ValidString(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
}