123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125 |
- // 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
- }
|