1
0
Fork 0
forked from External/ergo

use ergochat/irc-go instead of goshuirc/irc-go

This commit is contained in:
Shivaram Lingamneni 2021-06-18 02:41:57 -04:00
parent 66af8cd63c
commit 4910aefa37
32 changed files with 95 additions and 53 deletions

13
vendor/github.com/ergochat/irc-go/LICENSE generated vendored Normal file
View file

@ -0,0 +1,13 @@
Copyright (c) 2016-2017 Daniel Oaks
Permission to use, copy, modify, and/or distribute this software for any
purpose with or without fee is hereby granted, provided that the above
copyright notice and this permission notice appear in all copies.
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND
FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
PERFORMANCE OF THIS SOFTWARE.

86
vendor/github.com/ergochat/irc-go/ircfmt/doc.go generated vendored Normal file
View file

@ -0,0 +1,86 @@
// written by Daniel Oaks <daniel@danieloaks.net>
// released under the ISC license
/*
Package ircfmt handles IRC formatting codes, escaping and unescaping.
This allows for a simpler representation of strings that contain colour codes,
bold codes, and such, without having to write and handle raw bytes when
assembling outgoing messages.
This lets you turn raw IRC messages into our escaped versions, and turn escaped
versions back into raw messages suitable for sending on IRC connections. This
is designed to be used on things like PRIVMSG / NOTICE commands, MOTD blocks,
and such.
The escape character we use in this library is the dollar sign ("$"), along
with the given escape characters:
--------------------------------
Name | Escape | Raw
--------------------------------
Dollarsign | $$ | $
Bold | $b | 0x02
Colour | $c | 0x03
Monospace | $m | 0x11
Reverse Colour | $v | 0x16
Italic | $i | 0x1d
Strikethrough | $s | 0x1e
Underscore | $u | 0x1f
Reset | $r | 0x0f
--------------------------------
Colours are escaped in a slightly different way, using the actual names of them
rather than just the raw numbers.
In our escaped format, the colours for the fore and background are contained in
square brackets after the colour ("$c") escape. For example:
Red foreground:
Escaped: This is a $c[red]cool message!
Raw: This is a 0x034cool message!
Blue foreground, green background:
Escaped: This is a $c[blue,green]rad message!
Raw: This is a 0x032,3rad message!
When assembling a raw message, we make sure to use the full colour code
("02" vs just "2") when it could become confused due to numbers just after the
colour escape code. For instance, lines like this will be unescaped correctly:
No number after colour escape:
Escaped: This is a $c[red]cool message!
Raw: This is a 0x034cool message!
Number after colour escape:
Escaped: This is $c[blue]20% cooler!
Raw: This is 0x030220% cooler
Here are the colour names and codes we recognise:
--------------------
Code | Name
--------------------
00 | white
01 | black
02 | blue
03 | green
04 | red
05 | brown
06 | magenta
07 | orange
08 | yellow
09 | light green
10 | cyan
11 | light cyan
12 | light blue
13 | pink
14 | grey
15 | light grey
99 | default
--------------------
These other colours aren't given names:
https://modern.ircdocs.horse/formatting.html#colors-16-98
*/
package ircfmt

317
vendor/github.com/ergochat/irc-go/ircfmt/ircfmt.go generated vendored Normal file
View file

