Files
bluekeyes-go-gitdiff/gitdiff/io.go
T
Billy Keyes f3702115a6 Add LineReaderAt interface and implementation
This is a line-oriented parallel to io.ReaderAt, meant for text applies.
While the mapping isn't quite as clean as in the binary case, a text
apply still reads a fixed chunk of lines starting at a specific line
number and modifies them. This also allows a consistent interface for
strict and fuzzy applies.

The implementation wraps an io.ReaderAt and reads data in chunks,
indexing line boundaries as it goes. This is probably not the most
efficient way to implement this interface, but it works and allows file
application to take a consistent interface.
2020-01-25 16:56:32 -08:00

212 lines
5.9 KiB
Go

package gitdiff
import (
"bufio"
"errors"
"fmt"
"io"
)
// StringReader is the interface that wraps the ReadString method.
//
// ReadString reads until the first occurrence of delim in the input, returning
// a string containing the data up to and including the delimiter. If
// ReadString encounters an error before finding a delimiter, it returns the
// data read before the error and the error itself (often io.EOF). ReadString
// returns err != nil if and only if the returned data does not end in delim.
type StringReader interface {
ReadString(delim byte) (string, error)
}
type readStringReader interface {
io.Reader
StringReader
}
// LineReader is the interface that wraps the ReadLine method.
//
// ReadLine reads the next full line in the input, returing the the data
// including the line ending character(s) and the zero-indexed line number. If
// ReadLine encounters an error before reaching the end of the line, it returns
// the data read before the error, the number of the line, and the error itself
// (often io.EOF). ReadLine returns err != nil if and only if the returned data
// is not a complete line.
//
// If an implementation defines other methods for reading the same input, line
// numbers may be incorrect if calls to ReadLine are mixed with calls to other
// read methods.
type LineReader interface {
ReadLine() (string, int64, error)
}
// NewLineReader returns a LineReader starting at a specific line and using the
// newline character, \n, as a line separator. If r is a StringReader, it is
// used directly. Otherwise, it is wrapped in a way that may read extra data
// from the underlying input.
func NewLineReader(r io.Reader, lineno int64) LineReader {
sr, ok := r.(readStringReader)
if !ok {
sr = bufio.NewReader(r)
}
return &lineReader{r: sr, n: lineno}
}
type lineReader struct {
r readStringReader
n int64
}
func (lr *lineReader) ReadLine() (line string, lineno int64, err error) {
lineno = lr.n
line, err = lr.r.ReadString('\n')
if err == nil {
lr.n++
}
return
}
// unwrapLineReader returns a plain io.Reader that was converted to a
// LineReader by wrapping or casting. It should only be called from functions
// that accept an io.Reader as an argument and then convert it.
func unwrapLineReader(lr LineReader) io.Reader {
switch r := lr.(type) {
case io.Reader:
return r
case *lineReader:
return r.r
default:
panic(fmt.Sprintf("%T does not implement io.Reader and is not a gitdiff wrapper", lr))
}
}
// LineReaderAt is the interface that wraps the ReadLinesAt method.
//
// ReadLinesAt reads len(lines) into lines starting at line offset in the
// input source. It returns number of full lines read (0 <= n <= len(lines))
// and any error encountered. Line numbers are zero-indexed.
//
// If n < len(lines), ReadLinesAt returns a non-nil error explaining why more
// lines were not returned.
//
// Each full line includes the line ending character(s). If the last line of
// the input does not have a line ending character, ReadLinesAt returns the
// content of the line and io.EOF.
//
// If the content of the input source changes after the first call to
// ReadLinesAt, the behavior of future calls is undefined.
type LineReaderAt interface {
ReadLinesAt(lines [][]byte, offset int64) (n int, err error)
}
// NewLineReaderAt creates a LineReaderAt from an io.ReaderAt.
func NewLineReaderAt(r io.ReaderAt) LineReaderAt {
return &lineReaderAt{r: r}
}
type lineReaderAt struct {
r io.ReaderAt
index []int64
eof bool
}
func (r *lineReaderAt) ReadLinesAt(lines [][]byte, offset int64) (n int, err error) {
// TODO(bkeyes): revisit variable names
// - it's generally not clear when something is bytes vs lines
// - offset is a good example of this
if len(lines) == 0 {
return 0, nil
}
endLine := offset + int64(len(lines))
if endLine > int64(len(r.index)) && !r.eof {
if err := r.indexTo(endLine); err != nil {
return 0, err
}
}
if offset > int64(len(r.index)) {
return 0, io.EOF
}
// TODO(bkeyes): check usage of int / int64
// - interface uses int64 for arbitrarily large files
// - implementation is limited to int lines by index array
// offset <= len(r.index) means that it must fit in int without loss
size, readOffset := lookupLines(r.index, int(offset), len(lines))
b := make([]byte, size)
if _, err := r.r.ReadAt(b, readOffset); err != nil {
if err == io.EOF {
err = errors.New("ReadLinesAt: corrupt line index or changed source data")
}
return 0, err
}
for n = 0; n < len(lines) && offset+int64(n) < int64(len(r.index)); n++ {
i := int(offset) + n
start, end := readOffset, r.index[i]
if i > 0 {
start = r.index[i-1]
}
lines[n] = b[start-readOffset : end-readOffset]
}
if n < len(lines) || b[size-1] != '\n' {
return n, io.EOF
}
return n, nil
}
// indexTo reads data and computes the line index until there is information
// for line or a read returns io.EOF. It returns an error if and only if there
// is an error reading data.
func (r *lineReaderAt) indexTo(line int64) error {
var buf [1024]byte
var offset int64
if len(r.index) > 0 {
offset = r.index[len(r.index)-1]
}
for int64(len(r.index)) < line {
n, err := r.r.ReadAt(buf[:], offset)
if err != nil && err != io.EOF {
return err
}
for _, b := range buf[:n] {
offset++
if b == '\n' {
r.index = append(r.index, offset)
}
}
if err == io.EOF {
if n > 0 && buf[n-1] != '\n' {
r.index = append(r.index, offset)
}
r.eof = true
break
}
}
return nil
}
// lookupLines gets the byte offset and size of a range of lines from an index
// where the value at n is the offset of the first byte after line number n.
func lookupLines(index []int64, start, n int) (size int64, offset int64) {
if start > len(index) {
offset = index[len(index)-1]
} else if start > 0 {
offset = index[start-1]
}
if n > 0 {
// TODO(bkeyes): check types for overflow
if start+n > len(index) {
size = index[len(index)-1] - offset
} else {
size = index[start+n-1] - offset
}
}
return
}