github.com/oweisse/u-root@v0.0.0-20181109060735-d005ad25fef1/pkg/lineio/lineio.go (about)

     1  // Copyright 2018 the u-root Authors. All rights reserved
     2  // Use of this source code is governed by a BSD-style
     3  // license that can be found in the LICENSE file.
     4  
     5  package lineio
     6  
     7  import (
     8  	"io"
     9  	"regexp"
    10  
    11  	"github.com/u-root/u-root/pkg/sortedmap"
    12  )
    13  
    14  type LineReader struct {
    15  	src io.ReaderAt
    16  
    17  	// offsetCache remembers the offset of various lines in src.
    18  	// At minimum, line 1 must be prepopulated.
    19  	offsetCache sortedmap.Map
    20  }
    21  
    22  // scanForLine reads from curOffset (which is on curLine), looking for line,
    23  // returning the offset of line.  If err == io.EOF, offset is the offset of
    24  // the last valid byte.
    25  func (l *LineReader) scanForLine(line, curLine, curOffset int64) (offset int64, err error) {
    26  	lastGoodOffset := int64(-1)
    27  
    28  	for {
    29  		buf := make([]byte, 128)
    30  
    31  		n, err := l.src.ReadAt(buf, curOffset)
    32  		// Keep looking as long as *something* is returned
    33  		if n == 0 && err != nil {
    34  			// In the event of EOF, callers want to know the last
    35  			// byte read, to find the last byte in the last line.
    36  			return lastGoodOffset, err
    37  		}
    38  
    39  		buf = buf[:n]
    40  
    41  		for i, b := range buf {
    42  			if b != '\n' {
    43  				continue
    44  			}
    45  
    46  			offset := curOffset + int64(i) + 1
    47  
    48  			// We haven't read this offset yet; it may not exist.
    49  			// Read-ahead by a byte to double-check this offset
    50  			// exists before adding a new line.
    51  			// This is a common case for files ending in a newline.
    52  			if offset >= curOffset+int64(len(buf)) {
    53  				t := make([]byte, 1)
    54  				_, err = l.src.ReadAt(t, offset)
    55  				if err != nil {
    56  					continue
    57  				}
    58  			}
    59  
    60  			curLine++
    61  
    62  			l.offsetCache.Insert(curLine, offset)
    63  
    64  			if curLine == line {
    65  				return offset, nil
    66  			}
    67  		}
    68  
    69  		curOffset += int64(len(buf))
    70  		// The last byte in the buffer must have been good if we read it.
    71  		lastGoodOffset = curOffset - 1
    72  	}
    73  }
    74  
    75  // Populate scans the file, populating the offsetCache, so that future
    76  // lookups will be faster.
    77  func (l *LineReader) Populate() {
    78  	// Scan from the start of the file to the maximum possible line,
    79  	// populating the offsetCache along the way.
    80  	l.scanForLine(int64(0x7fffffffffffffff), 1, 0)
    81  }
    82  
    83  // findLine returns the offset of start of line.
    84  func (l *LineReader) findLine(line int64) (offset int64, err error) {
    85  	nearest, offset, err := l.offsetCache.NearestLessEqual(line)
    86  	if err != nil {
    87  		return 0, err
    88  	}
    89  
    90  	// Is this the line we want?
    91  	if nearest == line {
    92  		return offset, nil
    93  	}
    94  
    95  	return l.scanForLine(line, nearest, offset)
    96  }
    97  
    98  // findLineRange returns the offset of the first and last bytes in line.
    99  func (l *LineReader) findLineRange(line int64) (start, end int64, err error) {
   100  	start, err = l.findLine(line)
   101  	if err != nil {
   102  		return 0, 0, err
   103  	}
   104  
   105  	end, err = l.findLine(line + 1)
   106  	// EOF means there is no next line.  End is the last byte in the file,
   107  	// if it is positive.
   108  	if err == io.EOF && end >= 0 {
   109  		return start, end, nil
   110  	} else if err != nil {
   111  		return 0, 0, err
   112  	}
   113  
   114  	// The caller expects end to be the last character in the line,
   115  	// but findLine returns the start of the next line.  Subtract
   116  	// first character in next line and newline at end of previous line.
   117  	end -= 2
   118  
   119  	return start, end, nil
   120  }
   121  
   122  // LineExists returns true if the given line is in the file.
   123  func (l *LineReader) LineExists(line int64) bool {
   124  	_, err := l.findLine(line)
   125  	return err == nil
   126  }
   127  
   128  // ReadLine reads up to len(p) bytes from line number line from the source.
   129  // It returns the numbers of bytes written and any error encountered.
   130  // If n < len(p), err is set to a non-nil value explaining why.
   131  // See io.ReaderAt for full description of return values.
   132  func (l *LineReader) ReadLine(p []byte, line int64) (n int, err error) {
   133  	start, end, err := l.findLineRange(line)
   134  	if err != nil {
   135  		return 0, err
   136  	}
   137  
   138  	var shrunk bool
   139  	// Only read one line worth of data.
   140  	size := end - start + 1
   141  	if size < int64(len(p)) {
   142  		p = p[:size]
   143  		shrunk = true
   144  	}
   145  
   146  	n, err = l.src.ReadAt(p, start)
   147  	// We used less than len(p), we must return EOF.
   148  	if err == nil && shrunk {
   149  		err = io.EOF
   150  	}
   151  
   152  	return n, err
   153  }
   154  
   155  // SearchLine runs Regexp.FindAllIndex on the given line, providing the same
   156  // return value.
   157  func (l *LineReader) SearchLine(r *regexp.Regexp, line int64) ([][]int, error) {
   158  	start, end, err := l.findLineRange(line)
   159  	if err != nil {
   160  		return nil, err
   161  	}
   162  
   163  	size := end - start + 1
   164  	buf := make([]byte, size)
   165  
   166  	_, err = l.src.ReadAt(buf, start)
   167  	// TODO(prattmic): support partial reads
   168  	if err != nil {
   169  		return nil, err
   170  	}
   171  
   172  	return r.FindAllIndex(buf, -1), nil
   173  }
   174  
   175  func NewLineReader(src io.ReaderAt) LineReader {
   176  	l := LineReader{
   177  		src:         src,
   178  		offsetCache: sortedmap.NewMap(),
   179  	}
   180  
   181  	// Line 1 starts at the beginning of the file!
   182  	l.offsetCache.Insert(1, 0)
   183  
   184  	return l
   185  }