open-nomad/helper/escapingio/reader.go
Mahmood Ali b02852ef62 Add a escaping reader that mimics ssh behavior
Adds an escaping reading that mimics ssh handling of input escape
sequences.

The reader parses chunks to look for \n~
2019-05-16 16:22:52 -04:00

164 lines
3.8 KiB
Go

package escapingio
import (
"io"
)
// Handler is a callback for handling an escaped char. Reader would skip
// the escape char and passed char if returns true; otherwise, it preserves them
// in output
type Handler func(c byte) bool
// NewReader returns a reader that escapes the c character (following new lines),
// in the same manner OpenSSH handling, which defaults to `~`.
//
// For illustrative purposes, we use `~` in documentation as a shorthand for escaping character.
//
// If following a new line, reader sees:
// * `~~`, only one is emitted
// * `~.` (or any character), the handler is invoked with the character.
// If handler returns true, `~.` will be skipped; otherwise, it's propagated.
// * `~` and it's the last character in stream, it's propagated
//
// Appearances of `~` when not followed by a new line is propagated unmodified.
func NewReader(r io.Reader, c byte, h Handler) io.Reader {
return &reader{
impl: r,
escapeChar: c,
state: sLookEscapeChar,
handler: h,
}
}
// lookState represents the state of reader for what character of `\n~.` sequence
// reader is looking for
type lookState int
const (
// sLookNewLine indicates that reader is looking for new line
sLookNewLine lookState = iota
// sLookEscapeChar indicates that reader is looking for ~
sLookEscapeChar
// sLookChar indicates that reader just read `~` is waiting for next character
// before acting
sLookChar
)
// to ease comments, i'll assume escape character to be `~`
type reader struct {
impl io.Reader
escapeChar uint8
handler Handler
state lookState
// unread is a buffered character for next read if not-nil
unread *byte
}
func (r *reader) Read(buf []byte) (int, error) {
START:
var n int
var err error
if r.unread != nil {
// try to return the unread character immediately
// without trying to block for another read
buf[0] = *r.unread
n = 1
r.unread = nil
} else {
n, err = r.impl.Read(buf)
}
// when we get to the end, check if we have any unprocessed \n~
if n == 0 && err != nil {
if r.state == sLookChar && err != nil {
buf[0] = r.escapeChar
n = 1
}
return n, err
}
// inspect the state at beginning of read
if r.state == sLookChar {
r.state = sLookNewLine
// escape character hasn't been emitted yet
if buf[0] == r.escapeChar {
// earlier ~ was sallowed already, so leave this as is
} else if handled := r.handler(buf[0]); handled {
// need to drop a single letter
copy(buf, buf[1:n])
n--
} else {
// we need to re-introduce ~ with rest of body
// but be mindful if reintroducing ~ causes buffer to overflow
if n == len(buf) {
// in which case, save it for next read
c := buf[n-1]
r.unread = &c
copy(buf[1:], buf[:n])
buf[0] = r.escapeChar
} else {
copy(buf[1:], buf[:n])
buf[0] = r.escapeChar
n++
}
}
}
n = r.processBuffer(buf, n)
if n == 0 && err == nil {
goto START
}
return n, err
}
// handles escaped character inside body of read buf.
func (r *reader) processBuffer(buf []byte, read int) int {
b := 0
for b < read {
c := buf[b]
if r.state == sLookEscapeChar && r.escapeChar == c {
r.state = sLookEscapeChar
// are we at the end of read; wait for next read
if b == read-1 {
read--
r.state = sLookChar
return read
}
// otherwise peek at next
nc := buf[b+1]
if nc == r.escapeChar {
// repeated ~, only emit one - skip one character
copy(buf[b:], buf[b+1:read])
read--
b++
continue
} else if handled := r.handler(nc); handled {
// need to drop both ~ and letter
copy(buf[b:], buf[b+2:read])
read -= 2
continue
} else {
// need to pass output unmodified with ~ and letter
}
} else if c == '\n' || c == '\r' {
r.state = sLookEscapeChar
} else {
r.state = sLookNewLine
}
b++
}
return read
}