Source file src/internal/txtar/archive.go

     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  	"fmt"
    37  	"os"
    38  	"strings"
    39  )
    40  
    41  // An Archive is a collection of files.
    42  type Archive struct {
    43  	Comment []byte
    44  	Files   []File
    45  }
    46  
    47  // A File is a single file in an archive.
    48  type File struct {
    49  	Name string // name of file ("foo/bar.txt")
    50  	Data []byte // text content of file
    51  }
    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  	var buf bytes.Buffer
    59  	buf.Write(fixNL(a.Comment))
    60  	for _, f := range a.Files {
    61  		fmt.Fprintf(&buf, "-- %s --\n", f.Name)
    62  		buf.Write(fixNL(f.Data))
    63  	}
    64  	return buf.Bytes()
    65  }
    66  
    67  // ParseFile parses the named file as an archive.
    68  func ParseFile(file string) (*Archive, error) {
    69  	data, err := os.ReadFile(file)
    70  	if err != nil {
    71  		return nil, err
    72  	}
    73  	return Parse(data), nil
    74  }
    75  
    76  // Parse parses the serialized form of an Archive.
    77  // The returned Archive holds slices of data.
    78  func Parse(data []byte) *Archive {
    79  	a := new(Archive)
    80  	var name string
    81  	a.Comment, name, data = findFileMarker(data)
    82  	for name != "" {
    83  		f := File{name, nil}
    84  		f.Data, name, data = findFileMarker(data)
    85  		a.Files = append(a.Files, f)
    86  	}
    87  	return a
    88  }
    89  
    90  var (
    91  	newlineMarker = []byte("\n-- ")
    92  	marker        = []byte("-- ")
    93  	markerEnd     = []byte(" --")
    94  )
    95  
    96  // findFileMarker finds the next file marker in data,
    97  // extracts the file name, and returns the data before the marker,
    98  // the file name, and the data after the marker.
    99  // If there is no next marker, findFileMarker returns before = fixNL(data), name = "", after = nil.
   100  func findFileMarker(data []byte) (before []byte, name string, after []byte) {
   101  	var i int
   102  	for {
   103  		if name, after = isMarker(data[i:]); name != "" {
   104  			return data[:i], name, after
   105  		}
   106  		j := bytes.Index(data[i:], newlineMarker)
   107  		if j < 0 {
   108  			return fixNL(data), "", nil
   109  		}
   110  		i += j + 1 // positioned at start of new possible marker
   111  	}
   112  }
   113  
   114  // isMarker checks whether data begins with a file marker line.
   115  // If so, it returns the name from the line and the data after the line.
   116  // Otherwise it returns name == "" with an unspecified after.
   117  func isMarker(data []byte) (name string, after []byte) {
   118  	if !bytes.HasPrefix(data, marker) {
   119  		return "", nil
   120  	}
   121  	if i := bytes.IndexByte(data, '\n'); i >= 0 {
   122  		data, after = data[:i], data[i+1:]
   123  	}
   124  	if !(bytes.HasSuffix(data, markerEnd) && len(data) >= len(marker)+len(markerEnd)) {
   125  		return "", nil
   126  	}
   127  	return strings.TrimSpace(string(data[len(marker) : len(data)-len(markerEnd)])), after
   128  }
   129  
   130  // If data is empty or ends in \n, fixNL returns data.
   131  // Otherwise fixNL returns a new slice consisting of data with a final \n added.
   132  func fixNL(data []byte) []byte {
   133  	if len(data) == 0 || data[len(data)-1] == '\n' {
   134  		return data
   135  	}
   136  	d := make([]byte, len(data)+1)
   137  	copy(d, data)
   138  	d[len(data)] = '\n'
   139  	return d
   140  }
   141  

View as plain text