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 // ::= 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) } }