github.com/Ilhicas/nomad@v1.0.4-0.20210304152020-e86851182bc3/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  						if rb[0] == '\n' || rb[0] == '\r' {
    97  							state = sLookEscapeChar
    98  						}
    99  					}
   100  				}
   101  			}
   102  		}
   103  
   104  		if err != nil {
   105  			// write ~ if it's the last thing
   106  			if state == sLookChar {
   107  				bw.WriteByte(r.escapeChar)
   108  			}
   109  			bw.Flush()
   110  			r.pw.CloseWithError(err)
   111  			break
   112  		}
   113  	}
   114  }
   115  
   116  // processBuf process buffer and emits all output to writer
   117  // if the last part of buffer is a new line followed by sequnce, it writes
   118  // all output until the new line and returns sLookChar
   119  func (r *reader) processBuf(bw io.Writer, buf []byte, n int, s lookState) lookState {
   120  	i := 0
   121  
   122  	wi := 0
   123  
   124  START:
   125  	if s == sLookEscapeChar && buf[i] == r.escapeChar {
   126  		if i+1 >= n {
   127  			// buf terminates with ~ - write all before
   128  			bw.Write(buf[wi:i])
   129  			return sLookChar
   130  		}
   131  
   132  		nc := buf[i+1]
   133  		if nc == r.escapeChar {
   134  			// skip one escape char
   135  			bw.Write(buf[wi:i])
   136  			i++
   137  			wi = i
   138  		} else if r.handler(nc) {
   139  			// skip both characters
   140  			bw.Write(buf[wi:i])
   141  			i = i + 2
   142  			wi = i
   143  		} else if nc == '\n' || nc == '\r' {
   144  			i = i + 2
   145  			s = sLookEscapeChar
   146  			goto START
   147  		} else {
   148  			i = i + 2
   149  			// need to write everything keep going
   150  		}
   151  	}
   152  
   153  	// search until we get \n~, or buf terminates
   154  	for {
   155  		if i >= n {
   156  			// got to end without new line, write and return
   157  			bw.Write(buf[wi:n])
   158  			return sLookNewLine
   159  		}
   160  
   161  		if buf[i] == '\n' || buf[i] == '\r' {
   162  			// buf terminated at new line
   163  			if i+1 >= n {
   164  				bw.Write(buf[wi:n])
   165  				return sLookEscapeChar
   166  			}
   167  
   168  			// peek to see escape character go back to START if so
   169  			if buf[i+1] == r.escapeChar {
   170  				s = sLookEscapeChar
   171  				i++
   172  				goto START
   173  			}
   174  		}
   175  
   176  		i++
   177  	}
   178  }