@ -0,0 +1,317 @@
// written by Daniel Oaks <daniel@danieloaks.net>
// released under the ISC license
package ircfmt
import (
"regexp"
"strings"
)
const (
// raw bytes and strings to do replacing with
bold string = "\x02"
colour string = "\x03"
monospace string = "\x11"
reverseColour string = "\x16"
italic string = "\x1d"
strikethrough string = "\x1e"
underline string = "\x1f"
reset string = "\x0f"
runecolour rune = '\x03'
runebold rune = '\x02'
runemonospace rune = '\x11'
runereverseColour rune = '\x16'
runeitalic rune = '\x1d'
runestrikethrough rune = '\x1e'
runereset rune = '\x0f'
runeunderline rune = '\x1f'
// valid characters in a colour code character, for speed
colours1 string = "0123456789"
)
var (
// valtoescape replaces most of IRC characters with our escapes.
valtoescape = strings.NewReplacer("$", "$$", colour, "$c", reverseColour, "$v", bold, "$b", italic, "$i", strikethrough, "$s", underline, "$u", monospace, "$m", reset, "$r")
// valToStrip replaces most of the IRC characters with nothing
valToStrip = strings.NewReplacer(colour, "$c", reverseColour, "", bold, "", italic, "", strikethrough, "", underline, "", monospace, "", reset, "")
// escapetoval contains most of our escapes and how they map to real IRC characters.
// intentionally skips colour, since that's handled elsewhere.
escapetoval = map[rune]string{
'$': "$",
'b': bold,
'i': italic,
'v': reverseColour,
's': strikethrough,
'u': underline,
'm': monospace,
'r': reset,
}
// valid colour codes
numtocolour = map[string]string{
"99": "default",
"15": "light grey",
"14": "grey",
"13": "pink",
"12": "light blue",
"11": "light cyan",
"10": "cyan",
"09": "light green",
"08": "yellow",
"07": "orange",
"06": "magenta",
"05": "brown",
"04": "red",
"03": "green",
"02": "blue",
"01": "black",
"00": "white",
"9": "light green",
"8": "yellow",
"7": "orange",
"6": "magenta",
"5": "brown",
"4": "red",
"3": "green",
"2": "blue",
"1": "black",
"0": "white",
}
colourcodesTruncated = map[string]string{
"white": "0",
"black": "1",
"blue": "2",
"green": "3",
"red": "4",
"brown": "5",
"magenta": "6",
"orange": "7",
"yellow": "8",
"light green": "9",
"cyan": "10",
"light cyan": "11",
"light blue": "12",
"pink": "13",
"grey": "14",
"light grey": "15",
"default": "99",
}
bracketedExpr = regexp.MustCompile(`^\[.*?\]`)
colourDigits = regexp.MustCompile(`^[0-9]{1,2}$`)
)
// Escape takes a raw IRC string and returns it with our escapes.
//
// IE, it turns this: "This is a \x02cool\x02, \x034red\x0f message!"
// into: "This is a $bcool$b, $c[red]red$r message!"
func Escape(in string) string {
// replace all our usual escapes
in = valtoescape.Replace(in)
inRunes := []rune(in)
//var out string
out := strings.Builder{}
for 0 < len(inRunes) {
if 1 < len(inRunes) && inRunes[0] == '$' && inRunes[1] == 'c' {
// handle colours
out.WriteString("$c")
inRunes = inRunes[2:] // strip colour code chars
if len(inRunes) < 1 || !strings.Contains(colours1, string(inRunes[0])) {
out.WriteString("[]")
continue
}
var foreBuffer, backBuffer string
foreBuffer += string(inRunes[0])
inRunes = inRunes[1:]
if 0 < len(inRunes) && strings.Contains(colours1, string(inRunes[0])) {
foreBuffer += string(inRunes[0])
inRunes = inRunes[1:]
}
if 1 < len(inRunes) && inRunes[0] == ',' && strings.Contains(colours1, string(inRunes[1])) {
backBuffer += string(inRunes[1])
inRunes = inRunes[2:]
if 0 < len(inRunes) && strings.Contains(colours1, string(inRunes[0])) {
backBuffer += string(inRunes[0])
inRunes = inRunes[1:]
}
}
foreName, exists := numtocolour[foreBuffer]
if !exists {
foreName = foreBuffer
}
backName, exists := numtocolour[backBuffer]
if !exists {
backName = backBuffer
}
out.WriteRune('[')
out.WriteString(foreName)
if backName != "" {
out.WriteRune(',')
out.WriteString(backName)
}
out.WriteRune(']')
} else {
// special case for $$c
if len(inRunes) > 2 && inRunes[0] == '$' && inRunes[1] == '$' && inRunes[2] == 'c' {
out.WriteRune(inRunes[0])
out.WriteRune(inRunes[1])
out.WriteRune(inRunes[2])
inRunes = inRunes[3:]
} else {
out.WriteRune(inRunes[0])
inRunes = inRunes[1:]
}
}
}
return out.String()
}
// Strip takes a raw IRC string and removes it with all formatting codes removed
// IE, it turns this: "This is a \x02cool\x02, \x034red\x0f message!"
// into: "This is a cool, red message!"
func Strip(in string) string {
out := strings.Builder{}
runes := []rune(in)
if out.Len() < len(runes) { // Reduce allocations where needed
out.Grow(len(in) - out.Len())
}
for len(runes) > 0 {
switch runes[0] {
case runebold, runemonospace, runereverseColour, runeitalic, runestrikethrough, runeunderline, runereset:
runes = runes[1:]
case runecolour:
runes = removeColour(runes)
default:
out.WriteRune(runes[0])
runes = runes[1:]
}
}
return out.String()
}
func removeNumber(runes []rune) []rune {
if len(runes) > 0 && runes[0] >= '0' && runes[0] <= '9' {
runes = runes[1:]
}
return runes
}
func removeColour(runes []rune) []rune {
if runes[0] != runecolour {
return runes
}
runes = runes[1:]
runes = removeNumber(runes)
runes = removeNumber(runes)
if len(runes) > 1 && runes[0] == ',' && runes[1] >= '0' && runes[1] <= '9' {
runes = runes[2:]
} else {
return runes // Nothing else because we dont have a comma
}
runes = removeNumber(runes)
return runes
}
// resolve "light blue" to "12", "12" to "12", "asdf" to "", etc.
func resolveToColourCode(str string) (result string) {
str = strings.ToLower(strings.TrimSpace(str))
if colourDigits.MatchString(str) {
return str
}
return colourcodesTruncated[str]
}
// resolve "[light blue, black]" to ("13, "1")
func resolveToColourCodes(namedColors string) (foreground, background string) {
// cut off the brackets
namedColors = strings.TrimPrefix(namedColors, "[")
namedColors = strings.TrimSuffix(namedColors, "]")
var foregroundStr, backgroundStr string
commaIdx := strings.IndexByte(namedColors, ',')
if commaIdx != -1 {
foregroundStr = namedColors[:commaIdx]
backgroundStr = namedColors[commaIdx+1:]
} else {
foregroundStr = namedColors
}
return resolveToColourCode(foregroundStr), resolveToColourCode(backgroundStr)
}
// Unescape takes our escaped string and returns a raw IRC string.
//
// IE, it turns this: "This is a $bcool$b, $c[red]red$r message!"
// into this: "This is a \x02cool\x02, \x034red\x0f message!"
func Unescape(in string) string {
var out strings.Builder
remaining := in
for len(remaining) != 0 {
char := remaining[0]
remaining = remaining[1:]
if char != '$' || len(remaining) == 0 {
// not an escape
out.WriteByte(char)
continue
}
// ingest the next character of the escape
char = remaining[0]
remaining = remaining[1:]
if char == 'c' {
out.WriteString(colour)
namedColors := bracketedExpr.FindString(remaining)
if namedColors == "" {
// for a non-bracketed color code, output the following characters directly,
// e.g., `$c1,8` will become `\x031,8`
continue
}
// process bracketed color codes:
remaining = remaining[len(namedColors):]
followedByDigit := len(remaining) != 0 && ('0' <= remaining[0] && remaining[0] <= '9')
foreground, background := resolveToColourCodes(namedColors)
if foreground != "" {
if len(foreground) == 1 && background == "" && followedByDigit {
out.WriteByte('0')
}
out.WriteString(foreground)
if background != "" {
out.WriteByte(',')
if len(background) == 1 && followedByDigit {
out.WriteByte('0')
}
out.WriteString(background)
}
}
} else {
val, exists := escapetoval[rune(char)]
if exists {
out.WriteString(val)
} else {
// invalid escape, use the raw char
out.WriteByte(char)
}
}
}
return out.String()
}

