package xturbo import ( "encoding/binary" "errors" "io" "os" "golang.org/x/sys/unix" ) // Seq identifies a request; zero is invalid. type Seq uint64 var ( ErrWouldBlock = errors.New("xturbo: write would block") ErrClosed = errors.New("xturbo: connection closed") ErrRequestTooLarge = errors.New("xturbo: request too large") ErrExtMissing = errors.New("xturbo: extension missing") ErrBadLength = errors.New("xturbo: bad length") ErrSeqOverflow = errors.New("xturbo: sequence overflow") ) const ( defaultBufSize = 64 << 10 // coreMaxReq is the largest core request in bytes (0xFFFF four-byte units). coreMaxReq = 0xffff * 4 // maxSeqAhead bounds unacknowledged requests so 16-bit wire sequence // numbers widen unambiguously. maxSeqAhead = 0xffff ) // Buf is the caller-owned request encoder. Conn owns flushing and // acknowledgment. It is not safe for concurrent use. type Buf struct { Ext *ExtTable b []byte flushed int next, acked Seq maxReq uint32 err error } // NewBuf returns an encoder with size bytes preallocated; size <= 0 means 64 KiB. func NewBuf(size int) *Buf { if size <= 0 { size = defaultBufSize } return &Buf{b: make([]byte, 0, size), next: 1, maxReq: coreMaxReq} } // Fail records err as the sticky error; the first error wins. func (b *Buf) Fail(err error) { if b.err == nil { b.err = err } } func (b *Buf) Err() error { return b.err } // Reset drops unflushed bytes and keeps sequence counters. Tests only. func (b *Buf) Reset() { b.b = b.b[:0]; b.flushed = 0 } // Bytes returns the unflushed bytes. func (b *Buf) Bytes() []byte { return b.b[b.flushed:] } func (b *Buf) Len() int { return len(b.b) - b.flushed } // Next returns the sequence number the next Request will get. func (b *Buf) Next() Seq { return b.next } // Request reserves a zeroed body of body bytes after the wire header and // returns it with the request's sequence number. body must be a multiple of 4. // Requests longer than 0xFFFF units get the BIG-REQUESTS 8-byte header. // On failure it records a sticky error and returns (nil, 0). // The body is valid only until the next Request, flush or Reset on b. func (b *Buf) Request(major, data uint8, body int) ([]byte, Seq) { if b.err != nil { return nil, 0 } if body < 0 || body&3 != 0 { b.Fail(ErrBadLength) return nil, 0 } header := 4 if uint64(body)+4 > coreMaxReq { header = 8 } if uint64(body)+uint64(header) > uint64(b.maxReq) { b.Fail(ErrRequestTooLarge) return nil, 0 } if b.next-b.acked >= maxSeqAhead { b.Fail(ErrSeqOverflow) return nil, 0 } total := body + header b.reserve(total) start := len(b.b) b.b = b.b[:start+total] p := b.b[start:] clear(p) p[0] = major p[1] = data if header == 4 { binary.LittleEndian.PutUint16(p[2:], uint16(total/4)) } else { binary.LittleEndian.PutUint32(p[4:], uint32(total/4)) } seq := b.next b.next++ return p[header:], seq } // reserve makes room for n more bytes. Flushed bytes are dropped first; the // buffer then doubles until n fits. It never shrinks. func (b *Buf) reserve(n int) { if n <= cap(b.b)-len(b.b) { return } pending := len(b.b) - b.flushed if b.flushed > 0 && n <= cap(b.b)-pending { copy(b.b, b.b[b.flushed:]) b.b = b.b[:pending] b.flushed = 0 return } capacity := max(cap(b.b), 1) for capacity-pending < n { capacity *= 2 } grown := make([]byte, pending, capacity) copy(grown, b.b[b.flushed:]) b.b = grown b.flushed = 0 } // setMaxReqWords sets the request size limit from the BIG-REQUESTS // maximum_request_length (four-byte units). Conn calls it after enabling it. func (b *Buf) setMaxReqWords(words uint32) { b.maxReq = uint32(min(uint64(words)*4, 0xfffffffc)) } // ack records seq as the highest sequence number seen on the read side. func (b *Buf) ack(seq Seq) { if seq > b.acked && seq < b.next { b.acked = seq } } // flush writes unflushed bytes to the non-blocking fd. It returns // ErrWouldBlock when the socket is full (the remainder stays queued) and nil // once everything is written. Any other failure becomes the sticky error. // Conn.Flush wraps it. func (b *Buf) flush(fd int) error { if b.err != nil { return b.err } for b.flushed < len(b.b) { n, err := unix.Write(fd, b.b[b.flushed:]) switch { case err == unix.EINTR: continue case err == unix.EAGAIN: return ErrWouldBlock case err == unix.EPIPE || err == unix.ECONNRESET: b.Fail(ErrClosed) return b.err case err != nil: b.Fail(os.NewSyscallError("write", err)) return b.err case n <= 0: b.Fail(io.ErrShortWrite) return b.err } if poison { fillPoison(b.b[b.flushed : b.flushed+n]) } b.flushed += n } b.b = b.b[:0] b.flushed = 0 return nil }