github.com/banmanh482/nomad@v0.11.8/helper/escapingio/reader.go (about)

     1  package escapingio
     2  
     3  import (
     4  	"bufio"
     5  	"io"
     6  )
     7  
     8  // Handler is a callback for handling an escaped char.  Reader would skip
     9  // the escape char and passed char if returns true; otherwise, it preserves them
    10  // in output
    11  type Handler func(c byte) bool
    12  
    13  // NewReader returns a reader that escapes the c character (following new lines),
    14  // in the same manner OpenSSH handling, which defaults to `~`.
    15  //
    16  // For illustrative purposes, we use `~` in documentation as a shorthand for escaping character.
    17  //
    18  // If following a new line, reader sees:
    19  //  * `~~`, only one is emitted
    20  //  * `~.` (or any character), the handler is invoked with the character.
    21  //     If handler returns true, `~.` will be skipped; otherwise, it's propagated.
    22  //  * `~` and it's the last character in stream, it's propagated
    23  //
    24  // Appearances of `~` when not preceded by a new line are propagated unmodified.
    25  func NewReader(r io.Reader, c byte, h Handler) io.Reader {
    26  	pr, pw := io.Pipe()
    27  	reader := &reader{
    28  		impl:       r,
    29  		escapeChar: c,
    30  		handler:    h,
    31  		pr:         pr,
    32  		pw:         pw,
    33  	}
    34  	go reader.pipe()
    35  	return reader
    36  }
    37  
    38  // lookState represents the state of reader for what character of `\n~.` sequence
    39  // reader is looking for
    40  type lookState int
    41  
    42  const (
    43  	// sLookNewLine indicates that reader is looking for new line
    44  	sLookNewLine lookState = iota
    45  
    46  	// sLookEscapeChar indicates that reader is looking for ~
    47  	sLookEscapeChar
    48  
    49  	// sLookChar indicates that reader just read `~` is waiting for next character
    50  	// before acting
    51  	sLookChar
    52  )
    53  
    54  // to ease comments, i'll assume escape character to be `~`
    55  type reader struct {
    56  	impl       io.Reader
    57  	escapeChar uint8
    58  	handler    Handler
    59  
    60  	// buffers
    61  	pw *io.PipeWriter
    62  	pr *io.PipeReader
    63  }
    64  
    65  func (r *reader) Read(buf []byte) (int, error) {
    66  	return r.pr.Read(buf)
    67  }
    68  
    69  func (r *reader) pipe() {
    70  	rb := make([]byte, 4096)
    71  	bw := bufio.NewWriter(r.pw)
    72  
    73  	state := sLookEscapeChar
    74  
    75  	for {
    76  		n, err := r.impl.Read(rb)
    77  
    78  		if n > 0 {
    79  			state = r.processBuf(bw, rb, n, state)
    80  			bw.Flush()
    81  			if state == sLookChar {
    82  				// terminated with ~ - let's read one more character
    83  				n, err = r.impl.Read(rb[:1])
    84  				if n == 1 {
    85  					state = sLookNewLine
    86  					if rb[0] == r.escapeChar {
    87  						// only emit escape character once
    88  						bw.WriteByte(rb[0])
    89  						bw.Flush()
    90  					} else if r.handler(rb[0]) {
    91  						// skip if handled
    92  					} else {
    93  						bw.WriteByte(r.escapeChar)
    94  						bw.WriteByte(rb[0])
    95  						bw.Flush()
    96  					}
    97  				}
    98  			}
    99  		}
   100  
   101  		if err != nil {
   102  			// write ~ if it's the last thing
   103  			if state == sLookChar {
   104  				bw.WriteByte(r.escapeChar)
   105  			}
   106  			bw.Flush()
   107  			r.pw.CloseWithError(err)
   108  			break
   109  		}
   110  	}
   111  }
   112  
   113  // processBuf process buffer and emits all output to writer
   114  // if the last part of buffer is a new line followed by sequnce, it writes
   115  // all output until the new line and returns sLookChar
   116  func (r *reader) processBuf(bw io.Writer, buf []byte, n int, s lookState) lookState {
   117  	i := 0
   118  
   119  	wi := 0
   120  
   121  START:
   122  	if s == sLookEscapeChar && buf[i] == r.escapeChar {
   123  		if i+1 >= n {
   124  			// buf terminates with ~ - write all before
   125  			bw.Write(buf[wi:i])
   126  			return sLookChar
   127  		}
   128  
   129  		nc := buf[i+1]
   130  		if nc == r.escapeChar {
   131  			// skip one escape char
   132  			bw.Write(buf[wi:i])
   133  			i++
   134  			wi = i
   135  		} else if r.handler(nc) {
   136  			// skip both characters
   137  			bw.Write(buf[wi:i])
   138  			i = i + 2
   139  			wi = i
   140  		} else {
   141  			i = i + 2
   142  			// need to write everything keep going
   143  		}
   144  	}
   145  
   146  	// search until we get \n~, or buf terminates
   147  	for {
   148  		if i >= n {
   149  			// got to end without new line, write and return
   150  			bw.Write(buf[wi:n])
   151  			return sLookNewLine
   152  		}
   153  
   154  		if buf[i] == '\n' || buf[i] == '\r' {
   155  			// buf terminated at new line
   156  			if i+1 >= n {
   157  				bw.Write(buf[wi:n])
   158  				return sLookEscapeChar
   159  			}
   160  
   161  			// peek to see escape character go back to START if so
   162  			if buf[i+1] == r.escapeChar {
   163  				s = sLookEscapeChar
   164  				i++
   165  				goto START
   166  			}
   167  		}
   168  
   169  		i++
   170  	}
   171  }