7
vendor/github.com/ergochat/irc-go/ircmsg/doc.go generated vendored Normal file
View file

@ -0,0 +1,7 @@
// written by Daniel Oaks <daniel@danieloaks.net>
// released under the ISC license
/*
Package ircmsg helps parse and create lines for IRC connections.
*/
package ircmsg

463
vendor/github.com/ergochat/irc-go/ircmsg/message.go generated vendored Normal file
View file

@ -0,0 +1,463 @@
// 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
import (
"bytes"
"errors"
"strings"
"unicode/utf8"
)
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")
// ErrorLineContainsBadChar indicates that the line contained invalid characters
ErrorLineContainsBadChar = errors.New("Line contains invalid characters")
// ErrorBodyTooLong indicates that the message body exceeded the specified
// length limit (typically 512 bytes). This error is non-fatal; if encountered
// when parsing a message, the message is parsed up to the length limit, and
// if encountered when serializing a message, the message is truncated to the limit.
ErrorBodyTooLong = errors.New("Line body exceeded the specified length limit; outgoing messages will be truncated")
// ErrorTagsTooLong indicates that the message exceeded the maximum tag length
// (the specified response on the server side is 417 ERR_INPUTTOOLONG).
ErrorTagsTooLong = errors.New("Line could not be processed because its tag data exceeded the length limit")
// ErrorInvalidTagContent indicates that a tag name or value was invalid
ErrorInvalidTagContent = errors.New("Line could not be processed because it contained an invalid tag name or value")
// ErrorCommandMissing indicates that an IRC message was invalid because it lacked a command.
ErrorCommandMissing = errors.New("IRC messages MUST have a command")
// ErrorBadParam indicates that an IRC message could not be serialized because
// its parameters violated the syntactic constraints on IRC parameters:
// non-final parameters cannot be empty, contain a space, or start with `:`.
ErrorBadParam = errors.New("Cannot have an empty param, a param with spaces, or a param that starts with ':' before the last parameter")
)
// Message 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 Message struct {
Prefix string
Command string
Params []string
forceTrailing bool
tags map[string]string
clientOnlyTags map[string]string
}
// ForceTrailing ensures that when the message is serialized, the final parameter
// will be encoded as a "trailing parameter" (preceded by a colon). This is
// almost never necessary and should not be used except when having to interact
// with broken implementations that don't correctly interpret IRC messages.
func (msg *Message) ForceTrailing() {
msg.forceTrailing = true
}
// GetTag returns whether a tag is present, and if so, what its value is.
func (msg *Message) 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
}
}
// HasTag returns whether a tag is present.
func (msg *Message) HasTag(tagName string) (present bool) {
present, _ = msg.GetTag(tagName)
return
}
// SetTag sets a tag.
func (msg *Message) 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 *Message) 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 *Message) UpdateTags(tags map[string]string) {
for name, value := range tags {
msg.SetTag(name, value)
}
}
// AllTags returns all tags as a single map.
func (msg *Message) 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 Message object and
// should not be modified.
func (msg *Message) ClientOnlyTags() map[string]string {
return msg.clientOnlyTags
}
// ParseLine creates and returns a message from the given IRC line.
func ParseLine(line string) (ircmsg Message, err error) {
return parseLine(line, 0, 0)
}
// ParseLineStrict creates and returns an Message from the given IRC line,
// taking the maximum length into account and truncating the message as appropriate.
// 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 Message, err error) {
maxTagDataLength := MaxlenTagData
if fromClient {
maxTagDataLength = MaxlenClientTagData
}
return parseLine(line, maxTagDataLength, truncateLen)
}
// slice off any amount of ' ' from the front of the string
func trimInitialSpaces(str string) string {
var i int
for i = 0; i < len(str) && str[i] == ' '; i++ {
}
return str[i:]
}
func parseLine(line string, maxTagDataLength int, truncateLen int) (ircmsg Message, err error) {
// remove either \n or \r\n from the end of the line:
line = strings.TrimSuffix(line, "\n")
line = strings.TrimSuffix(line, "\r")
// whether we removed them ourselves, or whether they were removed previously,
// they count against the line limit:
if truncateLen != 0 {
if truncateLen <= 2 {
return ircmsg, ErrorLineIsEmpty
}
truncateLen -= 2
}
// now validate for the 3 forbidden bytes:
if strings.IndexByte(line, '\x00') != -1 || strings.IndexByte(line, '\n') != -1 || strings.IndexByte(line, '\r') != -1 {
return ircmsg, ErrorLineContainsBadChar
}
if len(line) < 1 {
return ircmsg, ErrorLineIsEmpty
}
// tags
if line[0] == '@' {
tagEnd := strings.IndexByte(line, ' ')
if tagEnd == -1 {
return ircmsg, ErrorLineIsEmpty
}
tags := line[1:tagEnd]
if 0 < maxTagDataLength && maxTagDataLength < len(tags) {
return ircmsg, ErrorTagsTooLong
}
err = ircmsg.parseTags(tags)
if err != nil {
return
}
// skip over the tags and the separating space
line = line[tagEnd+1:]
}
// truncate if desired
if truncateLen != 0 && truncateLen < len(line) {
err = ErrorBodyTooLong
line = line[:truncateLen]
}
// modern: "These message parts, and parameters themselves, are separated
// by one or more ASCII SPACE characters"
line = trimInitialSpaces(line)
// prefix
if 0 < len(line) && line[0] == ':' {
prefixEnd := strings.IndexByte(line, ' ')
if prefixEnd == -1 {
return ircmsg, ErrorLineIsEmpty
}
ircmsg.Prefix = line[1:prefixEnd]
// skip over the prefix and the separating space
line = line[prefixEnd+1:]
}
line = trimInitialSpaces(line)
// command
commandEnd := strings.IndexByte(line, ' ')
paramStart := commandEnd + 1
if commandEnd == -1 {
commandEnd = len(line)
paramStart = len(line)
}
// normalize command to uppercase:
ircmsg.Command = strings.ToUpper(line[:commandEnd])
if len(ircmsg.Command) == 0 {
return ircmsg, ErrorLineIsEmpty
}
line = line[paramStart:]
for {
line = trimInitialSpaces(line)
if len(line) == 0 {
break
}
// 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
}
ircmsg.Params = append(ircmsg.Params, line[:paramEnd])
line = line[paramEnd+1:]
}
return ircmsg, err
}
// helper to parse tags
func (ircmsg *Message) 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:]
}
// "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) {
return ErrorInvalidTagContent
}
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 Message.
func MakeMessage(tags map[string]string, prefix string, command string, params ...string) (ircmsg Message) {
ircmsg.Prefix = prefix
ircmsg.Command = command
ircmsg.Params = params
ircmsg.UpdateTags(tags)
return ircmsg
}
// Line returns a sendable line created from an Message.
func (ircmsg *Message) 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 created from an Message.
func (ircmsg *Message) LineBytes() (result []byte, err error) {
result, err = ircmsg.line(0, 0, 0, 0)
return
}
// LineBytesStrict returns a sendable line, as a []byte, created from an Message.
// 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 *Message) 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)
}
func paramRequiresTrailing(param string) bool {
return len(param) == 0 || strings.IndexByte(param, ' ') != -1 || param[0] == ':'
}
// line returns a sendable line created from an Message.
func (ircmsg *Message) line(tagLimit, clientOnlyTagDataLimit, serverAddedTagDataLimit, truncateLen int) (result []byte, err error) {
if len(ircmsg.Command) == 0 {
return nil, ErrorCommandMissing
}
var buf bytes.Buffer
// 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) {
var tagError error
buf.WriteByte('@')
firstTag := true
writeTags := func(tags map[string]string) {
for tag, val := range tags {
if !(validateTagName(tag) && validateTagValue(val)) {
tagError = ErrorInvalidTagContent
}
if !firstTag {
buf.WriteByte(';') // delimiter
}
buf.WriteString(tag)
if val != "" {
buf.WriteByte('=')
buf.WriteString(EscapeTagValue(val))
}
firstTag = false
}
}
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.WriteByte(' ')
if tagError != nil {
return nil, tagError
}
}
lenTags = buf.Len()
if 0 < tagLimit && tagLimit < buf.Len() {
return nil, ErrorTagsTooLong
}
if (0 < clientOnlyTagDataLimit && clientOnlyTagDataLimit < lenClientOnlyTags) || (0 < serverAddedTagDataLimit && serverAddedTagDataLimit < lenRegularTags) {
return nil, ErrorTagsTooLong
}
if len(ircmsg.Prefix) > 0 {
buf.WriteByte(':')
buf.WriteString(ircmsg.Prefix)
buf.WriteByte(' ')
}
buf.WriteString(ircmsg.Command)
for i, param := range ircmsg.Params {
buf.WriteByte(' ')
requiresTrailing := paramRequiresTrailing(param)
lastParam := i == len(ircmsg.Params)-1
if (requiresTrailing || ircmsg.forceTrailing) && lastParam {
buf.WriteByte(':')
} else if requiresTrailing && !lastParam {
return nil, ErrorBadParam
}
buf.WriteString(param)
}
// truncate if desired; leave 2 bytes over for \r\n:
if truncateLen != 0 && (truncateLen-2) < (buf.Len()-lenTags) {
err = ErrorBodyTooLong
newBufLen := lenTags + (truncateLen - 2)
buf.Truncate(newBufLen)
// XXX: we may have truncated in the middle of a UTF8-encoded codepoint;
// if so, remove additional bytes, stopping when the sequence either
// ends in a valid codepoint, or we have removed 3 bytes (the maximum
// length of the remnant of a once-valid, truncated codepoint; we don't
// want to truncate the entire message if it wasn't UTF8 in the first
// place).
for i := 0; i < (utf8.UTFMax - 1); i++ {
r, n := utf8.DecodeLastRune(buf.Bytes())
if r == utf8.RuneError && n <= 1 {
newBufLen--
buf.Truncate(newBufLen)
} else {
break
}
}
}
buf.WriteString("\r\n")
result = buf.Bytes()
toValidate := result[:len(result)-2]
if bytes.IndexByte(toValidate, '\x00') != -1 || bytes.IndexByte(toValidate, '\r') != -1 || bytes.IndexByte(toValidate, '\n') != -1 {
return nil, ErrorLineContainsBadChar
}
return result, err
}

