Add write support for the GNU tar binary numeric field extension.

R=rsc
APPROVED=rsc
DELTA=102  (89 added, 1 deleted, 12 changed)
OCL=35321
CL=35327
diff --git a/src/pkg/archive/tar/testdata/writer-big.tar b/src/pkg/archive/tar/testdata/writer-big.tar
new file mode 100644
index 0000000..753e883
--- /dev/null
+++ b/src/pkg/archive/tar/testdata/writer-big.tar
Binary files differ
diff --git a/src/pkg/archive/tar/writer.go b/src/pkg/archive/tar/writer.go
index 42e628f..745a7c4 100644
--- a/src/pkg/archive/tar/writer.go
+++ b/src/pkg/archive/tar/writer.go
@@ -16,9 +16,8 @@
 )
 
 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");
+	ErrWriteTooLong = os.NewError("write too long");
+	ErrFieldTooLong = os.NewError("header field too long");
 )
 
 // A Writer provides sequential writing of a tar archive in POSIX.1 format.
@@ -42,6 +41,7 @@
 	nb int64;	// number of unwritten bytes for current file entry
 	pad int64;	// amount of padding to write after current file entry
 	closed bool;
+	usedBinary bool;	// whether the binary numeric field extension was used
 }
 
 // NewWriter creates a new Writer writing to w.
@@ -70,7 +70,7 @@
 func (tw *Writer) cString(b []byte, s string) {
 	if len(s) > len(b) {
 		if tw.err == nil {
-			tw.err = ErrIntFieldTooBig;
+			tw.err = ErrFieldTooLong;
 		}
 		return
 	}
@@ -92,6 +92,23 @@
 	tw.cString(b, s);
 }
 
+// Write x into b, either as octal or as binary (GNUtar/star extension).
+func (tw *Writer) numeric(b []byte, x int64) {
+	// Try octal first.
+	s := strconv.Itob64(x, 8);
+	if len(s) < len(b) {
+		tw.octal(b, x);
+		return
+	}
+	// Too big: use binary (big-endian).
+	tw.usedBinary = true;
+	for i := len(b)-1; x > 0 && i >= 0; i-- {
+		b[i] = byte(x);
+		x >>= 8;
+	}
+	b[0] |= 0x80;  // highest bit indicates binary format
+}
+
 // 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 {
@@ -112,18 +129,23 @@
 	bytes.Copy(s.next(100), strings.Bytes(hdr.Name));
 
 	tw.octal(s.next(8), hdr.Mode);	// 100:108
-	tw.octal(s.next(8), hdr.Uid);	// 108:116
-	tw.octal(s.next(8), hdr.Gid);	// 116:124
-	tw.octal(s.next(12), hdr.Size);	// 124:136
-	tw.octal(s.next(12), hdr.Mtime);	// 136:148
+	tw.numeric(s.next(8), hdr.Uid);	// 108:116
+	tw.numeric(s.next(8), hdr.Gid);	// 116:124
+	tw.numeric(s.next(12), hdr.Size);	// 124:136
+	tw.numeric(s.next(12), hdr.Mtime);	// 136:148
 	s.next(8);  // chksum (148:156)
 	s.next(1)[0] = hdr.Typeflag;	// 156:157
 	s.next(100);  // linkname (157:257)
 	bytes.Copy(s.next(8), strings.Bytes("ustar\x0000"));	// 257:265
 	tw.cString(s.next(32), hdr.Uname);	// 265:297
 	tw.cString(s.next(32), hdr.Gname);	// 297:329
-	tw.octal(s.next(8), hdr.Devmajor);	// 329:337
-	tw.octal(s.next(8), hdr.Devminor);	// 337:345
+	tw.numeric(s.next(8), hdr.Devmajor);	// 329:337
+	tw.numeric(s.next(8), hdr.Devminor);	// 337:345
+
+	// Use the GNU magic instead of POSIX magic if we used any GNU extensions.
+	if tw.usedBinary {
+		bytes.Copy(header[257:265], strings.Bytes("ustar  \x00"));
+	}
 
 	// The chksum field is terminated by a NUL and a space.
 	// This is different from the other octal fields.
diff --git a/src/pkg/archive/tar/writer_test.go b/src/pkg/archive/tar/writer_test.go
index 69f069f..cd67fed 100644
--- a/src/pkg/archive/tar/writer_test.go
+++ b/src/pkg/archive/tar/writer_test.go
@@ -9,6 +9,7 @@
 	"fmt";
 	"io";
 	"testing";
+	"testing/iotest";
 )
 
 type writerTestEntry struct {
@@ -37,7 +38,7 @@
 					Uname: "dsymonds",
 					Gname: "eng",
 				},
-				contents: `Kilts`,
+				contents: "Kilts",
 			},
 			&writerTestEntry{
 				header: &Header{
@@ -55,6 +56,28 @@
 			},
 		}
 	},
+	// The truncated test file was produced using these commands:
+	//   dd if=/dev/zero bs=1048576 count=16384 > /tmp/16gig.txt
+	//   tar -b 1 -c -f- /tmp/16gig.txt | dd bs=512 count=8 > writer-big.tar
+	&writerTest{
+		file: "testdata/writer-big.tar",
+		entries: []*writerTestEntry{
+			&writerTestEntry{
+				header: &Header{
+					Name: "tmp/16gig.txt",
+					Mode: 0640,
+					Uid: 73025,
+					Gid: 5000,
+					Size: 16 << 30,
+					Mtime: 1254699560,
+					Typeflag: '0',
+					Uname: "dsymonds",
+					Gname: "eng",
+				},
+				// no contents
+			},
+		},
+	},
 }
 
 // Render byte array in a two-character hexadecimal string, spaced for easy visual inspection.
@@ -105,7 +128,7 @@
 		}
 
 		buf := new(bytes.Buffer);
-		tw := NewWriter(buf);
+		tw := NewWriter(iotest.TruncateWriter(buf, 4 << 10));  // only catch the first 4 KB
 		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);
@@ -116,7 +139,10 @@
 				continue testLoop
 			}
 		}
-		tw.Close();
+		if err := tw.Close(); err != nil {
+			t.Errorf("test %d: Failed closing archive: %v", err);
+			continue testLoop
+		}
 
 		actual := buf.Bytes();
 		if !bytes.Equal(expected, actual) {