1// Copyright 2009 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
5package tar
6
7import (
8	"bytes"
9	"fmt"
10	"io"
11	"io/ioutil"
12	"math"
13	"os"
14	"reflect"
15	"sort"
16	"strings"
17	"testing"
18	"testing/iotest"
19	"time"
20)
21
22type writerTestEntry struct {
23	header   *Header
24	contents string
25}
26
27type writerTest struct {
28	file    string // filename of expected output
29	entries []*writerTestEntry
30}
31
32var writerTests = []*writerTest{
33	// The writer test file was produced with this command:
34	// tar (GNU tar) 1.26
35	//   ln -s small.txt link.txt
36	//   tar -b 1 --format=ustar -c -f writer.tar small.txt small2.txt link.txt
37	{
38		file: "testdata/writer.tar",
39		entries: []*writerTestEntry{
40			{
41				header: &Header{
42					Name:     "small.txt",
43					Mode:     0640,
44					Uid:      73025,
45					Gid:      5000,
46					Size:     5,
47					ModTime:  time.Unix(1246508266, 0),
48					Typeflag: '0',
49					Uname:    "dsymonds",
50					Gname:    "eng",
51				},
52				contents: "Kilts",
53			},
54			{
55				header: &Header{
56					Name:     "small2.txt",
57					Mode:     0640,
58					Uid:      73025,
59					Gid:      5000,
60					Size:     11,
61					ModTime:  time.Unix(1245217492, 0),
62					Typeflag: '0',
63					Uname:    "dsymonds",
64					Gname:    "eng",
65				},
66				contents: "Google.com\n",
67			},
68			{
69				header: &Header{
70					Name:     "link.txt",
71					Mode:     0777,
72					Uid:      1000,
73					Gid:      1000,
74					Size:     0,
75					ModTime:  time.Unix(1314603082, 0),
76					Typeflag: '2',
77					Linkname: "small.txt",
78					Uname:    "strings",
79					Gname:    "strings",
80				},
81				// no contents
82			},
83		},
84	},
85	// The truncated test file was produced using these commands:
86	//   dd if=/dev/zero bs=1048576 count=16384 > /tmp/16gig.txt
87	//   tar -b 1 -c -f- /tmp/16gig.txt | dd bs=512 count=8 > writer-big.tar
88	{
89		file: "testdata/writer-big.tar",
90		entries: []*writerTestEntry{
91			{
92				header: &Header{
93					Name:     "tmp/16gig.txt",
94					Mode:     0640,
95					Uid:      73025,
96					Gid:      5000,
97					Size:     16 << 30,
98					ModTime:  time.Unix(1254699560, 0),
99					Typeflag: '0',
100					Uname:    "dsymonds",
101					Gname:    "eng",
102				},
103				// fake contents
104				contents: strings.Repeat("\x00", 4<<10),
105			},
106		},
107	},
108	// The truncated test file was produced using these commands:
109	//   dd if=/dev/zero bs=1048576 count=16384 > (longname/)*15 /16gig.txt
110	//   tar -b 1 -c -f- (longname/)*15 /16gig.txt | dd bs=512 count=8 > writer-big-long.tar
111	{
112		file: "testdata/writer-big-long.tar",
113		entries: []*writerTestEntry{
114			{
115				header: &Header{
116					Name:     strings.Repeat("longname/", 15) + "16gig.txt",
117					Mode:     0644,
118					Uid:      1000,
119					Gid:      1000,
120					Size:     16 << 30,
121					ModTime:  time.Unix(1399583047, 0),
122					Typeflag: '0',
123					Uname:    "guillaume",
124					Gname:    "guillaume",
125				},
126				// fake contents
127				contents: strings.Repeat("\x00", 4<<10),
128			},
129		},
130	},
131	// This file was produced using gnu tar 1.17
132	// gnutar  -b 4 --format=ustar (longname/)*15 + file.txt
133	{
134		file: "testdata/ustar.tar",
135		entries: []*writerTestEntry{
136			{
137				header: &Header{
138					Name:     strings.Repeat("longname/", 15) + "file.txt",
139					Mode:     0644,
140					Uid:      0765,
141					Gid:      024,
142					Size:     06,
143					ModTime:  time.Unix(1360135598, 0),
144					Typeflag: '0',
145					Uname:    "shane",
146					Gname:    "staff",
147				},
148				contents: "hello\n",
149			},
150		},
151	},
152	// This file was produced using gnu tar 1.26
153	// echo "Slartibartfast" > file.txt
154	// ln file.txt hard.txt
155	// tar -b 1 --format=ustar -c -f hardlink.tar file.txt hard.txt
156	{
157		file: "testdata/hardlink.tar",
158		entries: []*writerTestEntry{
159			{
160				header: &Header{
161					Name:     "file.txt",
162					Mode:     0644,
163					Uid:      1000,
164					Gid:      100,
165					Size:     15,
166					ModTime:  time.Unix(1425484303, 0),
167					Typeflag: '0',
168					Uname:    "vbatts",
169					Gname:    "users",
170				},
171				contents: "Slartibartfast\n",
172			},
173			{
174				header: &Header{
175					Name:     "hard.txt",
176					Mode:     0644,
177					Uid:      1000,
178					Gid:      100,
179					Size:     0,
180					ModTime:  time.Unix(1425484303, 0),
181					Typeflag: '1',
182					Linkname: "file.txt",
183					Uname:    "vbatts",
184					Gname:    "users",
185				},
186				// no contents
187			},
188		},
189	},
190}
191
192// Render byte array in a two-character hexadecimal string, spaced for easy visual inspection.
193func bytestr(offset int, b []byte) string {
194	const rowLen = 32
195	s := fmt.Sprintf("%04x ", offset)
196	for _, ch := range b {
197		switch {
198		case '0' <= ch && ch <= '9', 'A' <= ch && ch <= 'Z', 'a' <= ch && ch <= 'z':
199			s += fmt.Sprintf("  %c", ch)
200		default:
201			s += fmt.Sprintf(" %02x", ch)
202		}
203	}
204	return s
205}
206
207// Render a pseudo-diff between two blocks of bytes.
208func bytediff(a []byte, b []byte) string {
209	const rowLen = 32
210	s := fmt.Sprintf("(%d bytes vs. %d bytes)\n", len(a), len(b))
211	for offset := 0; len(a)+len(b) > 0; offset += rowLen {
212		na, nb := rowLen, rowLen
213		if na > len(a) {
214			na = len(a)
215		}
216		if nb > len(b) {
217			nb = len(b)
218		}
219		sa := bytestr(offset, a[0:na])
220		sb := bytestr(offset, b[0:nb])
221		if sa != sb {
222			s += fmt.Sprintf("-%v\n+%v\n", sa, sb)
223		}
224		a = a[na:]
225		b = b[nb:]
226	}
227	return s
228}
229
230func TestWriter(t *testing.T) {
231testLoop:
232	for i, test := range writerTests {
233		expected, err := ioutil.ReadFile(test.file)
234		if err != nil {
235			t.Errorf("test %d: Unexpected error: %v", i, err)
236			continue
237		}
238
239		buf := new(bytes.Buffer)
240		tw := NewWriter(iotest.TruncateWriter(buf, 4<<10)) // only catch the first 4 KB
241		big := false
242		for j, entry := range test.entries {
243			big = big || entry.header.Size > 1<<10
244			if err := tw.WriteHeader(entry.header); err != nil {
245				t.Errorf("test %d, entry %d: Failed writing header: %v", i, j, err)
246				continue testLoop
247			}
248			if _, err := io.WriteString(tw, entry.contents); err != nil {
249				t.Errorf("test %d, entry %d: Failed writing contents: %v", i, j, err)
250				continue testLoop
251			}
252		}
253		// Only interested in Close failures for the small tests.
254		if err := tw.Close(); err != nil && !big {
255			t.Errorf("test %d: Failed closing archive: %v", i, err)
256			continue testLoop
257		}
258
259		actual := buf.Bytes()
260		if !bytes.Equal(expected, actual) {
261			t.Errorf("test %d: Incorrect result: (-=expected, +=actual)\n%v",
262				i, bytediff(expected, actual))
263		}
264		if testing.Short() { // The second test is expensive.
265			break
266		}
267	}
268}
269
270func TestPax(t *testing.T) {
271	// Create an archive with a large name
272	fileinfo, err := os.Stat("testdata/small.txt")
273	if err != nil {
274		t.Fatal(err)
275	}
276	hdr, err := FileInfoHeader(fileinfo, "")
277	if err != nil {
278		t.Fatalf("os.Stat: %v", err)
279	}
280	// Force a PAX long name to be written
281	longName := strings.Repeat("ab", 100)
282	contents := strings.Repeat(" ", int(hdr.Size))
283	hdr.Name = longName
284	var buf bytes.Buffer
285	writer := NewWriter(&buf)
286	if err := writer.WriteHeader(hdr); err != nil {
287		t.Fatal(err)
288	}
289	if _, err = writer.Write([]byte(contents)); err != nil {
290		t.Fatal(err)
291	}
292	if err := writer.Close(); err != nil {
293		t.Fatal(err)
294	}
295	// Simple test to make sure PAX extensions are in effect
296	if !bytes.Contains(buf.Bytes(), []byte("PaxHeaders.0")) {
297		t.Fatal("Expected at least one PAX header to be written.")
298	}
299	// Test that we can get a long name back out of the archive.
300	reader := NewReader(&buf)
301	hdr, err = reader.Next()
302	if err != nil {
303		t.Fatal(err)
304	}
305	if hdr.Name != longName {
306		t.Fatal("Couldn't recover long file name")
307	}
308}
309
310func TestPaxSymlink(t *testing.T) {
311	// Create an archive with a large linkname
312	fileinfo, err := os.Stat("testdata/small.txt")
313	if err != nil {
314		t.Fatal(err)
315	}
316	hdr, err := FileInfoHeader(fileinfo, "")
317	hdr.Typeflag = TypeSymlink
318	if err != nil {
319		t.Fatalf("os.Stat:1 %v", err)
320	}
321	// Force a PAX long linkname to be written
322	longLinkname := strings.Repeat("1234567890/1234567890", 10)
323	hdr.Linkname = longLinkname
324
325	hdr.Size = 0
326	var buf bytes.Buffer
327	writer := NewWriter(&buf)
328	if err := writer.WriteHeader(hdr); err != nil {
329		t.Fatal(err)
330	}
331	if err := writer.Close(); err != nil {
332		t.Fatal(err)
333	}
334	// Simple test to make sure PAX extensions are in effect
335	if !bytes.Contains(buf.Bytes(), []byte("PaxHeaders.0")) {
336		t.Fatal("Expected at least one PAX header to be written.")
337	}
338	// Test that we can get a long name back out of the archive.
339	reader := NewReader(&buf)
340	hdr, err = reader.Next()
341	if err != nil {
342		t.Fatal(err)
343	}
344	if hdr.Linkname != longLinkname {
345		t.Fatal("Couldn't recover long link name")
346	}
347}
348
349func TestPaxNonAscii(t *testing.T) {
350	// Create an archive with non ascii. These should trigger a pax header
351	// because pax headers have a defined utf-8 encoding.
352	fileinfo, err := os.Stat("testdata/small.txt")
353	if err != nil {
354		t.Fatal(err)
355	}
356
357	hdr, err := FileInfoHeader(fileinfo, "")
358	if err != nil {
359		t.Fatalf("os.Stat:1 %v", err)
360	}
361
362	// some sample data
363	chineseFilename := "文件名"
364	chineseGroupname := "組"
365	chineseUsername := "用戶名"
366
367	hdr.Name = chineseFilename
368	hdr.Gname = chineseGroupname
369	hdr.Uname = chineseUsername
370
371	contents := strings.Repeat(" ", int(hdr.Size))
372
373	var buf bytes.Buffer
374	writer := NewWriter(&buf)
375	if err := writer.WriteHeader(hdr); err != nil {
376		t.Fatal(err)
377	}
378	if _, err = writer.Write([]byte(contents)); err != nil {
379		t.Fatal(err)
380	}
381	if err := writer.Close(); err != nil {
382		t.Fatal(err)
383	}
384	// Simple test to make sure PAX extensions are in effect
385	if !bytes.Contains(buf.Bytes(), []byte("PaxHeaders.0")) {
386		t.Fatal("Expected at least one PAX header to be written.")
387	}
388	// Test that we can get a long name back out of the archive.
389	reader := NewReader(&buf)
390	hdr, err = reader.Next()
391	if err != nil {
392		t.Fatal(err)
393	}
394	if hdr.Name != chineseFilename {
395		t.Fatal("Couldn't recover unicode name")
396	}
397	if hdr.Gname != chineseGroupname {
398		t.Fatal("Couldn't recover unicode group")
399	}
400	if hdr.Uname != chineseUsername {
401		t.Fatal("Couldn't recover unicode user")
402	}
403}
404
405func TestPaxXattrs(t *testing.T) {
406	xattrs := map[string]string{
407		"user.key": "value",
408	}
409
410	// Create an archive with an xattr
411	fileinfo, err := os.Stat("testdata/small.txt")
412	if err != nil {
413		t.Fatal(err)
414	}
415	hdr, err := FileInfoHeader(fileinfo, "")
416	if err != nil {
417		t.Fatalf("os.Stat: %v", err)
418	}
419	contents := "Kilts"
420	hdr.Xattrs = xattrs
421	var buf bytes.Buffer
422	writer := NewWriter(&buf)
423	if err := writer.WriteHeader(hdr); err != nil {
424		t.Fatal(err)
425	}
426	if _, err = writer.Write([]byte(contents)); err != nil {
427		t.Fatal(err)
428	}
429	if err := writer.Close(); err != nil {
430		t.Fatal(err)
431	}
432	// Test that we can get the xattrs back out of the archive.
433	reader := NewReader(&buf)
434	hdr, err = reader.Next()
435	if err != nil {
436		t.Fatal(err)
437	}
438	if !reflect.DeepEqual(hdr.Xattrs, xattrs) {
439		t.Fatalf("xattrs did not survive round trip: got %+v, want %+v",
440			hdr.Xattrs, xattrs)
441	}
442}
443
444func TestPaxHeadersSorted(t *testing.T) {
445	fileinfo, err := os.Stat("testdata/small.txt")
446	if err != nil {
447		t.Fatal(err)
448	}
449	hdr, err := FileInfoHeader(fileinfo, "")
450	if err != nil {
451		t.Fatalf("os.Stat: %v", err)
452	}
453	contents := strings.Repeat(" ", int(hdr.Size))
454
455	hdr.Xattrs = map[string]string{
456		"foo": "foo",
457		"bar": "bar",
458		"baz": "baz",
459		"qux": "qux",
460	}
461
462	var buf bytes.Buffer
463	writer := NewWriter(&buf)
464	if err := writer.WriteHeader(hdr); err != nil {
465		t.Fatal(err)
466	}
467	if _, err = writer.Write([]byte(contents)); err != nil {
468		t.Fatal(err)
469	}
470	if err := writer.Close(); err != nil {
471		t.Fatal(err)
472	}
473	// Simple test to make sure PAX extensions are in effect
474	if !bytes.Contains(buf.Bytes(), []byte("PaxHeaders.0")) {
475		t.Fatal("Expected at least one PAX header to be written.")
476	}
477
478	// xattr bar should always appear before others
479	indices := []int{
480		bytes.Index(buf.Bytes(), []byte("bar=bar")),
481		bytes.Index(buf.Bytes(), []byte("baz=baz")),
482		bytes.Index(buf.Bytes(), []byte("foo=foo")),
483		bytes.Index(buf.Bytes(), []byte("qux=qux")),
484	}
485	if !sort.IntsAreSorted(indices) {
486		t.Fatal("PAX headers are not sorted")
487	}
488}
489
490func TestUSTARLongName(t *testing.T) {
491	// Create an archive with a path that failed to split with USTAR extension in previous versions.
492	fileinfo, err := os.Stat("testdata/small.txt")
493	if err != nil {
494		t.Fatal(err)
495	}
496	hdr, err := FileInfoHeader(fileinfo, "")
497	hdr.Typeflag = TypeDir
498	if err != nil {
499		t.Fatalf("os.Stat:1 %v", err)
500	}
501	// Force a PAX long name to be written. The name was taken from a practical example
502	// that fails and replaced ever char through numbers to anonymize the sample.
503	longName := "/0000_0000000/00000-000000000/0000_0000000/00000-0000000000000/0000_0000000/00000-0000000-00000000/0000_0000000/00000000/0000_0000000/000/0000_0000000/00000000v00/0000_0000000/000000/0000_0000000/0000000/0000_0000000/00000y-00/0000/0000/00000000/0x000000/"
504	hdr.Name = longName
505
506	hdr.Size = 0
507	var buf bytes.Buffer
508	writer := NewWriter(&buf)
509	if err := writer.WriteHeader(hdr); err != nil {
510		t.Fatal(err)
511	}
512	if err := writer.Close(); err != nil {
513		t.Fatal(err)
514	}
515	// Test that we can get a long name back out of the archive.
516	reader := NewReader(&buf)
517	hdr, err = reader.Next()
518	if err != nil {
519		t.Fatal(err)
520	}
521	if hdr.Name != longName {
522		t.Fatal("Couldn't recover long name")
523	}
524}
525
526func TestValidTypeflagWithPAXHeader(t *testing.T) {
527	var buffer bytes.Buffer
528	tw := NewWriter(&buffer)
529
530	fileName := strings.Repeat("ab", 100)
531
532	hdr := &Header{
533		Name:     fileName,
534		Size:     4,
535		Typeflag: 0,
536	}
537	if err := tw.WriteHeader(hdr); err != nil {
538		t.Fatalf("Failed to write header: %s", err)
539	}
540	if _, err := tw.Write([]byte("fooo")); err != nil {
541		t.Fatalf("Failed to write the file's data: %s", err)
542	}
543	tw.Close()
544
545	tr := NewReader(&buffer)
546
547	for {
548		header, err := tr.Next()
549		if err == io.EOF {
550			break
551		}
552		if err != nil {
553			t.Fatalf("Failed to read header: %s", err)
554		}
555		if header.Typeflag != 0 {
556			t.Fatalf("Typeflag should've been 0, found %d", header.Typeflag)
557		}
558	}
559}
560
561func TestWriteAfterClose(t *testing.T) {
562	var buffer bytes.Buffer
563	tw := NewWriter(&buffer)
564
565	hdr := &Header{
566		Name: "small.txt",
567		Size: 5,
568	}
569	if err := tw.WriteHeader(hdr); err != nil {
570		t.Fatalf("Failed to write header: %s", err)
571	}
572	tw.Close()
573	if _, err := tw.Write([]byte("Kilts")); err != ErrWriteAfterClose {
574		t.Fatalf("Write: got %v; want ErrWriteAfterClose", err)
575	}
576}
577
578func TestSplitUSTARPath(t *testing.T) {
579	var sr = strings.Repeat
580
581	var vectors = []struct {
582		input  string // Input path
583		prefix string // Expected output prefix
584		suffix string // Expected output suffix
585		ok     bool   // Split success?
586	}{
587		{"", "", "", false},
588		{"abc", "", "", false},
589		{"用戶名", "", "", false},
590		{sr("a", fileNameSize), "", "", false},
591		{sr("a", fileNameSize) + "/", "", "", false},
592		{sr("a", fileNameSize) + "/a", sr("a", fileNameSize), "a", true},
593		{sr("a", fileNamePrefixSize) + "/", "", "", false},
594		{sr("a", fileNamePrefixSize) + "/a", sr("a", fileNamePrefixSize), "a", true},
595		{sr("a", fileNameSize+1), "", "", false},
596		{sr("/", fileNameSize+1), sr("/", fileNameSize-1), "/", true},
597		{sr("a", fileNamePrefixSize) + "/" + sr("b", fileNameSize),
598			sr("a", fileNamePrefixSize), sr("b", fileNameSize), true},
599		{sr("a", fileNamePrefixSize) + "//" + sr("b", fileNameSize), "", "", false},
600		{sr("a/", fileNameSize), sr("a/", 77) + "a", sr("a/", 22), true},
601	}
602
603	for _, v := range vectors {
604		prefix, suffix, ok := splitUSTARPath(v.input)
605		if prefix != v.prefix || suffix != v.suffix || ok != v.ok {
606			t.Errorf("splitUSTARPath(%q):\ngot  (%q, %q, %v)\nwant (%q, %q, %v)",
607				v.input, prefix, suffix, ok, v.prefix, v.suffix, v.ok)
608		}
609	}
610}
611
612func TestFormatPAXRecord(t *testing.T) {
613	var medName = strings.Repeat("CD", 50)
614	var longName = strings.Repeat("AB", 100)
615
616	var vectors = []struct {
617		inputKey string
618		inputVal string
619		output   string
620	}{
621		{"k", "v", "6 k=v\n"},
622		{"path", "/etc/hosts", "19 path=/etc/hosts\n"},
623		{"path", longName, "210 path=" + longName + "\n"},
624		{"path", medName, "110 path=" + medName + "\n"},
625		{"foo", "ba", "9 foo=ba\n"},
626		{"foo", "bar", "11 foo=bar\n"},
627		{"foo", "b=\nar=\n==\x00", "18 foo=b=\nar=\n==\x00\n"},
628		{"foo", "hello9 foo=ba\nworld", "27 foo=hello9 foo=ba\nworld\n"},
629		{"☺☻☹", "日a本b語ç", "27 ☺☻☹=日a本b語ç\n"},
630		{"\x00hello", "\x00world", "17 \x00hello=\x00world\n"},
631	}
632
633	for _, v := range vectors {
634		output := formatPAXRecord(v.inputKey, v.inputVal)
635		if output != v.output {
636			t.Errorf("formatPAXRecord(%q, %q): got %q, want %q",
637				v.inputKey, v.inputVal, output, v.output)
638		}
639	}
640}
641
642func TestFitsInBase256(t *testing.T) {
643	var vectors = []struct {
644		input int64
645		width int
646		ok    bool
647	}{
648		{+1, 8, true},
649		{0, 8, true},
650		{-1, 8, true},
651		{1 << 56, 8, false},
652		{(1 << 56) - 1, 8, true},
653		{-1 << 56, 8, true},
654		{(-1 << 56) - 1, 8, false},
655		{121654, 8, true},
656		{-9849849, 8, true},
657		{math.MaxInt64, 9, true},
658		{0, 9, true},
659		{math.MinInt64, 9, true},
660		{math.MaxInt64, 12, true},
661		{0, 12, true},
662		{math.MinInt64, 12, true},
663	}
664
665	for _, v := range vectors {
666		ok := fitsInBase256(v.width, v.input)
667		if ok != v.ok {
668			t.Errorf("checkNumeric(%d, %d): got %v, want %v", v.input, v.width, ok, v.ok)
669		}
670	}
671}
672
673func TestFormatNumeric(t *testing.T) {
674	var vectors = []struct {
675		input  int64
676		output string
677		ok     bool
678	}{
679		// Test base-256 (binary) encoded values.
680		{-1, "\xff", true},
681		{-1, "\xff\xff", true},
682		{-1, "\xff\xff\xff", true},
683		{(1 << 0), "0", false},
684		{(1 << 8) - 1, "\x80\xff", true},
685		{(1 << 8), "0\x00", false},
686		{(1 << 16) - 1, "\x80\xff\xff", true},
687		{(1 << 16), "00\x00", false},
688		{-1 * (1 << 0), "\xff", true},
689		{-1*(1<<0) - 1, "0", false},
690		{-1 * (1 << 8), "\xff\x00", true},
691		{-1*(1<<8) - 1, "0\x00", false},
692		{-1 * (1 << 16), "\xff\x00\x00", true},
693		{-1*(1<<16) - 1, "00\x00", false},
694		{537795476381659745, "0000000\x00", false},
695		{537795476381659745, "\x80\x00\x00\x00\x07\x76\xa2\x22\xeb\x8a\x72\x61", true},
696		{-615126028225187231, "0000000\x00", false},
697		{-615126028225187231, "\xff\xff\xff\xff\xf7\x76\xa2\x22\xeb\x8a\x72\x61", true},
698		{math.MaxInt64, "0000000\x00", false},
699		{math.MaxInt64, "\x80\x00\x00\x00\x7f\xff\xff\xff\xff\xff\xff\xff", true},
700		{math.MinInt64, "0000000\x00", false},
701		{math.MinInt64, "\xff\xff\xff\xff\x80\x00\x00\x00\x00\x00\x00\x00", true},
702		{math.MaxInt64, "\x80\x7f\xff\xff\xff\xff\xff\xff\xff", true},
703		{math.MinInt64, "\xff\x80\x00\x00\x00\x00\x00\x00\x00", true},
704	}
705
706	for _, v := range vectors {
707		var f formatter
708		output := make([]byte, len(v.output))
709		f.formatNumeric(output, v.input)
710		ok := (f.err == nil)
711		if ok != v.ok {
712			if v.ok {
713				t.Errorf("formatNumeric(%d): got formatting failure, want success", v.input)
714			} else {
715				t.Errorf("formatNumeric(%d): got formatting success, want failure", v.input)
716			}
717		}
718		if string(output) != v.output {
719			t.Errorf("formatNumeric(%d): got %q, want %q", v.input, output, v.output)
720		}
721	}
722}
723