103
vendor/github.com/ergochat/irc-go/ircmsg/tags.go generated vendored Normal file
View file

@ -0,0 +1,103 @@
// written by Daniel Oaks <daniel@danieloaks.net>
// released under the ISC license
package ircmsg
import (
"strings"
"unicode/utf8"
)
var (
// valtoescape replaces real characters with message tag escapes.
valtoescape = strings.NewReplacer("\\", "\\\\", ";", "\\:", " ", "\\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
// Message, so you don't need to call it yourself before creating a line.
func EscapeTagValue(inString string) string {
return valtoescape.Replace(inString)
}
// UnescapeTagValue takes an escaped message tag value, and returns the raw value.
//
// 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 {
// 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() == 0 {
buf.Grow(len(inString)) // just an optimization
}
buf.WriteString(remainder[:backslashPos])
buf.WriteByte(escapedCharLookupTable[remainder[backslashPos+1]])
remainder = remainder[backslashPos+2:]
}
return buf.String()
}
// 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)
}

View file

@ -0,0 +1,125 @@
// Copyright (c) 2020-2021 Shivaram Lingamneni
// released under the MIT license
package ircreader
import (
"bytes"
"errors"
"io"
)
/*
Reader is an optimized line reader for IRC lines containing tags;
most IRC lines will not approach the maximum line length (8191 bytes
of tag data, plus 512 bytes of message data), so we want a buffered
reader that can start with a smaller buffer and expand if necessary,
while also maintaining a hard upper limit on the size of the buffer.
*/
var (
ErrReadQ = errors.New("readQ exceeded (read too many bytes without terminating newline)")
)
type Reader struct {
conn io.Reader
initialSize int
maxSize int
buf []byte
start int // start of valid (i.e., read but not yet consumed) data in the buffer
end int // end of valid data in the buffer
searchFrom int // start of valid data in the buffer not yet searched for \n
eof bool
}
// Returns a new *Reader with sane buffer size limits.
func NewIRCReader(conn io.Reader) *Reader {
var reader Reader
reader.Initialize(conn, 512, 8192+1024)
return &reader
}
// "Placement new" for a Reader; initializes it with custom buffer size
// limits.
func (cc *Reader) Initialize(conn io.Reader, initialSize, maxSize int) {
*cc = Reader{}
cc.conn = conn
cc.initialSize = initialSize
cc.maxSize = maxSize
}
// Blocks until a full IRC line is read, then returns it. Accepts either \n
// or \r\n as the line terminator (but not \r in isolation). Passes through
// errors from the underlying connection. Returns ErrReadQ if the buffer limit
// was exceeded without a terminating \n.
func (cc *Reader) ReadLine() ([]byte, error) {
for {
// try to find a terminated line in the buffered data already read
nlidx := bytes.IndexByte(cc.buf[cc.searchFrom:cc.end], '\n')
if nlidx != -1 {
// got a complete line
line := cc.buf[cc.start : cc.searchFrom+nlidx]
cc.start = cc.searchFrom + nlidx + 1
cc.searchFrom = cc.start
// treat \r\n as the line terminator if it was present
if 0 < len(line) && line[len(line)-1] == '\r' {
line = line[:len(line)-1]
}
return line, nil
}
// are we out of space? we can read more if any of these are true:
// 1. cc.start != 0, so we can slide the existing data back
// 2. cc.end < len(cc.buf), so we can read data into the end of the buffer
// 3. len(cc.buf) < cc.maxSize, so we can grow the buffer
if cc.start == 0 && cc.end == len(cc.buf) && len(cc.buf) == cc.maxSize {
return nil, ErrReadQ
}
if cc.eof {
return nil, io.EOF
}
if len(cc.buf) < cc.maxSize && (len(cc.buf)-(cc.end-cc.start) < cc.initialSize/2) {
// allocate a new buffer, copy any remaining data
newLen := roundUpToPowerOfTwo(len(cc.buf) + 1)
if newLen > cc.maxSize {
newLen = cc.maxSize
} else if newLen < cc.initialSize {
newLen = cc.initialSize
}
newBuf := make([]byte, newLen)
copy(newBuf, cc.buf[cc.start:cc.end])
cc.buf = newBuf
} else if cc.start != 0 {
// slide remaining data back to the front of the buffer
copy(cc.buf, cc.buf[cc.start:cc.end])
}
cc.end = cc.end - cc.start
cc.start = 0
cc.searchFrom = cc.end
n, err := cc.conn.Read(cc.buf[cc.end:])
cc.end += n
if n != 0 && err == io.EOF {
// we may have received new \n-terminated lines, try to parse them
cc.eof = true
} else if err != nil {
return nil, err
}
}
}
// return n such that v <= n and n == 2**i for some i
func roundUpToPowerOfTwo(v int) int {
// http://graphics.stanford.edu/~seander/bithacks.html
v -= 1
v |= v >> 1
v |= v >> 2
v |= v >> 4
v |= v >> 8
v |= v >> 16
return v + 1
}

