...

Source file src/github.com/rogpeppe/go-internal/txtar/archive.go

Documentation: github.com/rogpeppe/go-internal/txtar

     1  // Copyright 2018 The Go 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 txtar implements a trivial text-based file archive format.
     6  //
     7  // The goals for the format are:
     8  //
     9  //   - be trivial enough to create and edit by hand.
    10  //   - be able to store trees of text files describing go command test cases.
    11  //   - diff nicely in git history and code reviews.
    12  //
    13  // Non-goals include being a completely general archive format,
    14  // storing binary data, storing file modes, storing special files like
    15  // symbolic links, and so on.
    16  //
    17  // # Txtar format
    18  //
    19  // A txtar archive is zero or more comment lines and then a sequence of file entries.
    20  // Each file entry begins with a file marker line of the form "-- FILENAME --"
    21  // and is followed by zero or more file content lines making up the file data.
    22  // The comment or file content ends at the next file marker line.
    23  // The file marker line must begin with the three-byte sequence "-- "
    24  // and end with the three-byte sequence " --", but the enclosed
    25  // file name can be surrounding by additional white space,
    26  // all of which is stripped.
    27  //
    28  // If the txtar file is missing a trailing newline on the final line,
    29  // parsers should consider a final newline to be present anyway.
    30  //
    31  // There are no possible syntax errors in a txtar archive.
    32  package txtar
    33  
    34  import (
    35  	"bytes"
    36  	"errors"
    37  	"fmt"
    38  	"io/ioutil"
    39  	"os"
    40  	"path/filepath"
    41  	"strings"
    42  	"unicode/utf8"
    43  
    44  	"golang.org/x/tools/txtar"
    45  )
    46  
    47  // An Archive is a collection of files.
    48  type Archive = txtar.Archive
    49  
    50  // A File is a single file in an archive.
    51  type File = txtar.File
    52  
    53  // Format returns the serialized form of an Archive.
    54  // It is assumed that the Archive data structure is well-formed:
    55  // a.Comment and all a.File[i].Data contain no file marker lines,
    56  // and all a.File[i].Name is non-empty.
    57  func Format(a *Archive) []byte {
    58  	return txtar.Format(a)
    59  }
    60  
    61  // ParseFile parses the named file as an archive.
    62  func ParseFile(file string) (*Archive, error) {
    63  	data, err := ioutil.ReadFile(file)
    64  	if err != nil {
    65  		return nil, err
    66  	}
    67  	return Parse(data), nil
    68  }
    69  
    70  // Parse parses the serialized form of an Archive.
    71  // The returned Archive holds slices of data.
    72  //
    73  // TODO use golang.org/x/tools/txtar.Parse when https://github.com/golang/go/issues/59264
    74  // is fixed.
    75  func Parse(data []byte) *Archive {
    76  	a := new(Archive)
    77  	var name string
    78  	a.Comment, name, data = findFileMarker(data)
    79  	for name != "" {
    80  		f := File{name, nil}
    81  		f.Data, name, data = findFileMarker(data)
    82  		a.Files = append(a.Files, f)
    83  	}
    84  	return a
    85  }
    86  
    87  // NeedsQuote reports whether the given data needs to
    88  // be quoted before it's included as a txtar file.
    89  func NeedsQuote(data []byte) bool {
    90  	_, _, after := findFileMarker(data)
    91  	return after != nil
    92  }
    93  
    94  // Quote quotes the data so that it can be safely stored in a txtar
    95  // file. This copes with files that contain lines that look like txtar
    96  // separators.
    97  //
    98  // The original data can be recovered with Unquote. It returns an error
    99  // if the data cannot be quoted (for example because it has no final
   100  // newline or it holds unprintable characters)
   101  func Quote(data []byte) ([]byte, error) {
   102  	if len(data) == 0 {
   103  		return nil, nil
   104  	}
   105  	if data[len(data)-1] != '\n' {
   106  		return nil, errors.New("data has no final newline")
   107  	}
   108  	if !utf8.Valid(data) {
   109  		return nil, fmt.Errorf("data contains non-UTF-8 characters")
   110  	}
   111  	var nd []byte
   112  	prev := byte('\n')
   113  	for _, b := range data {
   114  		if prev == '\n' {
   115  			nd = append(nd, '>')
   116  		}
   117  		nd = append(nd, b)
   118  		prev = b
   119  	}
   120  	return nd, nil
   121  }
   122  
   123  // Unquote unquotes data as quoted by Quote.
   124  func Unquote(data []byte) ([]byte, error) {
   125  	if len(data) == 0 {
   126  		return nil, nil
   127  	}
   128  	if data[0] != '>' || data[len(data)-1] != '\n' {
   129  		return nil, errors.New("data does not appear to be quoted")
   130  	}
   131  	data = bytes.Replace(data, []byte("\n>"), []byte("\n"), -1)
   132  	data = bytes.TrimPrefix(data, []byte(">"))
   133  	return data, nil
   134  }
   135  
   136  var (
   137  	newlineMarker = []byte("\n-- ")
   138  	marker        = []byte("-- ")
   139  	markerEnd     = []byte(" --")
   140  )
   141  
   142  // findFileMarker finds the next file marker in data,
   143  // extracts the file name, and returns the data before the marker,
   144  // the file name, and the data after the marker.
   145  // If there is no next marker, findFileMarker returns before = fixNL(data), name = "", after = nil.
   146  func findFileMarker(data []byte) (before []byte, name string, after []byte) {
   147  	var i int
   148  	for {
   149  		if name, after = isMarker(data[i:]); name != "" {
   150  			return data[:i], name, after
   151  		}
   152  		j := bytes.Index(data[i:], newlineMarker)
   153  		if j < 0 {
   154  			return fixNL(data), "", nil
   155  		}
   156  		i += j + 1 // positioned at start of new possible marker
   157  	}
   158  }
   159  
   160  // isMarker checks whether data begins with a file marker line.
   161  // If so, it returns the name from the line and the data after the line.
   162  // Otherwise it returns name == "" with an unspecified after.
   163  func isMarker(data []byte) (name string, after []byte) {
   164  	if !bytes.HasPrefix(data, marker) {
   165  		return "", nil
   166  	}
   167  	if i := bytes.IndexByte(data, '\n'); i >= 0 {
   168  		data, after = data[:i], data[i+1:]
   169  		if data[i-1] == '\r' {
   170  			data = data[:len(data)-1]
   171  		}
   172  	}
   173  	if !bytes.HasSuffix(data, markerEnd) {
   174  		return "", nil
   175  	}
   176  	return strings.TrimSpace(string(data[len(marker) : len(data)-len(markerEnd)])), after
   177  }
   178  
   179  // If data is empty or ends in \n, fixNL returns data.
   180  // Otherwise fixNL returns a new slice consisting of data with a final \n added.
   181  func fixNL(data []byte) []byte {
   182  	if len(data) == 0 || data[len(data)-1] == '\n' {
   183  		return data
   184  	}
   185  	d := make([]byte, len(data)+1)
   186  	copy(d, data)
   187  	d[len(data)] = '\n'
   188  	return d
   189  }
   190  
   191  // Write writes each File in an Archive to the given directory, returning any
   192  // errors encountered. An error is also returned in the event a file would be
   193  // written outside of dir.
   194  func Write(a *Archive, dir string) error {
   195  	for _, f := range a.Files {
   196  		fp := filepath.Clean(filepath.FromSlash(f.Name))
   197  		if isAbs(fp) || strings.HasPrefix(fp, ".."+string(filepath.Separator)) {
   198  			return fmt.Errorf("%q: outside parent directory", f.Name)
   199  		}
   200  		fp = filepath.Join(dir, fp)
   201  
   202  		if err := os.MkdirAll(filepath.Dir(fp), 0o777); err != nil {
   203  			return err
   204  		}
   205  		// Avoid overwriting existing files by using O_EXCL.
   206  		out, err := os.OpenFile(fp, os.O_WRONLY|os.O_CREATE|os.O_EXCL, 0o666)
   207  		if err != nil {
   208  			return err
   209  		}
   210  
   211  		_, err = out.Write(f.Data)
   212  		cerr := out.Close()
   213  		if err != nil {
   214  			return err
   215  		}
   216  		if cerr != nil {
   217  			return cerr
   218  		}
   219  	}
   220  	return nil
   221  }
   222  
   223  func isAbs(p string) bool {
   224  	// Note: under Windows, filepath.IsAbs(`\foo`) returns false,
   225  	// so we need to check for that case specifically.
   226  	return filepath.IsAbs(p) || strings.HasPrefix(p, string(filepath.Separator))
   227  }
   228  

View as plain text