Skip to content

Commit 7aebf30

Browse files
committed
add CRLF handling
1 parent a5338c9 commit 7aebf30

File tree

2 files changed

+93
-21
lines changed

2 files changed

+93
-21
lines changed

txtar/archive.go

Lines changed: 37 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
// Copyright 2018 The Go Authors. All rights reserved.
21
// Use of this source code is governed by a BSD-style
32
// license that can be found in the LICENSE file.
43

@@ -34,14 +33,15 @@ package txtar
3433
import (
3534
"bytes"
3635
"fmt"
37-
"io/ioutil"
36+
"os"
3837
"strings"
3938
)
4039

4140
// An Archive is a collection of files.
4241
type Archive struct {
4342
Comment []byte
4443
Files []File
44+
UseCRLF bool
4545
}
4646

4747
// A File is a single file in an archive.
@@ -55,18 +55,22 @@ type File struct {
5555
// a.Comment and all a.File[i].Data contain no file marker lines,
5656
// and all a.File[i].Name is non-empty.
5757
func Format(a *Archive) []byte {
58+
lineSeparator := lf
59+
if a.UseCRLF {
60+
lineSeparator = crlf
61+
}
5862
var buf bytes.Buffer
59-
buf.Write(fixNL(a.Comment))
63+
buf.Write(fixNL(a.Comment, a.UseCRLF))
6064
for _, f := range a.Files {
61-
fmt.Fprintf(&buf, "-- %s --\n", f.Name)
62-
buf.Write(fixNL(f.Data))
65+
fmt.Fprintf(&buf, "-- %s --%s", f.Name, lineSeparator)
66+
buf.Write(fixNL(f.Data, a.UseCRLF))
6367
}
6468
return buf.Bytes()
6569
}
6670

6771
// ParseFile parses the named file as an archive.
6872
func ParseFile(file string) (*Archive, error) {
69-
data, err := ioutil.ReadFile(file)
73+
data, err := os.ReadFile(file)
7074
if err != nil {
7175
return nil, err
7276
}
@@ -77,17 +81,23 @@ func ParseFile(file string) (*Archive, error) {
7781
// The returned Archive holds slices of data.
7882
func Parse(data []byte) *Archive {
7983
a := new(Archive)
84+
i := bytes.IndexByte(data, '\n')
85+
if i > 0 && data[i-1] == '\r' {
86+
a.UseCRLF = true
87+
}
8088
var name string
81-
a.Comment, name, data = findFileMarker(data)
89+
a.Comment, name, data = findFileMarker(data, a.UseCRLF)
8290
for name != "" {
8391
f := File{name, nil}
84-
f.Data, name, data = findFileMarker(data)
92+
f.Data, name, data = findFileMarker(data, a.UseCRLF)
8593
a.Files = append(a.Files, f)
8694
}
8795
return a
8896
}
8997

9098
var (
99+
crlf = []byte("\r\n")
100+
lf = []byte("\n")
91101
newlineMarker = []byte("\n-- ")
92102
marker = []byte("-- ")
93103
markerEnd = []byte(" --")
@@ -97,15 +107,15 @@ var (
97107
// extracts the file name, and returns the data before the marker,
98108
// the file name, and the data after the marker.
99109
// 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) {
110+
func findFileMarker(data []byte, useCRLF bool) (before []byte, name string, after []byte) {
101111
var i int
102112
for {
103-
if name, after = isMarker(data[i:]); name != "" {
113+
if name, after = isMarker(data[i:], useCRLF); name != "" {
104114
return data[:i], name, after
105115
}
106116
j := bytes.Index(data[i:], newlineMarker)
107117
if j < 0 {
108-
return fixNL(data), "", nil
118+
return fixNL(data, useCRLF), "", nil
109119
}
110120
i += j + 1 // positioned at start of new possible marker
111121
}
@@ -114,27 +124,35 @@ func findFileMarker(data []byte) (before []byte, name string, after []byte) {
114124
// isMarker checks whether data begins with a file marker line.
115125
// If so, it returns the name from the line and the data after the line.
116126
// Otherwise it returns name == "" with an unspecified after.
117-
func isMarker(data []byte) (name string, after []byte) {
127+
func isMarker(data []byte, useCRLF bool) (name string, after []byte) {
118128
if !bytes.HasPrefix(data, marker) {
119129
return "", nil
120130
}
121131
if i := bytes.IndexByte(data, '\n'); i >= 0 {
122-
data, after = data[:i], data[i+1:]
132+
if useCRLF && len(data) > 0 && data[i-1] == '\r' {
133+
data, after = data[:i-1], data[i+1:]
134+
} else {
135+
data, after = data[:i], data[i+1:]
136+
}
123137
}
124138
if !(bytes.HasSuffix(data, markerEnd) && len(data) >= len(marker)+len(markerEnd)) {
125139
return "", nil
126140
}
127141
return strings.TrimSpace(string(data[len(marker) : len(data)-len(markerEnd)])), after
128142
}
129143

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' {
144+
// If data is empty or ends in lineSeparator, fixNL returns data.
145+
// Otherwise fixNL returns a new slice consisting of data with a final lineSeparator added.
146+
func fixNL(data []byte, useCRLF bool) []byte {
147+
lineSeparator := lf
148+
if useCRLF {
149+
lineSeparator = crlf
150+
}
151+
if len(data) == 0 || (len(data) >= len(lineSeparator) && bytes.Equal(data[len(data)-len(lineSeparator):], lineSeparator)) {
134152
return data
135153
}
136-
d := make([]byte, len(data)+1)
154+
d := make([]byte, len(data)+len(lineSeparator))
137155
copy(d, data)
138-
d[len(data)] = '\n'
156+
d = append(d[:len(d)-len(lineSeparator)], lineSeparator...)
139157
return d
140158
}

txtar/archive_test.go

Lines changed: 56 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ func TestParse(t *testing.T) {
1818
parsed *Archive
1919
}{
2020
{
21-
name: "basic",
21+
name: "basic LF",
2222
text: `comment1
2323
comment2
2424
-- file1 --
@@ -42,6 +42,35 @@ some content
4242
{"noNL", []byte("hello world\n")},
4343
{"empty filename line", []byte("some content\n-- --\n")},
4444
},
45+
UseCRLF: false,
46+
},
47+
},
48+
{
49+
name: "basic CRLF",
50+
text: "comment1\r\n" +
51+
"comment2\r\n" +
52+
"-- file1 --\r\n" +
53+
"File 1 text.\r\n" +
54+
"-- foo ---\r\n" +
55+
"More file 1 text.\r\n" +
56+
"-- file 2 --\r\n" +
57+
"File 2 text.\r\n" +
58+
"-- empty --\r\n" +
59+
"-- noNL --\r\n" +
60+
"hello world\r\n" +
61+
"-- empty filename line --\r\n" +
62+
"some content\r\n" +
63+
"-- --\r\n",
64+
parsed: &Archive{
65+
Comment: []byte("comment1\r\ncomment2\r\n"),
66+
Files: []File{
67+
{"file1", []byte("File 1 text.\r\n-- foo ---\r\nMore file 1 text.\r\n")},
68+
{"file 2", []byte("File 2 text.\r\n")},
69+
{"empty", []byte{}},
70+
{"noNL", []byte("hello world\r\n")},
71+
{"empty filename line", []byte("some content\r\n-- --\r\n")},
72+
},
73+
UseCRLF: true,
4574
},
4675
},
4776
}
@@ -67,7 +96,7 @@ func TestFormat(t *testing.T) {
6796
wanted string
6897
}{
6998
{
70-
name: "basic",
99+
name: "basic LF",
71100
input: &Archive{
72101
Comment: []byte("comment1\ncomment2\n"),
73102
Files: []File{
@@ -76,6 +105,7 @@ func TestFormat(t *testing.T) {
76105
{"empty", []byte{}},
77106
{"noNL", []byte("hello world")},
78107
},
108+
UseCRLF: false,
79109
},
80110
wanted: `comment1
81111
comment2
@@ -90,6 +120,30 @@ File 2 text.
90120
hello world
91121
`,
92122
},
123+
{
124+
name: "basic CRLF",
125+
input: &Archive{
126+
Comment: []byte("comment1\r\ncomment2\r\n"),
127+
Files: []File{
128+
{"file1", []byte("File 1 text.\r\n-- foo ---\r\nMore file 1 text.\r\n")},
129+
{"file 2", []byte("File 2 text.\r\n")},
130+
{"empty", []byte{}},
131+
{"noNL", []byte("hello world")},
132+
},
133+
UseCRLF: true,
134+
},
135+
wanted: "comment1\r\n" +
136+
"comment2\r\n" +
137+
"-- file1 --\r\n" +
138+
"File 1 text.\r\n" +
139+
"-- foo ---\r\n" +
140+
"More file 1 text.\r\n" +
141+
"-- file 2 --\r\n" +
142+
"File 2 text.\r\n" +
143+
"-- empty --\r\n" +
144+
"-- noNL --\r\n" +
145+
"hello world\r\n",
146+
},
93147
}
94148
for _, tt := range tests {
95149
t.Run(tt.name, func(t *testing.T) {

0 commit comments

Comments
 (0)