9
vendor/github.com/ergochat/irc-go/ircutils/doc.go generated vendored Normal file
View file

@ -0,0 +1,9 @@
// written by Daniel Oaks <daniel@danieloaks.net>
// released under the ISC license
/*
Package ircutils provides small, useful utility functions and classes.
This package is in an alpha stage.
*/
package ircutils

View file

@ -0,0 +1,41 @@
// written by Daniel Oaks <daniel@danieloaks.net>
// released under the ISC license
package ircutils
import "strings"
var allowedHostnameChars = "abcdefghijklmnopqrstuvwxyz1234567890-."
// HostnameIsValid provides a way for servers to check whether a looked-up client
// hostname is valid (see InspIRCd #1033 for why this is required).
//
// This function shouldn't be called by clients since they don't need to validate
// hostnames for IRC use, just by servers that need to confirm hostnames of incoming
// clients.
//
// In addition to this function, servers should impose their own limits on max
// hostname length -- this function limits it to 200 but most servers will probably
// want to make it smaller than that.
func HostnameIsValid(hostname string) bool {
// IRC hostnames specifically require a period, rough limit of 200 chars
if !strings.Contains(hostname, ".") || len(hostname) < 1 || len(hostname) > 200 {
return false
}
// ensure each part of hostname is valid
for _, part := range strings.Split(hostname, ".") {
if len(part) < 1 || len(part) > 63 || strings.HasPrefix(part, "-") || strings.HasSuffix(part, "-") {
return false
}
}
// ensure all chars of hostname are valid
for _, char := range strings.Split(strings.ToLower(hostname), "") {
if !strings.Contains(allowedHostnameChars, char) {
return false
}
}
return true
}

