From d6d0a3905062a00b201e089dcbd9a6c42a6a4dcf Mon Sep 17 00:00:00 2001 From: David Symonds Date: Wed, 8 Jul 2009 17:15:18 -0700 Subject: [PATCH] Basic POSIX-compatible tar writer. R=rsc APPROVED=rsc DELTA=456 (382 added, 66 deleted, 8 changed) OCL=31246 CL=31372 --- src/pkg/archive/tar/Makefile | 16 ++- src/pkg/archive/tar/common.go | 74 ++++++++++ src/pkg/archive/tar/testdata/writer.tar | Bin 0 -> 3072 bytes src/pkg/archive/tar/untar.go | 73 +--------- src/pkg/archive/tar/untar_test.go | 4 +- src/pkg/archive/tar/writer.go | 180 ++++++++++++++++++++++++ src/pkg/archive/tar/writer_test.go | 122 ++++++++++++++++ 7 files changed, 395 insertions(+), 74 deletions(-) create mode 100644 src/pkg/archive/tar/common.go create mode 100644 src/pkg/archive/tar/testdata/writer.tar create mode 100644 src/pkg/archive/tar/writer.go create mode 100644 src/pkg/archive/tar/writer_test.go diff --git a/src/pkg/archive/tar/Makefile b/src/pkg/archive/tar/Makefile index 579ed4c351..2689b30f6a 100644 --- a/src/pkg/archive/tar/Makefile +++ b/src/pkg/archive/tar/Makefile @@ -2,6 +2,7 @@ # Use of this source code is governed by a BSD-style # license that can be found in the LICENSE file. + # DO NOT EDIT. Automatically generated by gobuild. # gobuild -m >Makefile @@ -20,7 +21,7 @@ test: packages coverage: packages gotest - 6cov -g `pwd` | grep -v '_test\.go:' + 6cov -g $$(pwd) | grep -v '_test\.go:' %.$O: %.go $(GC) -I_obj $*.go @@ -32,16 +33,24 @@ coverage: packages $(AS) $*.s O1=\ + common.$O\ + +O2=\ untar.$O\ + writer.$O\ -phases: a1 +phases: a1 a2 _obj$D/tar.a: phases a1: $(O1) - $(AR) grc _obj$D/tar.a untar.$O + $(AR) grc _obj$D/tar.a common.$O rm -f $(O1) +a2: $(O2) + $(AR) grc _obj$D/tar.a untar.$O writer.$O + rm -f $(O2) + newpkg: clean mkdir -p _obj$D @@ -49,6 +58,7 @@ newpkg: clean $(O1): newpkg $(O2): a1 +$(O3): a2 nuke: clean rm -f $(GOROOT)/pkg/$(GOOS)_$(GOARCH)$D/tar.a diff --git a/src/pkg/archive/tar/common.go b/src/pkg/archive/tar/common.go new file mode 100644 index 0000000000..182d465278 --- /dev/null +++ b/src/pkg/archive/tar/common.go @@ -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 +} diff --git a/src/pkg/archive/tar/testdata/writer.tar b/src/pkg/archive/tar/testdata/writer.tar new file mode 100644 index 0000000000000000000000000000000000000000..0358f91b98b3c72134167f4917d567f53fa1b9a2 GIT binary patch literal 3072 zcmeHHTMEK35X`ry@C0Rd^Y93Mc#2|65t0T>1iih9sHFw-)j*M*uWTkPvlC{6YpreK zJ&JmO1fxIzq0xW>0MTHozt94aNP;A(NdQU(AofsC($x7DVtY+ey6|v*?83y%PyIN| zFBa8%md{WNcC+Q_V&l)5H>;Y3t>r+-`KP)IAV;H^f0Q}@c_*MG^~%3KaQ@f&<5Hia Y_k-=5vv<4FGv?R 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 +} diff --git a/src/pkg/archive/tar/writer_test.go b/src/pkg/archive/tar/writer_test.go new file mode 100644 index 0000000000..202530a0f0 --- /dev/null +++ b/src/pkg/archive/tar/writer_test.go @@ -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)); + } + } +}