all repos

clerk @ e2e9e2b

missing tooling for ledger/hledger

clerk/internal/testutil/txtar/archive.go (view raw)

Oleksandr Smirnov Oleksandr Smirnov
olexsmir@gmail.com
tests: switch to txtar, 3 days ago
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
// Get returns the data of the first file with the given name, or nil if not found.
54
func (a *Archive) Get(name string) []byte {
55
	for _, f := range a.Files {
56
		if f.Name == name {
57
			return f.Data
58
		}
59
	}
60
	return nil
61
}
62
63
// Format returns the serialized form of an Archive.
64
// It is assumed that the Archive data structure is well-formed:
65
// a.Comment and all a.File[i].Data contain no file marker lines,
66
// and all a.File[i].Name is non-empty.
67
func Format(a *Archive) []byte {
68
	var buf bytes.Buffer
69
	buf.Write(fixNL(a.Comment))
70
	for _, f := range a.Files {
71
		fmt.Fprintf(&buf, "-- %s --\n", f.Name)
72
		buf.Write(fixNL(f.Data))
73
	}
74
	return buf.Bytes()
75
}
76
77
// ParseFile parses the named file as an archive.
78
func ParseFile(file string) (*Archive, error) {
79
	data, err := os.ReadFile(file)
80
	if err != nil {
81
		return nil, err
82
	}
83
	return Parse(data), nil
84
}
85
86
// Parse parses the serialized form of an Archive.
87
// The returned Archive holds slices of data.
88
func Parse(data []byte) *Archive {
89
	a := new(Archive)
90
	var name string
91
	a.Comment, name, data = findFileMarker(data)
92
	for name != "" {
93
		f := File{name, nil}
94
		f.Data, name, data = findFileMarker(data)
95
		a.Files = append(a.Files, f)
96
	}
97
	return a
98
}
99
100
var (
101
	newlineMarker = []byte("\n-- ")
102
	marker        = []byte("-- ")
103
	markerEnd     = []byte(" --")
104
)
105
106
// findFileMarker finds the next file marker in data,
107
// extracts the file name, and returns the data before the marker,
108
// the file name, and the data after the marker.
109
// If there is no next marker, findFileMarker returns before = fixNL(data), name = "", after = nil.
110
func findFileMarker(data []byte) (before []byte, name string, after []byte) {
111
	var i int
112
	for {
113
		if name, after = isMarker(data[i:]); name != "" {
114
			return data[:i], name, after
115
		}
116
		j := bytes.Index(data[i:], newlineMarker)
117
		if j < 0 {
118
			return fixNL(data), "", nil
119
		}
120
		i += j + 1 // positioned at start of new possible marker
121
	}
122
}
123
124
// isMarker checks whether data begins with a file marker line.
125
// If so, it returns the name from the line and the data after the line.
126
// Otherwise it returns name == "" with an unspecified after.
127
func isMarker(data []byte) (name string, after []byte) {
128
	if !bytes.HasPrefix(data, marker) {
129
		return "", nil
130
	}
131
	if i := bytes.IndexByte(data, '\n'); i >= 0 {
132
		data, after = data[:i], data[i+1:]
133
		if data[i-1] == '\r' { // handle \r\n line ending
134
			data = data[:i-1]
135
		}
136
	}
137
	if !bytes.HasSuffix(data, markerEnd) || len(data) < len(marker)+len(markerEnd) {
138
		return "", nil
139
	}
140
	return strings.TrimSpace(string(data[len(marker) : len(data)-len(markerEnd)])), after
141
}
142
143
// If data is empty or ends in \n, fixNL returns data.
144
// Otherwise fixNL returns a new slice consisting of data with a final \n added.
145
func fixNL(data []byte) []byte {
146
	if len(data) == 0 || data[len(data)-1] == '\n' {
147
		return data
148
	}
149
	d := make([]byte, len(data)+1)
150
	copy(d, data)
151
	d[len(data)] = '\n'
152
	return d
153
}