62
vendor/github.com/ergochat/irc-go/ircutils/unicode.go generated vendored Normal file
View file

@ -0,0 +1,62 @@
// Copyright (c) 2021 Shivaram Lingamneni
// Released under the MIT License
package ircutils
import (
"strings"
"unicode"
"unicode/utf8"
)
// truncate a message, taking care not to make valid UTF8 into invalid UTF8
func TruncateUTF8Safe(message string, byteLimit int) (result string) {
if len(message) <= byteLimit {
return message
}
message = message[:byteLimit]
for i := 0; i < (utf8.UTFMax - 1); i++ {
r, n := utf8.DecodeLastRuneInString(message)
if r == utf8.RuneError && n <= 1 {
message = message[:len(message)-1]
} else {
break
}
}
return message
}
// Sanitizes human-readable text to make it safe for IRC;
// assumes UTF-8 and uses the replacement character where
// applicable.
func SanitizeText(message string, byteLimit int) (result string) {
var buf strings.Builder
for _, r := range message {
if r == '\x00' || r == '\r' {
continue
} else if r == '\n' {
if buf.Len()+2 <= byteLimit {
buf.WriteString(" ")
continue
} else {
break
}
} else if unicode.IsSpace(r) {
if buf.Len()+1 <= byteLimit {
buf.WriteString(" ")
} else {
break
}
} else {
rLen := utf8.RuneLen(r)
if buf.Len()+rLen <= byteLimit {
buf.WriteRune(r)
} else {
break
}
}
}
return buf.String()
}

