mirror of
https://github.com/golang/go
synced 2024-11-26 03:37:57 -07:00
Basic POSIX-compatible tar writer.
R=rsc APPROVED=rsc DELTA=456 (382 added, 66 deleted, 8 changed) OCL=31246 CL=31372
This commit is contained in:
parent
903d28bd1e
commit
d6d0a39050
@ -2,6 +2,7 @@
|
|||||||
# Use of this source code is governed by a BSD-style
|
# Use of this source code is governed by a BSD-style
|
||||||
# license that can be found in the LICENSE file.
|
# license that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
|
||||||
# DO NOT EDIT. Automatically generated by gobuild.
|
# DO NOT EDIT. Automatically generated by gobuild.
|
||||||
# gobuild -m >Makefile
|
# gobuild -m >Makefile
|
||||||
|
|
||||||
@ -20,7 +21,7 @@ test: packages
|
|||||||
|
|
||||||
coverage: packages
|
coverage: packages
|
||||||
gotest
|
gotest
|
||||||
6cov -g `pwd` | grep -v '_test\.go:'
|
6cov -g $$(pwd) | grep -v '_test\.go:'
|
||||||
|
|
||||||
%.$O: %.go
|
%.$O: %.go
|
||||||
$(GC) -I_obj $*.go
|
$(GC) -I_obj $*.go
|
||||||
@ -32,16 +33,24 @@ coverage: packages
|
|||||||
$(AS) $*.s
|
$(AS) $*.s
|
||||||
|
|
||||||
O1=\
|
O1=\
|
||||||
|
common.$O\
|
||||||
|
|
||||||
|
O2=\
|
||||||
untar.$O\
|
untar.$O\
|
||||||
|
writer.$O\
|
||||||
|
|
||||||
|
|
||||||
phases: a1
|
phases: a1 a2
|
||||||
_obj$D/tar.a: phases
|
_obj$D/tar.a: phases
|
||||||
|
|
||||||
a1: $(O1)
|
a1: $(O1)
|
||||||
$(AR) grc _obj$D/tar.a untar.$O
|
$(AR) grc _obj$D/tar.a common.$O
|
||||||
rm -f $(O1)
|
rm -f $(O1)
|
||||||
|
|
||||||
|
a2: $(O2)
|
||||||
|
$(AR) grc _obj$D/tar.a untar.$O writer.$O
|
||||||
|
rm -f $(O2)
|
||||||
|
|
||||||
|
|
||||||
newpkg: clean
|
newpkg: clean
|
||||||
mkdir -p _obj$D
|
mkdir -p _obj$D
|
||||||
@ -49,6 +58,7 @@ newpkg: clean
|
|||||||
|
|
||||||
$(O1): newpkg
|
$(O1): newpkg
|
||||||
$(O2): a1
|
$(O2): a1
|
||||||
|
$(O3): a2
|
||||||
|
|
||||||
nuke: clean
|
nuke: clean
|
||||||
rm -f $(GOROOT)/pkg/$(GOOS)_$(GOARCH)$D/tar.a
|
rm -f $(GOROOT)/pkg/$(GOOS)_$(GOARCH)$D/tar.a
|
||||||
|
74
src/pkg/archive/tar/common.go
Normal file
74
src/pkg/archive/tar/common.go
Normal file
@ -0,0 +1,74 @@
|
|||||||
|
// Copyright 2009 The Go Authors. All rights reserved.
|
||||||
|
// Use of this source code is governed by a BSD-style
|
||||||
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
// The tar package implements access to tar archives.
|
||||||
|
// It aims to cover most of the variations, including those produced
|
||||||
|
// by GNU and BSD tars.
|
||||||
|
//
|
||||||
|
// References:
|
||||||
|
// http://www.freebsd.org/cgi/man.cgi?query=tar&sektion=5
|
||||||
|
// http://www.gnu.org/software/tar/manual/html_node/Standard.html
|
||||||
|
package tar
|
||||||
|
|
||||||
|
const (
|
||||||
|
blockSize = 512;
|
||||||
|
|
||||||
|
// Types
|
||||||
|
TypeReg = '0';
|
||||||
|
TypeRegA = '\x00';
|
||||||
|
TypeLink = '1';
|
||||||
|
TypeSymlink = '2';
|
||||||
|
TypeChar = '3';
|
||||||
|
TypeBlock = '4';
|
||||||
|
TypeDir = '5';
|
||||||
|
TypeFifo = '6';
|
||||||
|
TypeCont = '7';
|
||||||
|
TypeXHeader = 'x';
|
||||||
|
TypeXGlobalHeader = 'g';
|
||||||
|
)
|
||||||
|
|
||||||
|
// A Header represents a single header in a tar archive.
|
||||||
|
// Some fields may not be populated.
|
||||||
|
type Header struct {
|
||||||
|
Name string;
|
||||||
|
Mode int64;
|
||||||
|
Uid int64;
|
||||||
|
Gid int64;
|
||||||
|
Size int64;
|
||||||
|
Mtime int64;
|
||||||
|
Typeflag byte;
|
||||||
|
Linkname string;
|
||||||
|
Uname string;
|
||||||
|
Gname string;
|
||||||
|
Devmajor int64;
|
||||||
|
Devminor int64;
|
||||||
|
Atime int64;
|
||||||
|
Ctime int64;
|
||||||
|
}
|
||||||
|
|
||||||
|
var zeroBlock = make([]byte, blockSize);
|
||||||
|
|
||||||
|
// POSIX specifies a sum of the unsigned byte values, but the Sun tar uses signed byte values.
|
||||||
|
// We compute and return both.
|
||||||
|
func checksum(header []byte) (unsigned int64, signed int64) {
|
||||||
|
for i := 0; i < len(header); i++ {
|
||||||
|
if i == 148 {
|
||||||
|
// The chksum field (header[148:156]) is special: it should be treated as space bytes.
|
||||||
|
unsigned += ' ' * 8;
|
||||||
|
signed += ' ' * 8;
|
||||||
|
i += 7;
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
unsigned += int64(header[i]);
|
||||||
|
signed += int64(int8(header[i]));
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
type slicer []byte
|
||||||
|
func (sp *slicer) next(n int) (b []byte) {
|
||||||
|
s := *sp;
|
||||||
|
b, *sp = s[0:n], s[n:len(s)];
|
||||||
|
return
|
||||||
|
}
|
BIN
src/pkg/archive/tar/testdata/writer.tar
vendored
Normal file
BIN
src/pkg/archive/tar/testdata/writer.tar
vendored
Normal file
Binary file not shown.
@ -2,20 +2,14 @@
|
|||||||
// Use of this source code is governed by a BSD-style
|
// Use of this source code is governed by a BSD-style
|
||||||
// license that can be found in the LICENSE file.
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
// The tar package implements access to tar archives.
|
|
||||||
// It aims to cover most of the variations, including those produced
|
|
||||||
// by GNU and BSD tars.
|
|
||||||
//
|
|
||||||
// References:
|
|
||||||
// http://www.freebsd.org/cgi/man.cgi?query=tar&sektion=5
|
|
||||||
// http://www.gnu.org/software/tar/manual/html_node/Standard.html
|
|
||||||
package tar
|
package tar
|
||||||
|
|
||||||
// TODO(dsymonds):
|
// TODO(dsymonds):
|
||||||
// - pax extensions
|
// - pax extensions
|
||||||
|
// - rename this file to reader.go
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bufio";
|
"archive/tar";
|
||||||
"bytes";
|
"bytes";
|
||||||
"io";
|
"io";
|
||||||
"os";
|
"os";
|
||||||
@ -51,25 +45,6 @@ type Reader struct {
|
|||||||
pad int64; // amount of padding (ignored) after current file entry
|
pad int64; // amount of padding (ignored) after current file entry
|
||||||
}
|
}
|
||||||
|
|
||||||
// A Header represents a single header in a tar archive.
|
|
||||||
// Only some fields may be populated.
|
|
||||||
type Header struct {
|
|
||||||
Name string;
|
|
||||||
Mode int64;
|
|
||||||
Uid int64;
|
|
||||||
Gid int64;
|
|
||||||
Size int64;
|
|
||||||
Mtime int64;
|
|
||||||
Typeflag byte;
|
|
||||||
Linkname string;
|
|
||||||
Uname string;
|
|
||||||
Gname string;
|
|
||||||
Devmajor int64;
|
|
||||||
Devminor int64;
|
|
||||||
Atime int64;
|
|
||||||
Ctime int64;
|
|
||||||
}
|
|
||||||
|
|
||||||
func (tr *Reader) skipUnread()
|
func (tr *Reader) skipUnread()
|
||||||
func (tr *Reader) readHeader() *Header
|
func (tr *Reader) readHeader() *Header
|
||||||
|
|
||||||
@ -90,25 +65,6 @@ func (tr *Reader) Next() (*Header, os.Error) {
|
|||||||
return hdr, tr.err
|
return hdr, tr.err
|
||||||
}
|
}
|
||||||
|
|
||||||
const (
|
|
||||||
blockSize = 512;
|
|
||||||
|
|
||||||
// Types
|
|
||||||
TypeReg = '0';
|
|
||||||
TypeRegA = '\x00';
|
|
||||||
TypeLink = '1';
|
|
||||||
TypeSymlink = '2';
|
|
||||||
TypeChar = '3';
|
|
||||||
TypeBlock = '4';
|
|
||||||
TypeDir = '5';
|
|
||||||
TypeFifo = '6';
|
|
||||||
TypeCont = '7';
|
|
||||||
TypeXHeader = 'x';
|
|
||||||
TypeXGlobalHeader = 'g';
|
|
||||||
)
|
|
||||||
|
|
||||||
var zeroBlock = make([]byte, blockSize);
|
|
||||||
|
|
||||||
// Parse bytes as a NUL-terminated C-style string.
|
// Parse bytes as a NUL-terminated C-style string.
|
||||||
// If a NUL byte is not found then the whole slice is returned as a string.
|
// If a NUL byte is not found then the whole slice is returned as a string.
|
||||||
func cString(b []byte) string {
|
func cString(b []byte) string {
|
||||||
@ -153,36 +109,15 @@ func (tr *Reader) skipUnread() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (tr *Reader) verifyChecksum(header []byte) bool {
|
func (tr *Reader) verifyChecksum(header []byte) bool {
|
||||||
given := tr.octal(header[148:156]);
|
|
||||||
if tr.err != nil {
|
if tr.err != nil {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
// POSIX specifies a sum of the unsigned byte values,
|
given := tr.octal(header[148:156]);
|
||||||
// but the Sun tar uses signed byte values. :-(
|
unsigned, signed := checksum(header);
|
||||||
var unsigned, signed int64;
|
|
||||||
for i := 0; i < len(header); i++ {
|
|
||||||
if i == 148 {
|
|
||||||
// The chksum field is special: it should be treated as space bytes.
|
|
||||||
unsigned += ' ' * 8;
|
|
||||||
signed += ' ' * 8;
|
|
||||||
i += 7;
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
unsigned += int64(header[i]);
|
|
||||||
signed += int64(int8(header[i]));
|
|
||||||
}
|
|
||||||
|
|
||||||
return given == unsigned || given == signed
|
return given == unsigned || given == signed
|
||||||
}
|
}
|
||||||
|
|
||||||
type slicer []byte
|
|
||||||
func (sp *slicer) next(n int) (b []byte) {
|
|
||||||
s := *sp;
|
|
||||||
b, *sp = s[0:n], s[n:len(s)];
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
func (tr *Reader) readHeader() *Header {
|
func (tr *Reader) readHeader() *Header {
|
||||||
header := make([]byte, blockSize);
|
header := make([]byte, blockSize);
|
||||||
var n int;
|
var n int;
|
||||||
|
@ -102,9 +102,9 @@ var untarTests = []*untarTest{
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
};
|
}
|
||||||
|
|
||||||
func TestAll(t *testing.T) {
|
func TestReader(t *testing.T) {
|
||||||
testLoop:
|
testLoop:
|
||||||
for i, test := range untarTests {
|
for i, test := range untarTests {
|
||||||
f, err := os.Open(test.file, os.O_RDONLY, 0444);
|
f, err := os.Open(test.file, os.O_RDONLY, 0444);
|
||||||
|
180
src/pkg/archive/tar/writer.go
Normal file
180
src/pkg/archive/tar/writer.go
Normal file
@ -0,0 +1,180 @@
|
|||||||
|
// Copyright 2009 The Go Authors. All rights reserved.
|
||||||
|
// Use of this source code is governed by a BSD-style
|
||||||
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
package tar
|
||||||
|
|
||||||
|
// TODO(dsymonds):
|
||||||
|
// - catch more errors (no first header, write after close, etc.)
|
||||||
|
|
||||||
|
import (
|
||||||
|
"archive/tar";
|
||||||
|
"bytes";
|
||||||
|
"io";
|
||||||
|
"os";
|
||||||
|
"strconv";
|
||||||
|
"strings";
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
ErrWriteTooLong os.Error = os.ErrorString("write too long");
|
||||||
|
// TODO(dsymonds): remove ErrIntFieldTooBig after we implement binary extension.
|
||||||
|
ErrIntFieldTooBig os.Error = os.ErrorString("an integer header field was too big");
|
||||||
|
)
|
||||||
|
|
||||||
|
// A Writer provides sequential writing of a tar archive in POSIX.1 format.
|
||||||
|
// A tar archive consists of a sequence of files.
|
||||||
|
// Call WriteHeader to begin a new file, and then call Write to supply that file's data,
|
||||||
|
// writing at most hdr.Size bytes in total.
|
||||||
|
//
|
||||||
|
// Example:
|
||||||
|
// tw := NewTarWriter(w);
|
||||||
|
// hdr := new(Header);
|
||||||
|
// hdr.Size = length of data in bytes;
|
||||||
|
// // populate other hdr fields as desired
|
||||||
|
// if err := tw.WriteHeader(hdr); err != nil {
|
||||||
|
// // handle error
|
||||||
|
// }
|
||||||
|
// io.Copy(data, tw);
|
||||||
|
// tw.Close();
|
||||||
|
type Writer struct {
|
||||||
|
w io.Writer;
|
||||||
|
err os.Error;
|
||||||
|
nb int64; // number of unwritten bytes for current file entry
|
||||||
|
pad int64; // amount of padding to write after current file entry
|
||||||
|
closed bool;
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewWriter creates a new Writer writing to w.
|
||||||
|
func NewWriter(w io.Writer) *Writer {
|
||||||
|
return &Writer{ w: w }
|
||||||
|
}
|
||||||
|
|
||||||
|
// Flush finishes writing the current file (optional).
|
||||||
|
func (tw *Writer) Flush() os.Error {
|
||||||
|
n := tw.nb + tw.pad;
|
||||||
|
for n > 0 && tw.err == nil {
|
||||||
|
nr := n;
|
||||||
|
if nr > blockSize {
|
||||||
|
nr = blockSize;
|
||||||
|
}
|
||||||
|
var nw int;
|
||||||
|
nw, tw.err = tw.w.Write(zeroBlock[0:nr]);
|
||||||
|
n -= int64(nw);
|
||||||
|
}
|
||||||
|
tw.nb = 0;
|
||||||
|
tw.pad = 0;
|
||||||
|
return tw.err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Write s into b, terminating it with a NUL if there is room.
|
||||||
|
func (tw *Writer) cString(b []byte, s string) {
|
||||||
|
if len(s) > len(b) {
|
||||||
|
if tw.err == nil {
|
||||||
|
tw.err = ErrIntFieldTooBig;
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
for i, ch := range strings.Bytes(s) {
|
||||||
|
b[i] = ch;
|
||||||
|
}
|
||||||
|
if len(s) < len(b) {
|
||||||
|
b[len(s)] = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Encode x as an octal ASCII string and write it into b with leading zeros.
|
||||||
|
func (tw *Writer) octal(b []byte, x int64) {
|
||||||
|
s := strconv.Itob64(x, 8);
|
||||||
|
// leading zeros, but leave room for a NUL.
|
||||||
|
for len(s) + 1 < len(b) {
|
||||||
|
s = "0" + s;
|
||||||
|
}
|
||||||
|
tw.cString(b, s);
|
||||||
|
}
|
||||||
|
|
||||||
|
// WriteHeader writes hdr and prepares to accept the file's contents.
|
||||||
|
// WriteHeader calls Flush if it is not the first header.
|
||||||
|
func (tw *Writer) WriteHeader(hdr *Header) os.Error {
|
||||||
|
if tw.err == nil {
|
||||||
|
tw.Flush();
|
||||||
|
}
|
||||||
|
if tw.err != nil {
|
||||||
|
return tw.err
|
||||||
|
}
|
||||||
|
|
||||||
|
tw.nb = int64(hdr.Size);
|
||||||
|
tw.pad = -tw.nb & (blockSize - 1); // blockSize is a power of two
|
||||||
|
|
||||||
|
header := make([]byte, blockSize);
|
||||||
|
s := slicer(header);
|
||||||
|
|
||||||
|
// TODO(dsymonds): handle names longer than 100 chars
|
||||||
|
nr := bytes.Copy(s.next(100), strings.Bytes(hdr.Name));
|
||||||
|
|
||||||
|
tw.octal(s.next(8), hdr.Mode);
|
||||||
|
tw.octal(s.next(8), hdr.Uid);
|
||||||
|
tw.octal(s.next(8), hdr.Gid);
|
||||||
|
tw.octal(s.next(12), hdr.Size);
|
||||||
|
tw.octal(s.next(12), hdr.Mtime);
|
||||||
|
s.next(8); // chksum
|
||||||
|
s.next(1)[0] = hdr.Typeflag;
|
||||||
|
s.next(100); // linkname
|
||||||
|
bytes.Copy(s.next(8), strings.Bytes("ustar\x0000"));
|
||||||
|
tw.cString(s.next(32), hdr.Uname);
|
||||||
|
tw.cString(s.next(32), hdr.Gname);
|
||||||
|
tw.octal(s.next(8), hdr.Devmajor);
|
||||||
|
tw.octal(s.next(8), hdr.Devminor);
|
||||||
|
|
||||||
|
// The chksum field is terminated by a NUL and a space.
|
||||||
|
// This is different from the other octal fields.
|
||||||
|
chksum, _ := checksum(header);
|
||||||
|
tw.octal(header[148:155], chksum);
|
||||||
|
header[155] = ' ';
|
||||||
|
|
||||||
|
if tw.err != nil {
|
||||||
|
// problem with header; probably integer too big for a field.
|
||||||
|
return tw.err
|
||||||
|
}
|
||||||
|
|
||||||
|
var n int;
|
||||||
|
n, tw.err = tw.w.Write(header);
|
||||||
|
|
||||||
|
return tw.err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Write writes to the current entry in the tar archive.
|
||||||
|
// Write returns the error ErrWriteTooLong if more than
|
||||||
|
// hdr.Size bytes are written after WriteHeader.
|
||||||
|
func (tw *Writer) Write(b []uint8) (n int, err os.Error) {
|
||||||
|
overwrite := false;
|
||||||
|
if int64(len(b)) > tw.nb {
|
||||||
|
b = b[0:tw.nb];
|
||||||
|
overwrite = true;
|
||||||
|
}
|
||||||
|
n, err = tw.w.Write(b);
|
||||||
|
tw.nb -= int64(n);
|
||||||
|
if err == nil && overwrite {
|
||||||
|
err = ErrWriteTooLong;
|
||||||
|
}
|
||||||
|
tw.err = err;
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (tw *Writer) Close() os.Error {
|
||||||
|
if tw.err != nil || tw.closed {
|
||||||
|
return tw.err
|
||||||
|
}
|
||||||
|
tw.Flush();
|
||||||
|
tw.closed = true;
|
||||||
|
|
||||||
|
// trailer: two zero blocks
|
||||||
|
for i := 0; i < 2; i++ {
|
||||||
|
var n int;
|
||||||
|
n, tw.err = tw.w.Write(zeroBlock);
|
||||||
|
if tw.err != nil {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return tw.err
|
||||||
|
}
|
122
src/pkg/archive/tar/writer_test.go
Normal file
122
src/pkg/archive/tar/writer_test.go
Normal file
@ -0,0 +1,122 @@
|
|||||||
|
// Copyright 2009 The Go Authors. All rights reserved.
|
||||||
|
// Use of this source code is governed by a BSD-style
|
||||||
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
package tar
|
||||||
|
|
||||||
|
import (
|
||||||
|
"archive/tar";
|
||||||
|
"bytes";
|
||||||
|
"fmt";
|
||||||
|
"io";
|
||||||
|
"os";
|
||||||
|
"reflect";
|
||||||
|
"strings";
|
||||||
|
"testing";
|
||||||
|
)
|
||||||
|
|
||||||
|
type writerTestEntry struct {
|
||||||
|
header *Header;
|
||||||
|
contents string;
|
||||||
|
}
|
||||||
|
|
||||||
|
type writerTest struct {
|
||||||
|
file string; // filename of expected output
|
||||||
|
entries []*writerTestEntry;
|
||||||
|
}
|
||||||
|
|
||||||
|
var writerTests = []*writerTest{
|
||||||
|
&writerTest{
|
||||||
|
file: "testdata/writer.tar",
|
||||||
|
entries: []*writerTestEntry{
|
||||||
|
&writerTestEntry{
|
||||||
|
header: &Header{
|
||||||
|
Name: "small.txt",
|
||||||
|
Mode: 0640,
|
||||||
|
Uid: 73025,
|
||||||
|
Gid: 5000,
|
||||||
|
Size: 5,
|
||||||
|
Mtime: 1246508266,
|
||||||
|
Typeflag: '0',
|
||||||
|
Uname: "dsymonds",
|
||||||
|
Gname: "eng",
|
||||||
|
},
|
||||||
|
contents: `Kilts`,
|
||||||
|
},
|
||||||
|
&writerTestEntry{
|
||||||
|
header: &Header{
|
||||||
|
Name: "small2.txt",
|
||||||
|
Mode: 0640,
|
||||||
|
Uid: 73025,
|
||||||
|
Gid: 5000,
|
||||||
|
Size: 11,
|
||||||
|
Mtime: 1245217492,
|
||||||
|
Typeflag: '0',
|
||||||
|
Uname: "dsymonds",
|
||||||
|
Gname: "eng",
|
||||||
|
},
|
||||||
|
contents: "Google.com\n",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
// Render byte array in a two-character hexadecimal string, spaced for easy visual inspection.
|
||||||
|
func bytestr(b []byte) string {
|
||||||
|
s := fmt.Sprintf("(%d bytes)\n", len(b));
|
||||||
|
const rowLen = 32;
|
||||||
|
for i, ch := range b {
|
||||||
|
if i % rowLen == 0 {
|
||||||
|
// start of line: hex offset
|
||||||
|
s += fmt.Sprintf("%04x", i);
|
||||||
|
}
|
||||||
|
switch {
|
||||||
|
case '0' <= ch && ch <= '9', 'A' <= ch && ch <= 'Z', 'a' <= ch && ch <= 'z':
|
||||||
|
s += fmt.Sprintf(" %c", ch);
|
||||||
|
default:
|
||||||
|
s += fmt.Sprintf(" %02x", ch);
|
||||||
|
}
|
||||||
|
if (i + 1) % rowLen == 0 {
|
||||||
|
// end of line
|
||||||
|
s += "\n";
|
||||||
|
} else if (i + 1) % (rowLen / 2) == 0 {
|
||||||
|
// extra space
|
||||||
|
s += " ";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if s[len(s)-1] != '\n' {
|
||||||
|
s += "\n"
|
||||||
|
}
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestWriter(t *testing.T) {
|
||||||
|
testLoop:
|
||||||
|
for i, test := range writerTests {
|
||||||
|
expected, err := io.ReadFile(test.file);
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("test %d: Unexpected error: %v", i, err);
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
buf := new(bytes.Buffer);
|
||||||
|
tw := NewWriter(buf);
|
||||||
|
for j, entry := range test.entries {
|
||||||
|
if err := tw.WriteHeader(entry.header); err != nil {
|
||||||
|
t.Errorf("test %d, entry %d: Failed writing header: %v", i, j, err);
|
||||||
|
continue testLoop
|
||||||
|
}
|
||||||
|
if n, err := io.WriteString(tw, entry.contents); err != nil {
|
||||||
|
t.Errorf("test %d, entry %d: Failed writing contents: %v", i, j, err);
|
||||||
|
continue testLoop
|
||||||
|
}
|
||||||
|
}
|
||||||
|
tw.Close();
|
||||||
|
|
||||||
|
actual := buf.Data();
|
||||||
|
if !bytes.Equal(expected, actual) {
|
||||||
|
t.Errorf("test %d: Incorrect result:\n%v\nwant:\n%v",
|
||||||
|
i, bytestr(actual), bytestr(expected));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user