198 lines
3.5 KiB
Go
198 lines
3.5 KiB
Go
|
package ircdecoder
|
||
|
|
||
|
import (
|
||
|
"errors"
|
||
|
"fmt"
|
||
|
"io"
|
||
|
"strings"
|
||
|
"unicode/utf8"
|
||
|
|
||
|
"tuxpa.in/a/irc/pkg/ircv3"
|
||
|
)
|
||
|
|
||
|
type Decoder struct {
|
||
|
}
|
||
|
|
||
|
func (d *Decoder) readByte(r io.Reader) (byte, error) {
|
||
|
var o [1]byte
|
||
|
if c, ok := r.(io.ByteReader); ok {
|
||
|
return c.ReadByte()
|
||
|
}
|
||
|
_, err := io.ReadFull(r, o[:])
|
||
|
if err != nil {
|
||
|
return 0, err
|
||
|
}
|
||
|
return o[0], nil
|
||
|
}
|
||
|
|
||
|
// read a message from the stream
|
||
|
func (d *Decoder) Decode(r io.Reader, msg *ircv3.Message) error {
|
||
|
return d.decode(r, msg)
|
||
|
}
|
||
|
|
||
|
func (d *Decoder) decodeTags(r io.Reader, msg *ircv3.Message) error {
|
||
|
// we assume we have already read the @
|
||
|
if msg.Tags == nil {
|
||
|
msg.Tags = make(ircv3.Tags)
|
||
|
}
|
||
|
|
||
|
kb := new(strings.Builder)
|
||
|
vb := new(strings.Builder)
|
||
|
readingValue := false
|
||
|
for {
|
||
|
// keep reading until space
|
||
|
b, err := d.readByte(r)
|
||
|
if err != nil {
|
||
|
return err
|
||
|
}
|
||
|
if b == ';' {
|
||
|
msg.Tags.Set(kb.String(), vb.String())
|
||
|
readingValue = false
|
||
|
continue
|
||
|
} else if b == '=' {
|
||
|
readingValue = true
|
||
|
continue
|
||
|
}
|
||
|
if b == 0x20 {
|
||
|
kstr := kb.String()
|
||
|
if !utf8.ValidString(kstr) {
|
||
|
return fmt.Errorf("non utf-8 tag key")
|
||
|
}
|
||
|
msg.Tags.Set(kstr, ircv3.UnescapeTagValue(vb.String()))
|
||
|
readingValue = false
|
||
|
break
|
||
|
}
|
||
|
if readingValue {
|
||
|
vb.WriteByte(b)
|
||
|
} else {
|
||
|
// TODO: technically we should check the validity of key bytes, not scan for utf8 at the end
|
||
|
// <key_name> ::= <non-empty sequence of ascii letters, digits, hyphens ('-')>
|
||
|
kb.WriteByte(b)
|
||
|
}
|
||
|
}
|
||
|
return nil
|
||
|
}
|
||
|
|
||
|
func (d *Decoder) decodeSource(r io.Reader, msg *ircv3.Message) error {
|
||
|
// we assume we have already read the :
|
||
|
buf := new(strings.Builder)
|
||
|
for {
|
||
|
// keep reading until space
|
||
|
b, err := d.readByte(r)
|
||
|
if err != nil {
|
||
|
return err
|
||
|
}
|
||
|
if b == 0x20 {
|
||
|
break
|
||
|
}
|
||
|
buf.WriteByte(b)
|
||
|
}
|
||
|
nuh, err := ircv3.ParseNUH(buf.String())
|
||
|
if err != nil {
|
||
|
return err
|
||
|
}
|
||
|
msg.Source = &nuh
|
||
|
return nil
|
||
|
}
|
||
|
|
||
|
// read a message from the stream
|
||
|
func (d *Decoder) decode(r io.Reader, msg *ircv3.Message) error {
|
||
|
b, err := d.readByte(r)
|
||
|
if err != nil {
|
||
|
return err
|
||
|
}
|
||
|
|
||
|
switch b {
|
||
|
case '@':
|
||
|
if err := d.decodeTags(r, msg); err != nil {
|
||
|
return err
|
||
|
}
|
||
|
b, err = d.readByte(r)
|
||
|
if err != nil {
|
||
|
return err
|
||
|
}
|
||
|
if b == ':' {
|
||
|
if err := d.decodeSource(r, msg); err != nil {
|
||
|
return err
|
||
|
}
|
||
|
b, err = d.readByte(r)
|
||
|
if err != nil {
|
||
|
return err
|
||
|
}
|
||
|
}
|
||
|
case ':':
|
||
|
if err := d.decodeSource(r, msg); err != nil {
|
||
|
return err
|
||
|
}
|
||
|
b, err = d.readByte(r)
|
||
|
if err != nil {
|
||
|
return err
|
||
|
}
|
||
|
default:
|
||
|
}
|
||
|
cb := new(strings.Builder)
|
||
|
// at this point we've no matter waht read the first byte of the command in b
|
||
|
cb.WriteByte(b)
|
||
|
// add a limit reader for the irc size limit
|
||
|
r = io.LimitReader(r, 511)
|
||
|
// read until first space
|
||
|
for {
|
||
|
b, err := d.readByte(r)
|
||
|
if err != nil {
|
||
|
if errors.Is(err, io.EOF) {
|
||
|
msg.Command = cb.String()
|
||
|
return nil
|
||
|
}
|
||
|
return err
|
||
|
}
|
||
|
if b == 0x20 {
|
||
|
break
|
||
|
}
|
||
|
cb.WriteByte(b)
|
||
|
}
|
||
|
msg.Command = cb.String()
|
||
|
cb.Reset()
|
||
|
// now read the params
|
||
|
|
||
|
var trailing bool
|
||
|
var lastCr bool
|
||
|
for {
|
||
|
b, err := d.readByte(r)
|
||
|
if err != nil {
|
||
|
if errors.Is(err, io.EOF) {
|
||
|
if cb.Len() > 0 {
|
||
|
msg.Params = append(msg.Params, cb.String())
|
||
|
}
|
||
|
return nil
|
||
|
}
|
||
|
return err
|
||
|
}
|
||
|
if cb.Len() == 0 {
|
||
|
if b == ':' {
|
||
|
trailing = true
|
||
|
continue
|
||
|
}
|
||
|
}
|
||
|
if !trailing {
|
||
|
if b == 0x20 {
|
||
|
msg.Params = append(msg.Params, cb.String())
|
||
|
cb.Reset()
|
||
|
continue
|
||
|
}
|
||
|
}
|
||
|
if b == '\r' {
|
||
|
lastCr = true
|
||
|
continue
|
||
|
}
|
||
|
if lastCr {
|
||
|
if b == '\n' {
|
||
|
msg.Params = append(msg.Params, cb.String())
|
||
|
return nil
|
||
|
} else {
|
||
|
cb.WriteByte('\r')
|
||
|
}
|
||
|
}
|
||
|
cb.WriteByte(b)
|
||
|
}
|
||
|
}
|