56
vendor/github.com/ergochat/irc-go/ircutils/userhost.go generated vendored Normal file
View file

@ -0,0 +1,56 @@
// written by Daniel Oaks <daniel@danieloaks.net>
// released under the ISC license
package ircutils
import "strings"
// UserHost holds a username+host combination
type UserHost struct {
Nick string
User string
Host string
}
// ParseUserhost takes a userhost string and returns a UserHost instance.
func ParseUserhost(userhost string) UserHost {
var uh UserHost
if len(userhost) == 0 {
return uh
}
if strings.Contains(userhost, "!") {
usersplit := strings.SplitN(userhost, "!", 2)
var rest string
if len(usersplit) == 2 {
uh.Nick = usersplit[0]
rest = usersplit[1]
} else {
rest = usersplit[0]
}
hostsplit := strings.SplitN(rest, "@", 2)
if len(hostsplit) == 2 {
uh.User = hostsplit[0]
uh.Host = hostsplit[1]
} else {
uh.User = hostsplit[0]
}
} else {
hostsplit := strings.SplitN(userhost, "@", 2)
if len(hostsplit) == 2 {
uh.Nick = hostsplit[0]
uh.Host = hostsplit[1]
} else {
uh.User = hostsplit[0]
}
}
return uh
}
// // Canonical returns the canonical string representation of the userhost.
// func (uh *UserHost) Canonical() string {
// return ""
// }