Merge pull request #11 from slingamn/messagetags.6
ircmsg: support ratified message-tags spec
This commit is contained in:
commit
ca74bf6a17
10
.check-gofmt.sh
Executable file
10
.check-gofmt.sh
Executable file
@ -0,0 +1,10 @@
|
||||
#!/bin/bash
|
||||
|
||||
# exclude vendor/
|
||||
SOURCES="."
|
||||
|
||||
if [ -n "$(gofmt -s -l $SOURCES)" ]; then
|
||||
echo "Go code is not formatted correctly with \`gofmt -s\`:"
|
||||
gofmt -s -d $SOURCES
|
||||
exit 1
|
||||
fi
|
1
Makefile
1
Makefile
@ -4,3 +4,4 @@ test:
|
||||
cd ircmatch && go test . && go vet .
|
||||
cd ircmsg && go test . && go vet .
|
||||
cd ircutils && go test . && go vet .
|
||||
./.check_gofmt.sh
|
||||
|
@ -88,7 +88,7 @@ func TestTLSConnection(t *testing.T) {
|
||||
KeyUsage: x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature | x509.KeyUsageCertSign,
|
||||
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth},
|
||||
BasicConstraintsValid: true,
|
||||
IsCA: true,
|
||||
IsCA: true,
|
||||
}
|
||||
|
||||
template.IPAddresses = append(template.IPAddresses, net.ParseIP("127.0.0.1"))
|
||||
|
@ -27,7 +27,7 @@ var unescapetests = []testcase{
|
||||
{"test$c", "test\x03"},
|
||||
}
|
||||
|
||||
var stripTests = []testcase {
|
||||
var stripTests = []testcase{
|
||||
{"te\x02st", "test"},
|
||||
{"te\x033st", "test"},
|
||||
{"te\x034,3st", "test"},
|
||||
@ -94,7 +94,7 @@ func TestStrip(t *testing.T) {
|
||||
"For", pair.escaped,
|
||||
"expected", pair.unescaped,
|
||||
"got", val,
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,4 +1,6 @@
|
||||
// written by Daniel Oaks <daniel@danieloaks.net>
|
||||
// Copyright (c) 2016-2019 Daniel Oaks <daniel@danieloaks.net>
|
||||
// Copyright (c) 2018-2019 Shivaram Lingamneni <slingamn@cs.stanford.edu>
|
||||
|
||||
// released under the ISC license
|
||||
|
||||
package ircmsg
|
||||
@ -9,239 +11,370 @@ import (
|
||||
"strings"
|
||||
)
|
||||
|
||||
const (
|
||||
// "The size limit for message tags is 8191 bytes, including the leading
|
||||
// '@' (0x40) and trailing space ' ' (0x20) characters."
|
||||
MaxlenTags = 8191
|
||||
|
||||
// MaxlenTags - ('@' + ' ')
|
||||
MaxlenTagData = MaxlenTags - 2
|
||||
|
||||
// "Clients MUST NOT send messages with tag data exceeding 4094 bytes,
|
||||
// this includes tags with or without the client-only prefix."
|
||||
MaxlenClientTagData = 4094
|
||||
|
||||
// "Servers MUST NOT add tag data exceeding 4094 bytes to messages."
|
||||
MaxlenServerTagData = 4094
|
||||
|
||||
// '@' + MaxlenClientTagData + ' '
|
||||
// this is the analogue of MaxlenTags when the source of the message is a client
|
||||
MaxlenTagsFromClient = MaxlenClientTagData + 2
|
||||
)
|
||||
|
||||
var (
|
||||
// ErrorLineIsEmpty indicates that the given IRC line was empty.
|
||||
ErrorLineIsEmpty = errors.New("Line is empty")
|
||||
// ErrorTagsContainsBadChar indicates that the passed tag string contains a space or newline.
|
||||
ErrorTagsContainsBadChar = errors.New("Tag string contains bad character (such as a space or newline)")
|
||||
// ErrorLineContainsBadChar indicates that the line contained invalid characters
|
||||
ErrorLineContainsBadChar = errors.New("Line contains invalid characters")
|
||||
// ErrorLineTooLong indicates that the message exceeded the maximum tag length
|
||||
// (the name references 417 ERR_INPUTTOOLONG; we reserve the right to return it
|
||||
// for messages that exceed the non-tag length limit)
|
||||
ErrorLineTooLong = errors.New("Line could not be parsed because a specified length limit was exceeded")
|
||||
|
||||
ErrorCommandMissing = errors.New("IRC messages MUST have a command")
|
||||
ErrorBadParam = errors.New("Cannot have an empty param, a param with spaces, or a param that starts with ':' before the last parameter")
|
||||
)
|
||||
|
||||
// IrcMessage represents an IRC message, as defined by the RFCs and as
|
||||
// extended by the IRCv3 Message Tags specification with the introduction
|
||||
// of message tags.
|
||||
type IrcMessage struct {
|
||||
Tags map[string]TagValue
|
||||
Prefix string
|
||||
Command string
|
||||
Params []string
|
||||
// SourceLine represents the original line that constructed this message, when created from ParseLine.
|
||||
SourceLine string
|
||||
Prefix string
|
||||
Command string
|
||||
Params []string
|
||||
tags map[string]string
|
||||
clientOnlyTags map[string]string
|
||||
}
|
||||
|
||||
// ParseLine creates and returns an IrcMessage from the given IRC line.
|
||||
//
|
||||
// Quirks:
|
||||
//
|
||||
// The RFCs say that last parameters with no characters MUST be a trailing.
|
||||
// IE, they need to be prefixed with ":". We disagree with that and handle
|
||||
// incoming last empty parameters whether they are trailing or ordinary
|
||||
// parameters. However, we do follow that rule when emitting lines.
|
||||
func ParseLine(line string) (IrcMessage, error) {
|
||||
return parseLine(line, 0, 0, false)
|
||||
// GetTag returns whether a tag is present, and if so, what its value is.
|
||||
func (msg *IrcMessage) GetTag(tagName string) (present bool, value string) {
|
||||
if len(tagName) == 0 {
|
||||
return
|
||||
} else if tagName[0] == '+' {
|
||||
value, present = msg.clientOnlyTags[tagName]
|
||||
return
|
||||
} else {
|
||||
value, present = msg.tags[tagName]
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// ParseLineMaxLen creates and returns an IrcMessage from the given IRC line,
|
||||
// HasTag returns whether a tag is present.
|
||||
func (msg *IrcMessage) HasTag(tagName string) (present bool) {
|
||||
present, _ = msg.GetTag(tagName)
|
||||
return
|
||||
}
|
||||
|
||||
// SetTag sets a tag.
|
||||
func (msg *IrcMessage) SetTag(tagName, tagValue string) {
|
||||
if len(tagName) == 0 {
|
||||
return
|
||||
} else if tagName[0] == '+' {
|
||||
if msg.clientOnlyTags == nil {
|
||||
msg.clientOnlyTags = make(map[string]string)
|
||||
}
|
||||
msg.clientOnlyTags[tagName] = tagValue
|
||||
} else {
|
||||
if msg.tags == nil {
|
||||
msg.tags = make(map[string]string)
|
||||
}
|
||||
msg.tags[tagName] = tagValue
|
||||
}
|
||||
}
|
||||
|
||||
// DeleteTag deletes a tag.
|
||||
func (msg *IrcMessage) DeleteTag(tagName string) {
|
||||
if len(tagName) == 0 {
|
||||
return
|
||||
} else if tagName[0] == '+' {
|
||||
delete(msg.clientOnlyTags, tagName)
|
||||
} else {
|
||||
delete(msg.tags, tagName)
|
||||
}
|
||||
}
|
||||
|
||||
// UpdateTags is a convenience to set multiple tags at once.
|
||||
func (msg *IrcMessage) UpdateTags(tags map[string]string) {
|
||||
for name, value := range tags {
|
||||
msg.SetTag(name, value)
|
||||
}
|
||||
}
|
||||
|
||||
// AllTags returns all tags as a single map.
|
||||
func (msg *IrcMessage) AllTags() (result map[string]string) {
|
||||
result = make(map[string]string, len(msg.tags)+len(msg.clientOnlyTags))
|
||||
for name, value := range msg.tags {
|
||||
result[name] = value
|
||||
}
|
||||
for name, value := range msg.clientOnlyTags {
|
||||
result[name] = value
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// ClientOnlyTags returns the client-only tags (the tags with the + prefix).
|
||||
// The returned map may be internal storage of the IrcMessage object and
|
||||
// should not be modified.
|
||||
func (msg *IrcMessage) ClientOnlyTags() map[string]string {
|
||||
return msg.clientOnlyTags
|
||||
}
|
||||
|
||||
// ParseLine creates and returns a message from the given IRC line.
|
||||
func ParseLine(line string) (ircmsg IrcMessage, err error) {
|
||||
return parseLine(line, 0, 0)
|
||||
}
|
||||
|
||||
// ParseLineStrict creates and returns an IrcMessage from the given IRC line,
|
||||
// taking the maximum length into account and truncating the message as appropriate.
|
||||
//
|
||||
// Quirks:
|
||||
//
|
||||
// The RFCs say that last parameters with no characters MUST be a trailing.
|
||||
// IE, they need to be prefixed with ":". We disagree with that and handle
|
||||
// incoming last empty parameters whether they are trailing or ordinary
|
||||
// parameters. However, we do follow that rule when emitting lines.
|
||||
func ParseLineMaxLen(line string, maxlenTags, maxlenRest int) (IrcMessage, error) {
|
||||
return parseLine(line, maxlenTags, maxlenRest, true)
|
||||
// If fromClient is true, it enforces the client limit on tag data length (4094 bytes),
|
||||
// allowing the server to return ERR_INPUTTOOLONG as appropriate. If truncateLen is
|
||||
// nonzero, it is the length at which the non-tag portion of the message is truncated.
|
||||
func ParseLineStrict(line string, fromClient bool, truncateLen int) (ircmsg IrcMessage, err error) {
|
||||
maxTagDataLength := MaxlenTagData
|
||||
if fromClient {
|
||||
maxTagDataLength = MaxlenClientTagData
|
||||
}
|
||||
return parseLine(line, maxTagDataLength, truncateLen)
|
||||
}
|
||||
|
||||
// parseLine does the actual line parsing for the above user-facing functions.
|
||||
func parseLine(line string, maxlenTags, maxlenRest int, useMaxLen bool) (IrcMessage, error) {
|
||||
line = strings.Trim(line, "\r\n")
|
||||
var ircmsg IrcMessage
|
||||
// slice off any amount of '\r' or '\n' from the end of the string
|
||||
func trimFinalNewlines(str string) string {
|
||||
var i int
|
||||
for i = len(str) - 1; 0 <= i && (str[i] == '\r' || str[i] == '\n'); i -= 1 {
|
||||
}
|
||||
return str[:i+1]
|
||||
}
|
||||
|
||||
ircmsg.SourceLine = line
|
||||
func parseLine(line string, maxTagDataLength int, truncateLen int) (ircmsg IrcMessage, err error) {
|
||||
if strings.IndexByte(line, '\x00') != -1 {
|
||||
err = ErrorLineContainsBadChar
|
||||
return
|
||||
}
|
||||
|
||||
line = trimFinalNewlines(line)
|
||||
|
||||
if len(line) < 1 {
|
||||
return ircmsg, ErrorLineIsEmpty
|
||||
}
|
||||
|
||||
// tags
|
||||
ircmsg.Tags = make(map[string]TagValue)
|
||||
if line[0] == '@' {
|
||||
splitLine := strings.SplitN(line, " ", 2)
|
||||
if len(splitLine) < 2 {
|
||||
tagEnd := strings.IndexByte(line, ' ')
|
||||
if tagEnd == -1 {
|
||||
return ircmsg, ErrorLineIsEmpty
|
||||
}
|
||||
tags := splitLine[0][1:]
|
||||
line = strings.TrimLeft(splitLine[1], " ")
|
||||
|
||||
if len(line) < 1 {
|
||||
return ircmsg, ErrorLineIsEmpty
|
||||
tags := line[1:tagEnd]
|
||||
if 0 < maxTagDataLength && maxTagDataLength < len(tags) {
|
||||
return ircmsg, ErrorLineTooLong
|
||||
}
|
||||
|
||||
var err error
|
||||
ircmsg.Tags, err = parseTags(tags, maxlenTags, useMaxLen)
|
||||
err = ircmsg.parseTags(tags)
|
||||
if err != nil {
|
||||
return ircmsg, err
|
||||
return
|
||||
}
|
||||
// skip over the tags and the separating space
|
||||
line = line[tagEnd+1:]
|
||||
}
|
||||
|
||||
// truncate if desired
|
||||
if useMaxLen && len(line) > maxlenRest {
|
||||
line = line[:maxlenRest]
|
||||
if 0 < truncateLen && truncateLen < len(line) {
|
||||
line = line[:truncateLen]
|
||||
}
|
||||
|
||||
// prefix
|
||||
if line[0] == ':' {
|
||||
splitLine := strings.SplitN(line, " ", 2)
|
||||
if len(splitLine) < 2 {
|
||||
prefixEnd := strings.IndexByte(line, ' ')
|
||||
if prefixEnd == -1 {
|
||||
return ircmsg, ErrorLineIsEmpty
|
||||
}
|
||||
ircmsg.Prefix = splitLine[0][1:]
|
||||
line = strings.TrimLeft(splitLine[1], " ")
|
||||
}
|
||||
|
||||
if len(line) < 1 {
|
||||
return ircmsg, ErrorLineIsEmpty
|
||||
ircmsg.Prefix = line[1:prefixEnd]
|
||||
// skip over the prefix and the separating space
|
||||
line = line[prefixEnd+1:]
|
||||
}
|
||||
|
||||
// command
|
||||
splitLine := strings.SplitN(line, " ", 2)
|
||||
if len(splitLine[0]) == 0 {
|
||||
commandEnd := strings.IndexByte(line, ' ')
|
||||
paramStart := commandEnd + 1
|
||||
if commandEnd == -1 {
|
||||
commandEnd = len(line)
|
||||
paramStart = len(line)
|
||||
}
|
||||
// normalize command to uppercase:
|
||||
ircmsg.Command = strings.ToUpper(strings.TrimSpace(line[:commandEnd]))
|
||||
if len(ircmsg.Command) == 0 {
|
||||
return ircmsg, ErrorLineIsEmpty
|
||||
}
|
||||
ircmsg.Command = strings.ToUpper(splitLine[0])
|
||||
if len(splitLine) > 1 {
|
||||
line = strings.TrimLeft(splitLine[1], " ")
|
||||
line = line[paramStart:]
|
||||
|
||||
// parameters
|
||||
for {
|
||||
// handle trailing
|
||||
if len(line) > 0 && line[0] == ':' {
|
||||
ircmsg.Params = append(ircmsg.Params, line[1:])
|
||||
break
|
||||
}
|
||||
|
||||
// regular params
|
||||
splitLine := strings.SplitN(line, " ", 2)
|
||||
if len(splitLine[0]) > 0 {
|
||||
ircmsg.Params = append(ircmsg.Params, splitLine[0])
|
||||
}
|
||||
|
||||
if len(splitLine) > 1 {
|
||||
line = strings.TrimLeft(splitLine[1], " ")
|
||||
} else {
|
||||
break
|
||||
}
|
||||
for 0 < len(line) {
|
||||
// handle trailing
|
||||
if line[0] == ':' {
|
||||
ircmsg.Params = append(ircmsg.Params, line[1:])
|
||||
break
|
||||
}
|
||||
paramEnd := strings.IndexByte(line, ' ')
|
||||
if paramEnd == -1 {
|
||||
ircmsg.Params = append(ircmsg.Params, line)
|
||||
break
|
||||
} else if paramEnd == 0 {
|
||||
// only a trailing parameter can be empty
|
||||
return ircmsg, ErrorLineContainsBadChar
|
||||
}
|
||||
ircmsg.Params = append(ircmsg.Params, line[:paramEnd])
|
||||
line = line[paramEnd+1:]
|
||||
}
|
||||
|
||||
return ircmsg, nil
|
||||
}
|
||||
|
||||
// MakeMessage provides a simple way to create a new IrcMessage.
|
||||
func MakeMessage(tags *map[string]TagValue, prefix string, command string, params ...string) IrcMessage {
|
||||
var ircmsg IrcMessage
|
||||
|
||||
ircmsg.Tags = make(map[string]TagValue)
|
||||
if tags != nil {
|
||||
for tag, value := range *tags {
|
||||
ircmsg.Tags[tag] = value
|
||||
// helper to parse tags
|
||||
func (ircmsg *IrcMessage) parseTags(tags string) (err error) {
|
||||
for 0 < len(tags) {
|
||||
tagEnd := strings.IndexByte(tags, ';')
|
||||
endPos := tagEnd
|
||||
nextPos := tagEnd + 1
|
||||
if tagEnd == -1 {
|
||||
endPos = len(tags)
|
||||
nextPos = len(tags)
|
||||
}
|
||||
tagPair := tags[: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:]
|
||||
}
|
||||
ircmsg.SetTag(tagName, UnescapeTagValue(tagValue))
|
||||
// skip over the tag just processed, plus the delimiting ; if any
|
||||
tags = tags[nextPos:]
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// MakeMessage provides a simple way to create a new IrcMessage.
|
||||
func MakeMessage(tags map[string]string, prefix string, command string, params ...string) (ircmsg IrcMessage) {
|
||||
ircmsg.Prefix = prefix
|
||||
ircmsg.Command = command
|
||||
ircmsg.Params = params
|
||||
|
||||
ircmsg.UpdateTags(tags)
|
||||
return ircmsg
|
||||
}
|
||||
|
||||
// Line returns a sendable line created from an IrcMessage.
|
||||
func (ircmsg *IrcMessage) Line() (string, error) {
|
||||
bytes, err := ircmsg.line(0, 0, false)
|
||||
return string(bytes), err
|
||||
func (ircmsg *IrcMessage) Line() (result string, err error) {
|
||||
bytes, err := ircmsg.line(0, 0, 0, 0)
|
||||
if err == nil {
|
||||
result = string(bytes)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// LineBytes returns a sendable line, as a []byte, created from an IrcMessage.
|
||||
func (ircmsg *IrcMessage) LineBytes() ([]byte, error) {
|
||||
return ircmsg.line(0, 0, false)
|
||||
}
|
||||
|
||||
// LineMaxLen returns a sendable line created from an IrcMessage, limited by maxlen.
|
||||
func (ircmsg *IrcMessage) LineMaxLen(maxlenTags, maxlenRest int) (string, error) {
|
||||
bytes, err := ircmsg.line(maxlenTags, maxlenRest, true)
|
||||
return string(bytes), err
|
||||
}
|
||||
|
||||
// LineMaxLen returns a sendable line created from an IrcMessage, limited by maxlen,
|
||||
// as a []byte.
|
||||
func (ircmsg *IrcMessage) LineMaxLenBytes(maxlenTags, maxlenRest int) ([]byte, error) {
|
||||
return ircmsg.line(maxlenTags, maxlenRest, true)
|
||||
// LineBytesStrict returns a sendable line, as a []byte, created from an IrcMessage.
|
||||
// fromClient controls whether the server-side or client-side tag length limit
|
||||
// is enforced. If truncateLen is nonzero, it is the length at which the
|
||||
// non-tag portion of the message is truncated.
|
||||
func (ircmsg *IrcMessage) LineBytesStrict(fromClient bool, truncateLen int) ([]byte, error) {
|
||||
var tagLimit, clientOnlyTagDataLimit, serverAddedTagDataLimit int
|
||||
if fromClient {
|
||||
// enforce client max tags:
|
||||
// <client_max> (4096) :: '@' <tag_data 4094> ' '
|
||||
tagLimit = MaxlenTagsFromClient
|
||||
} else {
|
||||
// on the server side, enforce separate client-only and server-added tag budgets:
|
||||
// "Servers MUST NOT add tag data exceeding 4094 bytes to messages."
|
||||
// <combined_max> (8191) :: '@' <tag_data 4094> ';' <tag_data 4094> ' '
|
||||
clientOnlyTagDataLimit = MaxlenClientTagData
|
||||
serverAddedTagDataLimit = MaxlenServerTagData
|
||||
}
|
||||
return ircmsg.line(tagLimit, clientOnlyTagDataLimit, serverAddedTagDataLimit, truncateLen)
|
||||
}
|
||||
|
||||
// line returns a sendable line created from an IrcMessage.
|
||||
func (ircmsg *IrcMessage) line(maxlenTags, maxlenRest int, useMaxLen bool) ([]byte, error) {
|
||||
func (ircmsg *IrcMessage) line(tagLimit, clientOnlyTagDataLimit, serverAddedTagDataLimit, truncateLen int) ([]byte, error) {
|
||||
if len(ircmsg.Command) < 1 {
|
||||
return nil, ErrorCommandMissing
|
||||
}
|
||||
|
||||
var buf bytes.Buffer
|
||||
|
||||
if len(ircmsg.Command) < 1 {
|
||||
return nil, errors.New("irc: IRC messages MUST have a command")
|
||||
}
|
||||
|
||||
if len(ircmsg.Tags) > 0 {
|
||||
buf.WriteString("@")
|
||||
|
||||
// write the tags, computing the budgets for client-only tags and regular tags
|
||||
var lenRegularTags, lenClientOnlyTags, lenTags int
|
||||
if 0 < len(ircmsg.tags) || 0 < len(ircmsg.clientOnlyTags) {
|
||||
buf.WriteByte('@')
|
||||
firstTag := true
|
||||
for tag, val := range ircmsg.Tags {
|
||||
if !firstTag {
|
||||
buf.WriteString(";") // delimiter
|
||||
writeTags := func(tags map[string]string) {
|
||||
for tag, val := range tags {
|
||||
if !firstTag {
|
||||
buf.WriteByte(';') // delimiter
|
||||
}
|
||||
buf.WriteString(tag)
|
||||
if val != "" {
|
||||
buf.WriteByte('=')
|
||||
buf.WriteString(EscapeTagValue(val))
|
||||
}
|
||||
firstTag = false
|
||||
}
|
||||
buf.WriteString(tag)
|
||||
if val.HasValue {
|
||||
buf.WriteString("=")
|
||||
buf.WriteString(EscapeTagValue(val.Value))
|
||||
}
|
||||
firstTag = false
|
||||
}
|
||||
|
||||
// truncate if desired
|
||||
if useMaxLen && buf.Len() > maxlenTags {
|
||||
buf.Truncate(maxlenTags)
|
||||
writeTags(ircmsg.tags)
|
||||
lenRegularTags = buf.Len() - 1 // '@' is not counted
|
||||
writeTags(ircmsg.clientOnlyTags)
|
||||
lenClientOnlyTags = (buf.Len() - 1) - lenRegularTags // '@' is not counted
|
||||
if lenRegularTags != 0 {
|
||||
// semicolon between regular and client-only tags is not counted
|
||||
lenClientOnlyTags -= 1
|
||||
}
|
||||
|
||||
buf.WriteString(" ")
|
||||
buf.WriteByte(' ')
|
||||
}
|
||||
lenTags = buf.Len()
|
||||
|
||||
tagsLen := buf.Len()
|
||||
if 0 < tagLimit && tagLimit < buf.Len() {
|
||||
return nil, ErrorLineTooLong
|
||||
}
|
||||
if (0 < clientOnlyTagDataLimit && clientOnlyTagDataLimit < lenClientOnlyTags) || (0 < serverAddedTagDataLimit && serverAddedTagDataLimit < lenRegularTags) {
|
||||
return nil, ErrorLineTooLong
|
||||
}
|
||||
|
||||
if len(ircmsg.Prefix) > 0 {
|
||||
buf.WriteString(":")
|
||||
buf.WriteByte(':')
|
||||
buf.WriteString(ircmsg.Prefix)
|
||||
buf.WriteString(" ")
|
||||
buf.WriteByte(' ')
|
||||
}
|
||||
|
||||
buf.WriteString(ircmsg.Command)
|
||||
|
||||
if len(ircmsg.Params) > 0 {
|
||||
for i, param := range ircmsg.Params {
|
||||
buf.WriteString(" ")
|
||||
if len(param) < 1 || strings.Contains(param, " ") || param[0] == ':' {
|
||||
if i != len(ircmsg.Params)-1 {
|
||||
return nil, errors.New("irc: Cannot have an empty param, a param with spaces, or a param that starts with ':' before the last parameter")
|
||||
}
|
||||
buf.WriteString(":")
|
||||
for i, param := range ircmsg.Params {
|
||||
buf.WriteByte(' ')
|
||||
if len(param) < 1 || strings.IndexByte(param, ' ') != -1 || param[0] == ':' {
|
||||
if i != len(ircmsg.Params)-1 {
|
||||
return nil, ErrorBadParam
|
||||
}
|
||||
buf.WriteString(param)
|
||||
buf.WriteByte(':')
|
||||
}
|
||||
buf.WriteString(param)
|
||||
}
|
||||
|
||||
// truncate if desired
|
||||
// -2 for \r\n
|
||||
restLen := buf.Len() - tagsLen
|
||||
if useMaxLen && restLen > maxlenRest-2 {
|
||||
buf.Truncate(tagsLen + (maxlenRest - 2))
|
||||
restLen := buf.Len() - lenTags
|
||||
if 0 < truncateLen && (truncateLen-2) < restLen {
|
||||
buf.Truncate(lenTags + (truncateLen - 2))
|
||||
}
|
||||
|
||||
buf.WriteString("\r\n")
|
||||
|
||||
return buf.Bytes(), nil
|
||||
result := buf.Bytes()
|
||||
if bytes.IndexByte(result, '\x00') != -1 {
|
||||
return nil, ErrorLineContainsBadChar
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
@ -1,8 +1,8 @@
|
||||
package ircmsg
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"reflect"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
@ -20,9 +20,9 @@ var decodelentests = []testcodewithlen{
|
||||
{":dan-!d@localhost PRIVMSG dan #test :What a cool message\r\n", 20,
|
||||
MakeMessage(nil, "dan-!d@localhost", "PR")},
|
||||
{"@time=12732;re TEST *\r\n", 512,
|
||||
MakeMessage(MakeTags("time", "12732", "re", nil), "", "TEST", "*")},
|
||||
{"@time=12732;re TEST *\r\n", 12,
|
||||
MakeMessage(MakeTags("time", "12732", "r", nil), "", "TEST", "*")},
|
||||
MakeMessage(map[string]string{"time": "12732", "re": ""}, "", "TEST", "*")},
|
||||
{"@time=12732;re TEST *\r\n", 512,
|
||||
MakeMessage(map[string]string{"time": "12732", "re": ""}, "", "TEST", "*")},
|
||||
{":dan- TESTMSG\r\n", 2048,
|
||||
MakeMessage(nil, "dan-", "TESTMSG")},
|
||||
{":dan- TESTMSG dan \r\n", 12,
|
||||
@ -36,30 +36,61 @@ var decodelentests = []testcodewithlen{
|
||||
{"TESTMSG\r\n", 9,
|
||||
MakeMessage(nil, "", "TESTMSG")},
|
||||
}
|
||||
|
||||
// map[string]string{"time": "12732", "re": ""}
|
||||
var decodetests = []testcode{
|
||||
{":dan-!d@localhost PRIVMSG dan #test :What a cool message\r\n",
|
||||
MakeMessage(nil, "dan-!d@localhost", "PRIVMSG", "dan", "#test", "What a cool message")},
|
||||
{"@time=2848 :dan-!d@localhost LIST\r\n",
|
||||
MakeMessage(map[string]string{"time": "2848"}, "dan-!d@localhost", "LIST")},
|
||||
{"@time=2848 LIST\r\n",
|
||||
MakeMessage(map[string]string{"time": "2848"}, "", "LIST")},
|
||||
{"LIST\r\n",
|
||||
MakeMessage(nil, "", "LIST")},
|
||||
{"@time=12732;re TEST *a asda:fs :fhye tegh\r\n",
|
||||
MakeMessage(MakeTags("time", "12732", "re", nil), "", "TEST", "*a", "asda:fs", "fhye tegh")},
|
||||
MakeMessage(map[string]string{"time": "12732", "re": ""}, "", "TEST", "*a", "asda:fs", "fhye tegh")},
|
||||
{"@time=12732;re TEST *\r\n",
|
||||
MakeMessage(MakeTags("time", "12732", "re", nil), "", "TEST", "*")},
|
||||
MakeMessage(map[string]string{"time": "12732", "re": ""}, "", "TEST", "*")},
|
||||
{":dan- TESTMSG\r\n",
|
||||
MakeMessage(nil, "dan-", "TESTMSG")},
|
||||
{":dan- TESTMSG dan \r\n",
|
||||
MakeMessage(nil, "dan-", "TESTMSG", "dan")},
|
||||
{"@time=2019-02-28T19:30:01.727Z ping HiThere!\r\n",
|
||||
MakeMessage(map[string]string{"time": "2019-02-28T19:30:01.727Z"}, "", "PING", "HiThere!")},
|
||||
{"@+draft/test=hi\\nthere PING HiThere!\r\n",
|
||||
MakeMessage(map[string]string{"+draft/test": "hi\nthere"}, "", "PING", "HiThere!")},
|
||||
{"ping asdf\n",
|
||||
MakeMessage(nil, "", "PING", "asdf")},
|
||||
{"list",
|
||||
MakeMessage(nil, "", "LIST")},
|
||||
}
|
||||
var decodetesterrors = []string{
|
||||
"\r\n",
|
||||
" \r\n",
|
||||
"@tags=tesa\r\n",
|
||||
"@tags=tested \r\n",
|
||||
":dan- \r\n",
|
||||
":dan-\r\n",
|
||||
|
||||
type testparseerror struct {
|
||||
raw string
|
||||
err error
|
||||
}
|
||||
|
||||
var decodetesterrors = []testparseerror{
|
||||
{"", ErrorLineIsEmpty},
|
||||
{"\r\n", ErrorLineIsEmpty},
|
||||
{"\r\n ", ErrorLineIsEmpty},
|
||||
{"\r\n ", ErrorLineIsEmpty},
|
||||
{" \r\n", ErrorLineIsEmpty},
|
||||
{" \r\n ", ErrorLineIsEmpty},
|
||||
{" \r\n ", ErrorLineIsEmpty},
|
||||
{"@tags=tesa\r\n", ErrorLineIsEmpty},
|
||||
{"@tags=tested \r\n", ErrorLineIsEmpty},
|
||||
{":dan- \r\n", ErrorLineIsEmpty},
|
||||
{":dan-\r\n", ErrorLineIsEmpty},
|
||||
{"@tag1=1;tag2=2 :dan \r\n", ErrorLineIsEmpty},
|
||||
{"@tag1=1;tag2=2 :dan \r\n", ErrorLineIsEmpty},
|
||||
{"@tag1=1;tag2=2\x00 :dan \r\n", ErrorLineContainsBadChar},
|
||||
{"@tag1=1;tag2=2\x00 :shivaram PRIVMSG #channel hi\r\n", ErrorLineContainsBadChar},
|
||||
}
|
||||
|
||||
func TestDecode(t *testing.T) {
|
||||
for _, pair := range decodelentests {
|
||||
ircmsg, err := ParseLineMaxLen(pair.raw, pair.length, pair.length)
|
||||
ircmsg, err := ParseLineStrict(pair.raw, true, pair.length)
|
||||
if err != nil {
|
||||
t.Error(
|
||||
"For", pair.raw,
|
||||
@ -67,9 +98,6 @@ func TestDecode(t *testing.T) {
|
||||
)
|
||||
}
|
||||
|
||||
// short-circuit sourceline so tests work
|
||||
pair.message.SourceLine = strings.TrimRight(pair.raw, "\r\n")
|
||||
|
||||
if !reflect.DeepEqual(ircmsg, pair.message) {
|
||||
t.Error(
|
||||
"For", pair.raw,
|
||||
@ -87,9 +115,6 @@ func TestDecode(t *testing.T) {
|
||||
)
|
||||
}
|
||||
|
||||
// short-circuit sourceline so tests work
|
||||
pair.message.SourceLine = strings.TrimRight(pair.raw, "\r\n")
|
||||
|
||||
if !reflect.DeepEqual(ircmsg, pair.message) {
|
||||
t.Error(
|
||||
"For", pair.raw,
|
||||
@ -98,11 +123,13 @@ func TestDecode(t *testing.T) {
|
||||
)
|
||||
}
|
||||
}
|
||||
for _, line := range decodetesterrors {
|
||||
_, err := ParseLine(line)
|
||||
if err == nil {
|
||||
for _, pair := range decodetesterrors {
|
||||
_, err := ParseLineStrict(pair.raw, true, 0)
|
||||
if err != pair.err {
|
||||
t.Error(
|
||||
"Expected to fail parsing", line,
|
||||
"For", pair.raw,
|
||||
"expected", pair.err,
|
||||
"got", err,
|
||||
)
|
||||
}
|
||||
}
|
||||
@ -112,19 +139,19 @@ var encodetests = []testcode{
|
||||
{":dan-!d@localhost PRIVMSG dan #test :What a cool message\r\n",
|
||||
MakeMessage(nil, "dan-!d@localhost", "PRIVMSG", "dan", "#test", "What a cool message")},
|
||||
{"@time=12732 TEST *a asda:fs :fhye tegh\r\n",
|
||||
MakeMessage(MakeTags("time", "12732"), "", "TEST", "*a", "asda:fs", "fhye tegh")},
|
||||
MakeMessage(map[string]string{"time": "12732"}, "", "TEST", "*a", "asda:fs", "fhye tegh")},
|
||||
{"@time=12732 TEST *\r\n",
|
||||
MakeMessage(MakeTags("time", "12732"), "", "TEST", "*")},
|
||||
MakeMessage(map[string]string{"time": "12732"}, "", "TEST", "*")},
|
||||
{"@re TEST *\r\n",
|
||||
MakeMessage(MakeTags("re", nil), "", "TEST", "*")},
|
||||
MakeMessage(map[string]string{"re": ""}, "", "TEST", "*")},
|
||||
}
|
||||
var encodelentests = []testcodewithlen{
|
||||
{":dan-!d@lo\r\n", 12,
|
||||
MakeMessage(nil, "dan-!d@localhost", "PRIVMSG", "dan", "#test", "What a cool message")},
|
||||
{"@time=12732 TEST *\r\n", 52,
|
||||
MakeMessage(MakeTags("time", "12732"), "", "TEST", "*")},
|
||||
{"@riohwih TEST *\r\n", 8,
|
||||
MakeMessage(MakeTags("riohwihowihirgowihre", nil), "", "TEST", "*", "*")},
|
||||
MakeMessage(map[string]string{"time": "12732"}, "", "TEST", "*")},
|
||||
{"@riohwihowihirgowihre TEST *\r\n", 8,
|
||||
MakeMessage(map[string]string{"riohwihowihirgowihre": ""}, "", "TEST", "*", "*")},
|
||||
}
|
||||
|
||||
func TestEncode(t *testing.T) {
|
||||
@ -146,7 +173,7 @@ func TestEncode(t *testing.T) {
|
||||
}
|
||||
}
|
||||
for _, pair := range encodelentests {
|
||||
line, err := pair.message.LineMaxLen(pair.length, pair.length)
|
||||
line, err := pair.message.LineBytesStrict(true, pair.length)
|
||||
if err != nil {
|
||||
t.Error(
|
||||
"For", pair.raw,
|
||||
@ -154,7 +181,7 @@ func TestEncode(t *testing.T) {
|
||||
)
|
||||
}
|
||||
|
||||
if line != pair.raw {
|
||||
if string(line) != pair.raw {
|
||||
t.Error(
|
||||
"For", pair.message,
|
||||
"expected", pair.raw,
|
||||
@ -165,7 +192,7 @@ func TestEncode(t *testing.T) {
|
||||
|
||||
// make sure we fail on no command
|
||||
msg := MakeMessage(nil, "example.com", "", "*")
|
||||
_, err := msg.Line()
|
||||
_, err := msg.LineBytesStrict(true, 0)
|
||||
if err == nil {
|
||||
t.Error(
|
||||
"For", "Test Failure 1",
|
||||
@ -176,7 +203,7 @@ func TestEncode(t *testing.T) {
|
||||
|
||||
// make sure we fail with params in right way
|
||||
msg = MakeMessage(nil, "example.com", "TEST", "*", "t s", "", "Param after empty!")
|
||||
_, err = msg.Line()
|
||||
_, err = msg.LineBytesStrict(true, 0)
|
||||
if err == nil {
|
||||
t.Error(
|
||||
"For", "Test Failure 2",
|
||||
@ -185,3 +212,154 @@ func TestEncode(t *testing.T) {
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
var testMessages = []IrcMessage{
|
||||
{
|
||||
tags: map[string]string{"time": "2019-02-27T04:38:57.489Z", "account": "dan-"},
|
||||
clientOnlyTags: map[string]string{"+status": "typing"},
|
||||
Prefix: "dan-!~user@example.com",
|
||||
Command: "TAGMSG",
|
||||
},
|
||||
{
|
||||
clientOnlyTags: map[string]string{"+status": "typing"},
|
||||
Command: "PING", // invalid PING command but we don't care
|
||||
},
|
||||
{
|
||||
tags: map[string]string{"time": "2019-02-27T04:38:57.489Z"},
|
||||
Command: "PING", // invalid PING command but we don't care
|
||||
Params: []string{"12345"},
|
||||
},
|
||||
{
|
||||
tags: map[string]string{"time": "2019-02-27T04:38:57.489Z", "account": "dan-"},
|
||||
Prefix: "dan-!~user@example.com",
|
||||
Command: "PRIVMSG",
|
||||
Params: []string{"#ircv3", ":smiley:"},
|
||||
},
|
||||
{
|
||||
tags: map[string]string{"time": "2019-02-27T04:38:57.489Z", "account": "dan-"},
|
||||
Prefix: "dan-!~user@example.com",
|
||||
Command: "PRIVMSG",
|
||||
Params: []string{"#ircv3", "\x01ACTION writes some specs!\x01"},
|
||||
},
|
||||
{
|
||||
Prefix: "dan-!~user@example.com",
|
||||
Command: "PRIVMSG",
|
||||
Params: []string{"#ircv3", ": long trailing command with langue française in it"},
|
||||
},
|
||||
{
|
||||
Prefix: "dan-!~user@example.com",
|
||||
Command: "PRIVMSG",
|
||||
Params: []string{"#ircv3", " : long trailing command with langue française in it "},
|
||||
},
|
||||
{
|
||||
Prefix: "shivaram",
|
||||
Command: "KLINE",
|
||||
Params: []string{"ANDKILL", "24h", "tkadich", "your", "client", "is", "disconnecting", "too", "much"},
|
||||
},
|
||||
{
|
||||
tags: map[string]string{"time": "2019-02-27T06:01:23.545Z", "draft/msgid": "xjmgr6e4ih7izqu6ehmrtrzscy"},
|
||||
Prefix: "שיברם",
|
||||
Command: "PRIVMSG",
|
||||
Params: []string{"ויקם מלך חדש על מצרים אשר לא ידע את יוסף"},
|
||||
},
|
||||
{
|
||||
Prefix: "shivaram!~user@2001:0db8::1",
|
||||
Command: "KICK",
|
||||
Params: []string{"#darwin", "devilbat", ":::::::::::::: :::::::::::::"},
|
||||
},
|
||||
}
|
||||
|
||||
func TestEncodeDecode(t *testing.T) {
|
||||
for _, message := range testMessages {
|
||||
encoded, err := message.LineBytesStrict(false, 0)
|
||||
if err != nil {
|
||||
t.Errorf("Couldn't encode %v: %v", message, err)
|
||||
}
|
||||
parsed, err := ParseLineStrict(string(encoded), true, 0)
|
||||
if err != nil {
|
||||
t.Errorf("Couldn't re-decode %v: %v", encoded, err)
|
||||
}
|
||||
if !reflect.DeepEqual(message, parsed) {
|
||||
t.Errorf("After encoding and re-parsing, got different messages:\n%v\n%v", message, parsed)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestErrorLineTooLongGeneration(t *testing.T) {
|
||||
message := IrcMessage{
|
||||
tags: map[string]string{"draft/msgid": "SAXV5OYJUr18CNJzdWa1qQ"},
|
||||
Prefix: "shivaram",
|
||||
Command: "PRIVMSG",
|
||||
Params: []string{"aaaaaaaaaaaaaaaaaaaaa"},
|
||||
}
|
||||
_, err := message.LineBytesStrict(true, 0)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
|
||||
for i := 0; i < 100; i += 1 {
|
||||
message.SetTag(fmt.Sprintf("+client-tag-%d", i), "ok")
|
||||
}
|
||||
line, err := message.LineBytesStrict(true, 0)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
if 4096 < len(line) {
|
||||
t.Errorf("line is too long: %d", len(line))
|
||||
}
|
||||
|
||||
// add excess tag data, pushing us over the limit
|
||||
for i := 100; i < 500; i += 1 {
|
||||
message.SetTag(fmt.Sprintf("+client-tag-%d", i), "ok")
|
||||
}
|
||||
line, err = message.LineBytesStrict(true, 0)
|
||||
if err != ErrorLineTooLong {
|
||||
t.Error(err)
|
||||
}
|
||||
|
||||
message.clientOnlyTags = nil
|
||||
for i := 0; i < 500; i += 1 {
|
||||
message.SetTag(fmt.Sprintf("server-tag-%d", i), "ok")
|
||||
}
|
||||
line, err = message.LineBytesStrict(true, 0)
|
||||
if err != ErrorLineTooLong {
|
||||
t.Error(err)
|
||||
}
|
||||
|
||||
message.tags = nil
|
||||
message.clientOnlyTags = nil
|
||||
for i := 0; i < 200; i += 1 {
|
||||
message.SetTag(fmt.Sprintf("server-tag-%d", i), "ok")
|
||||
message.SetTag(fmt.Sprintf("+client-tag-%d", i), "ok")
|
||||
}
|
||||
// client cannot send this much tag data:
|
||||
line, err = message.LineBytesStrict(true, 0)
|
||||
if err != ErrorLineTooLong {
|
||||
t.Error(err)
|
||||
}
|
||||
// but a server can, since the tags are split between client and server budgets:
|
||||
line, err = message.LineBytesStrict(false, 0)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkGenerate(b *testing.B) {
|
||||
msg := MakeMessage(
|
||||
map[string]string{"time": "2019-02-28T08:12:43.480Z", "account": "shivaram"},
|
||||
"shivaram_hexchat!~user@irc.darwin.network",
|
||||
"PRIVMSG",
|
||||
"#darwin", "what's up guys",
|
||||
)
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
msg.LineBytesStrict(false, 0)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkParse(b *testing.B) {
|
||||
line := "@account=shivaram;draft/msgid=dqhkgglocqikjqikbkcdnv5dsq;time=2019-03-01T20:11:21.833Z :shivaram!~shivaram@good-fortune PRIVMSG #darwin :you're an EU citizen, right? it's illegal for you to be here now"
|
||||
for i := 0; i < b.N; i++ {
|
||||
ParseLineStrict(line, false, 0)
|
||||
}
|
||||
}
|
||||
|
159
ircmsg/tags.go
159
ircmsg/tags.go
@ -3,28 +3,34 @@
|
||||
|
||||
package ircmsg
|
||||
|
||||
import "bytes"
|
||||
import "strings"
|
||||
|
||||
var (
|
||||
// valtoescape replaces real characters with message tag escapes.
|
||||
valtoescape = strings.NewReplacer("\\", "\\\\", ";", "\\:", " ", "\\s", "\r", "\\r", "\n", "\\n")
|
||||
|
||||
// escapetoval contains the IRCv3 Tag Escapes and how they map to characters.
|
||||
escapetoval = map[rune]byte{
|
||||
':': ';',
|
||||
's': ' ',
|
||||
'\\': '\\',
|
||||
'r': '\r',
|
||||
'n': '\n',
|
||||
}
|
||||
escapedCharLookupTable [256]byte
|
||||
)
|
||||
|
||||
func init() {
|
||||
// most chars escape to themselves
|
||||
for i := 0; i < 256; i += 1 {
|
||||
escapedCharLookupTable[i] = byte(i)
|
||||
}
|
||||
// these are the exceptions
|
||||
escapedCharLookupTable[':'] = ';'
|
||||
escapedCharLookupTable['s'] = ' '
|
||||
escapedCharLookupTable['r'] = '\r'
|
||||
escapedCharLookupTable['n'] = '\n'
|
||||
}
|
||||
|
||||
// EscapeTagValue takes a value, and returns an escaped message tag value.
|
||||
//
|
||||
// This function is automatically used when lines are created from an
|
||||
// IrcMessage, so you don't need to call it yourself before creating a line.
|
||||
func EscapeTagValue(in string) string {
|
||||
return valtoescape.Replace(in)
|
||||
func EscapeTagValue(inString string) string {
|
||||
return valtoescape.Replace(inString)
|
||||
}
|
||||
|
||||
// UnescapeTagValue takes an escaped message tag value, and returns the raw value.
|
||||
@ -32,117 +38,38 @@ func EscapeTagValue(in string) string {
|
||||
// This function is automatically used when lines are interpreted by ParseLine,
|
||||
// so you don't need to call it yourself after parsing a line.
|
||||
func UnescapeTagValue(inString string) string {
|
||||
in := []rune(inString)
|
||||
var out string
|
||||
for 0 < len(in) {
|
||||
if in[0] == '\\' && len(in) > 1 {
|
||||
val, exists := escapetoval[in[1]]
|
||||
if exists == true {
|
||||
out += string(val)
|
||||
// buf.Len() == 0 is the fastpath where we have not needed to unescape any chars
|
||||
var buf bytes.Buffer
|
||||
remainder := inString
|
||||
for {
|
||||
backslashPos := strings.IndexByte(remainder, '\\')
|
||||
|
||||
if backslashPos == -1 {
|
||||
if buf.Len() == 0 {
|
||||
return inString
|
||||
} else {
|
||||
out += string(in[1])
|
||||
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
|
||||
}
|
||||
in = in[2:]
|
||||
} else if in[0] == '\\' {
|
||||
// trailing slash
|
||||
in = in[1:]
|
||||
} else {
|
||||
out += string(in[0])
|
||||
in = in[1:]
|
||||
}
|
||||
}
|
||||
|
||||
return out
|
||||
}
|
||||
|
||||
// TagValue represents the value of a tag. This is because tags may have
|
||||
// no value at all or just an empty value, and this can represent both
|
||||
// using the HasValue attribute.
|
||||
type TagValue struct {
|
||||
HasValue bool
|
||||
Value string
|
||||
}
|
||||
|
||||
// NoTagValue returns an empty TagValue.
|
||||
func NoTagValue() TagValue {
|
||||
var tag TagValue
|
||||
tag.HasValue = false
|
||||
return tag
|
||||
}
|
||||
|
||||
// MakeTagValue returns a TagValue with a defined value.
|
||||
func MakeTagValue(value string) TagValue {
|
||||
var tag TagValue
|
||||
tag.HasValue = true
|
||||
tag.Value = value
|
||||
return tag
|
||||
}
|
||||
|
||||
// MakeTags simplifies tag creation for new messages.
|
||||
//
|
||||
// For example: MakeTags("intent", "PRIVMSG", "account", "bunny", "noval", nil)
|
||||
func MakeTags(values ...interface{}) *map[string]TagValue {
|
||||
var tags map[string]TagValue
|
||||
tags = make(map[string]TagValue)
|
||||
|
||||
for len(values) > 1 {
|
||||
tag := values[0].(string)
|
||||
value := values[1]
|
||||
var val TagValue
|
||||
|
||||
if value == nil {
|
||||
val = NoTagValue()
|
||||
} else {
|
||||
val = MakeTagValue(value.(string))
|
||||
}
|
||||
|
||||
tags[tag] = val
|
||||
|
||||
values = values[2:]
|
||||
}
|
||||
|
||||
return &tags
|
||||
}
|
||||
|
||||
// ParseTags takes a tag string such as "network=freenode;buffer=#chan;joined=1;topic=some\stopic" and outputs a TagValue map.
|
||||
func ParseTags(tags string) (map[string]TagValue, error) {
|
||||
return parseTags(tags, 0, false)
|
||||
}
|
||||
|
||||
// parseTags does the actual tags parsing for the above user-facing function.
|
||||
func parseTags(tags string, maxlenTags int, useMaxLen bool) (map[string]TagValue, error) {
|
||||
tagMap := make(map[string]TagValue)
|
||||
|
||||
// confirm no bad strings exist
|
||||
if strings.ContainsAny(tags, " \r\n") {
|
||||
return tagMap, ErrorTagsContainsBadChar
|
||||
}
|
||||
|
||||
// truncate if desired
|
||||
if useMaxLen && len(tags) > maxlenTags {
|
||||
tags = tags[:maxlenTags]
|
||||
}
|
||||
|
||||
for _, fulltag := range strings.Split(tags, ";") {
|
||||
// skip empty tag string values
|
||||
if len(fulltag) < 1 {
|
||||
continue
|
||||
// non-trailing backslash detected; we're now on the slowpath
|
||||
// where we modify the string
|
||||
if buf.Len() == 0 {
|
||||
buf.Grow(len(inString)) // just an optimization
|
||||
}
|
||||
|
||||
var name string
|
||||
var val TagValue
|
||||
if strings.Contains(fulltag, "=") {
|
||||
val.HasValue = true
|
||||
splittag := strings.SplitN(fulltag, "=", 2)
|
||||
name = splittag[0]
|
||||
val.Value = UnescapeTagValue(splittag[1])
|
||||
} else {
|
||||
name = fulltag
|
||||
val.HasValue = false
|
||||
}
|
||||
|
||||
tagMap[name] = val
|
||||
buf.WriteString(remainder[:backslashPos])
|
||||
buf.WriteByte(escapedCharLookupTable[remainder[backslashPos+1]])
|
||||
remainder = remainder[backslashPos+2:]
|
||||
}
|
||||
|
||||
return tagMap, nil
|
||||
return buf.String()
|
||||
}
|
||||
|
@ -1,6 +1,7 @@
|
||||
package ircmsg
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"reflect"
|
||||
"testing"
|
||||
)
|
||||
@ -21,6 +22,14 @@ var unescapeTests = []testcase{
|
||||
{"te\\n\\kst\\", "te\nkst"},
|
||||
{"te\\\\nst", "te\\nst"},
|
||||
{"te😃st", "te😃st"},
|
||||
{"0\\n1\\n2\\n3\\n4\\n5\\n6\\n\\", "0\n1\n2\n3\n4\n5\n6\n"},
|
||||
{"test\\", "test"},
|
||||
{"te\\:st\\", "te;st"},
|
||||
{"te\\:\\st\\", "te; t"},
|
||||
{"\\\\te\\:\\st", "\\te; t"},
|
||||
{"test\\", "test"},
|
||||
{"\\", ""},
|
||||
{"", ""},
|
||||
}
|
||||
|
||||
func TestEscape(t *testing.T) {
|
||||
@ -65,50 +74,24 @@ func TestUnescape(t *testing.T) {
|
||||
// tag string tests
|
||||
type testtags struct {
|
||||
raw string
|
||||
tags map[string]TagValue
|
||||
}
|
||||
type testtagswithlen struct {
|
||||
raw string
|
||||
length int
|
||||
tags map[string]TagValue
|
||||
tags map[string]string
|
||||
}
|
||||
|
||||
var tagdecodelentests = []testtagswithlen{
|
||||
{"time=12732;re", 512, *MakeTags("time", "12732", "re", nil)},
|
||||
{"time=12732;re", 12, *MakeTags("time", "12732", "r", nil)},
|
||||
{"", 512, *MakeTags()},
|
||||
}
|
||||
var tagdecodetests = []testtags{
|
||||
{"", *MakeTags()},
|
||||
{"time=12732;re", *MakeTags("time", "12732", "re", nil)},
|
||||
{"", map[string]string{}},
|
||||
{"time=12732;re", map[string]string{"time": "12732", "re": ""}},
|
||||
{"time=12732;re=;asdf=5678", map[string]string{"time": "12732", "re": "", "asdf": "5678"}},
|
||||
{"=these;time=12732;=shouldbe;re=;asdf=5678;=ignored", map[string]string{"time": "12732", "re": "", "asdf": "5678"}},
|
||||
}
|
||||
var tagdecodetesterrors = []string{
|
||||
"\r\n",
|
||||
" \r\n",
|
||||
"tags=tesa\r\n",
|
||||
"tags=tested \r\n",
|
||||
|
||||
func parseTags(rawTags string) (map[string]string, error) {
|
||||
message, err := ParseLineStrict(fmt.Sprintf("@%s :shivaram TAGMSG #darwin\r\n", rawTags), true, 0)
|
||||
return message.AllTags(), err
|
||||
}
|
||||
|
||||
func TestDecodeTags(t *testing.T) {
|
||||
for _, pair := range tagdecodelentests {
|
||||
tags, err := parseTags(pair.raw, pair.length, true)
|
||||
if err != nil {
|
||||
t.Error(
|
||||
"For", pair.raw,
|
||||
"Failed to parse tags:", err,
|
||||
)
|
||||
}
|
||||
|
||||
if !reflect.DeepEqual(tags, pair.tags) {
|
||||
t.Error(
|
||||
"For", pair.raw,
|
||||
"expected", pair.tags,
|
||||
"got", tags,
|
||||
)
|
||||
}
|
||||
}
|
||||
for _, pair := range tagdecodetests {
|
||||
tags, err := ParseTags(pair.raw)
|
||||
tags, err := parseTags(pair.raw)
|
||||
if err != nil {
|
||||
t.Error(
|
||||
"For", pair.raw,
|
||||
@ -124,12 +107,4 @@ func TestDecodeTags(t *testing.T) {
|
||||
)
|
||||
}
|
||||
}
|
||||
for _, line := range tagdecodetesterrors {
|
||||
_, err := ParseTags(line)
|
||||
if err == nil {
|
||||
t.Error(
|
||||
"Expected to fail parsing", line,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user