Giant blob of minor changes
[dotfiles/.git] / .config / coc / extensions / coc-go-data / tools / pkg / mod / golang.org / x / mod@v0.3.0 / zip / zip_test.go
1 // Copyright 2019 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
5 package zip_test
6
7 import (
8         "archive/zip"
9         "bytes"
10         "crypto/sha256"
11         "encoding/hex"
12         "fmt"
13         "io"
14         "io/ioutil"
15         "os"
16         "os/exec"
17         "path"
18         "path/filepath"
19         "runtime"
20         "strings"
21         "sync/atomic"
22         "testing"
23         "time"
24
25         "golang.org/x/mod/module"
26         "golang.org/x/mod/sumdb/dirhash"
27         modzip "golang.org/x/mod/zip"
28         "golang.org/x/tools/txtar"
29 )
30
31 const emptyHash = "h1:47DEQpj8HBSa+/TImW+5JCeuQeRkm5NMpJWZG3hSuFU="
32
33 type testParams struct {
34         path, version, wantErr, hash string
35         archive                      *txtar.Archive
36 }
37
38 // readTest loads a test from a txtar file. The comment section of the file
39 // should contain lines with key=value pairs. Valid keys are the field names
40 // from testParams.
41 func readTest(file string) (testParams, error) {
42         var test testParams
43         var err error
44         test.archive, err = txtar.ParseFile(file)
45         if err != nil {
46                 return testParams{}, err
47         }
48
49         lines := strings.Split(string(test.archive.Comment), "\n")
50         for n, line := range lines {
51                 n++ // report line numbers starting with 1
52                 if i := strings.IndexByte(line, '#'); i >= 0 {
53                         line = line[:i]
54                 }
55                 line = strings.TrimSpace(line)
56                 if line == "" {
57                         continue
58                 }
59                 eq := strings.IndexByte(line, '=')
60                 if eq < 0 {
61                         return testParams{}, fmt.Errorf("%s:%d: missing = separator", file, n)
62                 }
63                 key, value := strings.TrimSpace(line[:eq]), strings.TrimSpace(line[eq+1:])
64                 switch key {
65                 case "path":
66                         test.path = value
67                 case "version":
68                         test.version = value
69                 case "wantErr":
70                         test.wantErr = value
71                 case "hash":
72                         test.hash = value
73                 default:
74                         return testParams{}, fmt.Errorf("%s:%d: unknown key %q", file, n, key)
75                 }
76         }
77
78         return test, nil
79 }
80
81 type fakeFile struct {
82         name string
83         size uint64
84         data []byte // if nil, Open will access a sequence of 0-bytes
85 }
86
87 func (f fakeFile) Path() string                { return f.name }
88 func (f fakeFile) Lstat() (os.FileInfo, error) { return fakeFileInfo{f}, nil }
89 func (f fakeFile) Open() (io.ReadCloser, error) {
90         if f.data != nil {
91                 return ioutil.NopCloser(bytes.NewReader(f.data)), nil
92         }
93         if f.size >= uint64(modzip.MaxZipFile<<1) {
94                 return nil, fmt.Errorf("cannot open fakeFile of size %d", f.size)
95         }
96         return ioutil.NopCloser(io.LimitReader(zeroReader{}, int64(f.size))), nil
97 }
98
99 type fakeFileInfo struct {
100         f fakeFile
101 }
102
103 func (fi fakeFileInfo) Name() string       { return path.Base(fi.f.name) }
104 func (fi fakeFileInfo) Size() int64        { return int64(fi.f.size) }
105 func (fi fakeFileInfo) Mode() os.FileMode  { return 0644 }
106 func (fi fakeFileInfo) ModTime() time.Time { return time.Time{} }
107 func (fi fakeFileInfo) IsDir() bool        { return false }
108 func (fi fakeFileInfo) Sys() interface{}   { return nil }
109
110 type zeroReader struct{}
111
112 func (r zeroReader) Read(b []byte) (int, error) {
113         for i := range b {
114                 b[i] = 0
115         }
116         return len(b), nil
117 }
118
119 func TestCreate(t *testing.T) {
120         testDir := filepath.FromSlash("testdata/create")
121         testInfos, err := ioutil.ReadDir(testDir)
122         if err != nil {
123                 t.Fatal(err)
124         }
125         for _, testInfo := range testInfos {
126                 testInfo := testInfo
127                 base := filepath.Base(testInfo.Name())
128                 if filepath.Ext(base) != ".txt" {
129                         continue
130                 }
131                 t.Run(base[:len(base)-len(".txt")], func(t *testing.T) {
132                         t.Parallel()
133
134                         // Load the test.
135                         testPath := filepath.Join(testDir, testInfo.Name())
136                         test, err := readTest(testPath)
137                         if err != nil {
138                                 t.Fatal(err)
139                         }
140
141                         // Write zip to temporary file.
142                         tmpZip, err := ioutil.TempFile("", "TestCreate-*.zip")
143                         if err != nil {
144                                 t.Fatal(err)
145                         }
146                         tmpZipPath := tmpZip.Name()
147                         defer func() {
148                                 tmpZip.Close()
149                                 os.Remove(tmpZipPath)
150                         }()
151                         m := module.Version{Path: test.path, Version: test.version}
152                         files := make([]modzip.File, len(test.archive.Files))
153                         for i, tf := range test.archive.Files {
154                                 files[i] = fakeFile{
155                                         name: tf.Name,
156                                         size: uint64(len(tf.Data)),
157                                         data: tf.Data,
158                                 }
159                         }
160                         if err := modzip.Create(tmpZip, m, files); err != nil {
161                                 if test.wantErr == "" {
162                                         t.Fatalf("unexpected error: %v", err)
163                                 } else if !strings.Contains(err.Error(), test.wantErr) {
164                                         t.Fatalf("got error %q; want error containing %q", err.Error(), test.wantErr)
165                                 } else {
166                                         return
167                                 }
168                         } else if test.wantErr != "" {
169                                 t.Fatalf("unexpected success; wanted error containing %q", test.wantErr)
170                         }
171                         if err := tmpZip.Close(); err != nil {
172                                 t.Fatal(err)
173                         }
174
175                         // Hash zip file, compare with known value.
176                         if hash, err := dirhash.HashZip(tmpZipPath, dirhash.Hash1); err != nil {
177                                 t.Fatal(err)
178                         } else if hash != test.hash {
179                                 t.Fatalf("got hash: %q\nwant: %q", hash, test.hash)
180                         }
181                 })
182         }
183 }
184
185 func TestCreateFromDir(t *testing.T) {
186         testDir := filepath.FromSlash("testdata/create_from_dir")
187         testInfos, err := ioutil.ReadDir(testDir)
188         if err != nil {
189                 t.Fatal(err)
190         }
191         for _, testInfo := range testInfos {
192                 testInfo := testInfo
193                 base := filepath.Base(testInfo.Name())
194                 if filepath.Ext(base) != ".txt" {
195                         continue
196                 }
197                 t.Run(base[:len(base)-len(".txt")], func(t *testing.T) {
198                         t.Parallel()
199
200                         // Load the test.
201                         testPath := filepath.Join(testDir, testInfo.Name())
202                         test, err := readTest(testPath)
203                         if err != nil {
204                                 t.Fatal(err)
205                         }
206
207                         // Write files to a temporary directory.
208                         tmpDir, err := ioutil.TempDir("", "TestCreateFromDir")
209                         if err != nil {
210                                 t.Fatal(err)
211                         }
212                         defer os.RemoveAll(tmpDir)
213                         for _, f := range test.archive.Files {
214                                 filePath := filepath.Join(tmpDir, f.Name)
215                                 if err := os.MkdirAll(filepath.Dir(filePath), 0777); err != nil {
216                                         t.Fatal(err)
217                                 }
218                                 if err := ioutil.WriteFile(filePath, f.Data, 0666); err != nil {
219                                         t.Fatal(err)
220                                 }
221                         }
222
223                         // Create zip from the directory.
224                         tmpZip, err := ioutil.TempFile("", "TestCreateFromDir-*.zip")
225                         if err != nil {
226                                 t.Fatal(err)
227                         }
228                         tmpZipPath := tmpZip.Name()
229                         defer func() {
230                                 tmpZip.Close()
231                                 os.Remove(tmpZipPath)
232                         }()
233                         m := module.Version{Path: test.path, Version: test.version}
234                         if err := modzip.CreateFromDir(tmpZip, m, tmpDir); err != nil {
235                                 if test.wantErr == "" {
236                                         t.Fatalf("unexpected error: %v", err)
237                                 } else if !strings.Contains(err.Error(), test.wantErr) {
238                                         t.Fatalf("got error %q; want error containing %q", err, test.wantErr)
239                                 } else {
240                                         return
241                                 }
242                         } else if test.wantErr != "" {
243                                 t.Fatalf("unexpected success; want error containing %q", test.wantErr)
244                         }
245
246                         // Hash zip file, compare with known value.
247                         if hash, err := dirhash.HashZip(tmpZipPath, dirhash.Hash1); err != nil {
248                                 t.Fatal(err)
249                         } else if hash != test.hash {
250                                 t.Fatalf("got hash: %q\nwant: %q", hash, test.hash)
251                         }
252                 })
253         }
254 }
255
256 func TestCreateFromDirSpecial(t *testing.T) {
257         for _, test := range []struct {
258                 desc     string
259                 setup    func(t *testing.T, tmpDir string) string
260                 wantHash string
261         }{
262                 {
263                         desc: "ignore_empty_dir",
264                         setup: func(t *testing.T, tmpDir string) string {
265                                 if err := os.Mkdir(filepath.Join(tmpDir, "empty"), 0777); err != nil {
266                                         t.Fatal(err)
267                                 }
268                                 return tmpDir
269                         },
270                         wantHash: emptyHash,
271                 }, {
272                         desc: "ignore_symlink",
273                         setup: func(t *testing.T, tmpDir string) string {
274                                 if err := os.Symlink(tmpDir, filepath.Join(tmpDir, "link")); err != nil {
275                                         switch runtime.GOOS {
276                                         case "plan9", "windows":
277                                                 t.Skipf("could not create symlink: %v", err)
278                                         default:
279                                                 t.Fatal(err)
280                                         }
281                                 }
282                                 return tmpDir
283                         },
284                         wantHash: emptyHash,
285                 }, {
286                         desc: "dir_is_vendor",
287                         setup: func(t *testing.T, tmpDir string) string {
288                                 vendorDir := filepath.Join(tmpDir, "vendor")
289                                 if err := os.Mkdir(vendorDir, 0777); err != nil {
290                                         t.Fatal(err)
291                                 }
292                                 goModData := []byte("module example.com/m\n\ngo 1.13\n")
293                                 if err := ioutil.WriteFile(filepath.Join(vendorDir, "go.mod"), goModData, 0666); err != nil {
294                                         t.Fatal(err)
295                                 }
296                                 return vendorDir
297                         },
298                         wantHash: "h1:XduFAgX/GaspZa8Jv4pfzoGEzNaU/r88PiCunijw5ok=",
299                 },
300         } {
301                 t.Run(test.desc, func(t *testing.T) {
302                         tmpDir, err := ioutil.TempDir("", "TestCreateFromDirSpecial-"+test.desc)
303                         if err != nil {
304                                 t.Fatal(err)
305                         }
306                         defer os.RemoveAll(tmpDir)
307                         dir := test.setup(t, tmpDir)
308
309                         tmpZipFile, err := ioutil.TempFile("", "TestCreateFromDir-*.zip")
310                         if err != nil {
311                                 t.Fatal(err)
312                         }
313                         tmpZipPath := tmpZipFile.Name()
314                         defer func() {
315                                 tmpZipFile.Close()
316                                 os.Remove(tmpZipPath)
317                         }()
318
319                         m := module.Version{Path: "example.com/m", Version: "v1.0.0"}
320                         if err := modzip.CreateFromDir(tmpZipFile, m, dir); err != nil {
321                                 t.Fatal(err)
322                         }
323                         if err := tmpZipFile.Close(); err != nil {
324                                 t.Fatal(err)
325                         }
326
327                         if hash, err := dirhash.HashZip(tmpZipPath, dirhash.Hash1); err != nil {
328                                 t.Fatal(err)
329                         } else if hash != test.wantHash {
330                                 t.Fatalf("got hash %q; want %q", hash, emptyHash)
331                         }
332                 })
333         }
334 }
335
336 func TestUnzip(t *testing.T) {
337         testDir := filepath.FromSlash("testdata/unzip")
338         testInfos, err := ioutil.ReadDir(testDir)
339         if err != nil {
340                 t.Fatal(err)
341         }
342         for _, testInfo := range testInfos {
343                 base := filepath.Base(testInfo.Name())
344                 if filepath.Ext(base) != ".txt" {
345                         continue
346                 }
347                 t.Run(base[:len(base)-len(".txt")], func(t *testing.T) {
348                         // Load the test.
349                         testPath := filepath.Join(testDir, testInfo.Name())
350                         test, err := readTest(testPath)
351                         if err != nil {
352                                 t.Fatal(err)
353                         }
354
355                         // Convert txtar to temporary zip file.
356                         tmpZipFile, err := ioutil.TempFile("", "TestUnzip-*.zip")
357                         if err != nil {
358                                 t.Fatal(err)
359                         }
360                         tmpZipPath := tmpZipFile.Name()
361                         defer func() {
362                                 tmpZipFile.Close()
363                                 os.Remove(tmpZipPath)
364                         }()
365                         zw := zip.NewWriter(tmpZipFile)
366                         for _, f := range test.archive.Files {
367                                 zf, err := zw.Create(f.Name)
368                                 if err != nil {
369                                         t.Fatal(err)
370                                 }
371                                 if _, err := zf.Write(f.Data); err != nil {
372                                         t.Fatal(err)
373                                 }
374                         }
375                         if err := zw.Close(); err != nil {
376                                 t.Fatal(err)
377                         }
378                         if err := tmpZipFile.Close(); err != nil {
379                                 t.Fatal(err)
380                         }
381
382                         // Extract to a temporary directory.
383                         tmpDir, err := ioutil.TempDir("", "TestUnzip")
384                         if err != nil {
385                                 t.Fatal(err)
386                         }
387                         defer os.RemoveAll(tmpDir)
388                         m := module.Version{Path: test.path, Version: test.version}
389                         if err := modzip.Unzip(tmpDir, m, tmpZipPath); err != nil {
390                                 if test.wantErr == "" {
391                                         t.Fatalf("unexpected error: %v", err)
392                                 } else if !strings.Contains(err.Error(), test.wantErr) {
393                                         t.Fatalf("got error %q; want error containing %q", err.Error(), test.wantErr)
394                                 } else {
395                                         return
396                                 }
397                         } else if test.wantErr != "" {
398                                 t.Fatalf("unexpected success; wanted error containing %q", test.wantErr)
399                         }
400
401                         // Hash the directory, compare to known value.
402                         prefix := fmt.Sprintf("%s@%s/", test.path, test.version)
403                         if hash, err := dirhash.HashDir(tmpDir, prefix, dirhash.Hash1); err != nil {
404                                 t.Fatal(err)
405                         } else if hash != test.hash {
406                                 t.Fatalf("got hash %q\nwant: %q", hash, test.hash)
407                         }
408                 })
409         }
410 }
411
412 type sizeLimitTest struct {
413         desc                                 string
414         files                                []modzip.File
415         wantErr, wantCreateErr, wantUnzipErr string
416 }
417
418 // sizeLimitTests is shared by TestCreateSizeLimits and TestUnzipSizeLimits.
419 var sizeLimitTests = [...]sizeLimitTest{
420         {
421                 desc: "one_large",
422                 files: []modzip.File{fakeFile{
423                         name: "large.go",
424                         size: modzip.MaxZipFile,
425                 }},
426         }, {
427                 desc: "one_too_large",
428                 files: []modzip.File{fakeFile{
429                         name: "large.go",
430                         size: modzip.MaxZipFile + 1,
431                 }},
432                 wantCreateErr: "module source tree too large",
433                 wantUnzipErr:  "total uncompressed size of module contents too large",
434         }, {
435                 desc: "total_large",
436                 files: []modzip.File{
437                         fakeFile{
438                                 name: "small.go",
439                                 size: 10,
440                         },
441                         fakeFile{
442                                 name: "large.go",
443                                 size: modzip.MaxZipFile - 10,
444                         },
445                 },
446         }, {
447                 desc: "total_too_large",
448                 files: []modzip.File{
449                         fakeFile{
450                                 name: "small.go",
451                                 size: 10,
452                         },
453                         fakeFile{
454                                 name: "large.go",
455                                 size: modzip.MaxZipFile - 9,
456                         },
457                 },
458                 wantCreateErr: "module source tree too large",
459                 wantUnzipErr:  "total uncompressed size of module contents too large",
460         }, {
461                 desc: "large_gomod",
462                 files: []modzip.File{fakeFile{
463                         name: "go.mod",
464                         size: modzip.MaxGoMod,
465                 }},
466         }, {
467                 desc: "too_large_gomod",
468                 files: []modzip.File{fakeFile{
469                         name: "go.mod",
470                         size: modzip.MaxGoMod + 1,
471                 }},
472                 wantErr: "go.mod file too large",
473         }, {
474                 desc: "large_license",
475                 files: []modzip.File{fakeFile{
476                         name: "LICENSE",
477                         size: modzip.MaxLICENSE,
478                 }},
479         }, {
480                 desc: "too_large_license",
481                 files: []modzip.File{fakeFile{
482                         name: "LICENSE",
483                         size: modzip.MaxLICENSE + 1,
484                 }},
485                 wantErr: "LICENSE file too large",
486         },
487 }
488
489 var sizeLimitVersion = module.Version{Path: "example.com/large", Version: "v1.0.0"}
490
491 func TestCreateSizeLimits(t *testing.T) {
492         if testing.Short() {
493                 t.Skip("creating large files takes time")
494         }
495         tests := append(sizeLimitTests[:], sizeLimitTest{
496                 // negative file size may happen when size is represented as uint64
497                 // but is cast to int64, as is the case in zip files.
498                 desc: "negative",
499                 files: []modzip.File{fakeFile{
500                         name: "neg.go",
501                         size: 0x8000000000000000,
502                 }},
503                 wantErr: "module source tree too large",
504         }, sizeLimitTest{
505                 desc: "size_is_a_lie",
506                 files: []modzip.File{fakeFile{
507                         name: "lie.go",
508                         size: 1,
509                         data: []byte(`package large`),
510                 }},
511                 wantErr: "larger than declared size",
512         })
513
514         for _, test := range tests {
515                 test := test
516                 t.Run(test.desc, func(t *testing.T) {
517                         t.Parallel()
518                         wantErr := test.wantCreateErr
519                         if wantErr == "" {
520                                 wantErr = test.wantErr
521                         }
522                         if err := modzip.Create(ioutil.Discard, sizeLimitVersion, test.files); err == nil && wantErr != "" {
523                                 t.Fatalf("unexpected success; want error containing %q", wantErr)
524                         } else if err != nil && wantErr == "" {
525                                 t.Fatalf("got error %q; want success", err)
526                         } else if err != nil && !strings.Contains(err.Error(), wantErr) {
527                                 t.Fatalf("got error %q; want error containing %q", err, wantErr)
528                         }
529                 })
530         }
531 }
532
533 func TestUnzipSizeLimits(t *testing.T) {
534         if testing.Short() {
535                 t.Skip("creating large files takes time")
536         }
537         for _, test := range sizeLimitTests {
538                 test := test
539                 t.Run(test.desc, func(t *testing.T) {
540                         t.Parallel()
541                         tmpZipFile, err := ioutil.TempFile("", "TestUnzipSizeLimits-*.zip")
542                         if err != nil {
543                                 t.Fatal(err)
544                         }
545                         tmpZipPath := tmpZipFile.Name()
546                         defer func() {
547                                 tmpZipFile.Close()
548                                 os.Remove(tmpZipPath)
549                         }()
550
551                         zw := zip.NewWriter(tmpZipFile)
552                         prefix := fmt.Sprintf("%s@%s/", sizeLimitVersion.Path, sizeLimitVersion.Version)
553                         for _, tf := range test.files {
554                                 zf, err := zw.Create(prefix + tf.Path())
555                                 if err != nil {
556                                         t.Fatal(err)
557                                 }
558                                 rc, err := tf.Open()
559                                 if err != nil {
560                                         t.Fatal(err)
561                                 }
562                                 _, err = io.Copy(zf, rc)
563                                 rc.Close()
564                                 if err != nil {
565                                         t.Fatal(err)
566                                 }
567                         }
568                         if err := zw.Close(); err != nil {
569                                 t.Fatal(err)
570                         }
571                         if err := tmpZipFile.Close(); err != nil {
572                                 t.Fatal(err)
573                         }
574
575                         tmpDir, err := ioutil.TempDir("", "TestUnzipSizeLimits")
576                         if err != nil {
577                                 t.Fatal(err)
578                         }
579                         defer os.RemoveAll(tmpDir)
580                         wantErr := test.wantUnzipErr
581                         if wantErr == "" {
582                                 wantErr = test.wantErr
583                         }
584                         if err := modzip.Unzip(tmpDir, sizeLimitVersion, tmpZipPath); err == nil && wantErr != "" {
585                                 t.Fatalf("unexpected success; want error containing %q", wantErr)
586                         } else if err != nil && wantErr == "" {
587                                 t.Fatalf("got error %q; want success", err)
588                         } else if err != nil && !strings.Contains(err.Error(), wantErr) {
589                                 t.Fatalf("got error %q; want error containing %q", err, wantErr)
590                         }
591                 })
592         }
593 }
594
595 func TestUnzipSizeLimitsSpecial(t *testing.T) {
596         if testing.Short() {
597                 t.Skip("skipping test; creating large files takes time")
598         }
599
600         for _, test := range []struct {
601                 desc, wantErr string
602                 m             module.Version
603                 writeZip      func(t *testing.T, zipFile *os.File)
604         }{
605                 {
606                         desc: "large_zip",
607                         m:    module.Version{Path: "example.com/m", Version: "v1.0.0"},
608                         writeZip: func(t *testing.T, zipFile *os.File) {
609                                 if err := zipFile.Truncate(modzip.MaxZipFile); err != nil {
610                                         t.Fatal(err)
611                                 }
612                         },
613                         // this is not an error we care about; we're just testing whether
614                         // Unzip checks the size of the file before opening.
615                         // It's harder to create a valid zip file of exactly the right size.
616                         wantErr: "not a valid zip file",
617                 }, {
618                         desc: "too_large_zip",
619                         m:    module.Version{Path: "example.com/m", Version: "v1.0.0"},
620                         writeZip: func(t *testing.T, zipFile *os.File) {
621                                 if err := zipFile.Truncate(modzip.MaxZipFile + 1); err != nil {
622                                         t.Fatal(err)
623                                 }
624                         },
625                         wantErr: "module zip file is too large",
626                 }, {
627                         desc: "size_is_a_lie",
628                         m:    module.Version{Path: "example.com/m", Version: "v1.0.0"},
629                         writeZip: func(t *testing.T, zipFile *os.File) {
630                                 // Create a normal zip file in memory containing one file full of zero
631                                 // bytes. Use a distinctive size so we can find it later.
632                                 zipBuf := &bytes.Buffer{}
633                                 zw := zip.NewWriter(zipBuf)
634                                 f, err := zw.Create("example.com/m@v1.0.0/go.mod")
635                                 if err != nil {
636                                         t.Fatal(err)
637                                 }
638                                 realSize := 0x0BAD
639                                 buf := make([]byte, realSize)
640                                 if _, err := f.Write(buf); err != nil {
641                                         t.Fatal(err)
642                                 }
643                                 if err := zw.Close(); err != nil {
644                                         t.Fatal(err)
645                                 }
646
647                                 // Replace the uncompressed size of the file. As a shortcut, we just
648                                 // search-and-replace the byte sequence. It should occur twice because
649                                 // the 32- and 64-byte sizes are stored separately. All multi-byte
650                                 // values are little-endian.
651                                 zipData := zipBuf.Bytes()
652                                 realSizeData := []byte{0xAD, 0x0B}
653                                 fakeSizeData := []byte{0xAC, 0x00}
654                                 s := zipData
655                                 n := 0
656                                 for {
657                                         if i := bytes.Index(s, realSizeData); i < 0 {
658                                                 break
659                                         } else {
660                                                 s = s[i:]
661                                         }
662                                         copy(s[:len(fakeSizeData)], fakeSizeData)
663                                         n++
664                                 }
665                                 if n != 2 {
666                                         t.Fatalf("replaced size %d times; expected 2", n)
667                                 }
668
669                                 // Write the modified zip to the actual file.
670                                 if _, err := zipFile.Write(zipData); err != nil {
671                                         t.Fatal(err)
672                                 }
673                         },
674                         wantErr: "uncompressed size of file example.com/m@v1.0.0/go.mod is larger than declared size",
675                 },
676         } {
677                 test := test
678                 t.Run(test.desc, func(t *testing.T) {
679                         t.Parallel()
680                         tmpZipFile, err := ioutil.TempFile("", "TestUnzipSizeLimitsSpecial-*.zip")
681                         if err != nil {
682                                 t.Fatal(err)
683                         }
684                         tmpZipPath := tmpZipFile.Name()
685                         defer func() {
686                                 tmpZipFile.Close()
687                                 os.Remove(tmpZipPath)
688                         }()
689
690                         test.writeZip(t, tmpZipFile)
691                         if err := tmpZipFile.Close(); err != nil {
692                                 t.Fatal(err)
693                         }
694
695                         tmpDir, err := ioutil.TempDir("", "TestUnzipSizeLimitsSpecial")
696                         if err != nil {
697                                 t.Fatal(err)
698                         }
699                         defer os.RemoveAll(tmpDir)
700                         if err := modzip.Unzip(tmpDir, test.m, tmpZipPath); err == nil && test.wantErr != "" {
701                                 t.Fatalf("unexpected success; want error containing %q", test.wantErr)
702                         } else if err != nil && test.wantErr == "" {
703                                 t.Fatalf("got error %q; want success", err)
704                         } else if err != nil && !strings.Contains(err.Error(), test.wantErr) {
705                                 t.Fatalf("got error %q; want error containing %q", err, test.wantErr)
706                         }
707                 })
708         }
709 }
710
711 // TestVCS clones a repository, creates a zip for a known version,
712 // and verifies the zip file itself has the same SHA-256 hash as the one
713 // 'go mod download' produces.
714 //
715 // This test is intended to build confidence that this implementation produces
716 // the same output as the go command, given the same VCS zip input. This is
717 // not intended to be a complete conformance test. The code that produces zip
718 // archives from VCS repos is based on the go command, but it's for testing
719 // only, and we don't export it.
720 //
721 // Note that we test the hash of the zip file itself. This is stricter than
722 // testing the hash of the content, which is what we've promised users.
723 // It's okay if the zip hash changes without changing the content hash, but
724 // we should not let that happen accidentally.
725 func TestVCS(t *testing.T) {
726         if testing.Short() {
727                 t.Skip()
728         }
729
730         var downloadErrorCount int32
731         const downloadErrorLimit = 3
732
733         haveVCS := make(map[string]bool)
734         for _, vcs := range []string{"git", "hg"} {
735                 _, err := exec.LookPath(vcs)
736                 haveVCS[vcs] = err == nil
737         }
738
739         for _, test := range []struct {
740                 m                            module.Version
741                 vcs, url, subdir, rev        string
742                 wantContentHash, wantZipHash string
743         }{
744                 // Simple tests: all versions of rsc.io/quote + newer major versions
745                 {
746                         m:               module.Version{Path: "rsc.io/quote", Version: "v1.0.0"},
747                         vcs:             "git",
748                         url:             "https://github.com/rsc/quote",
749                         rev:             "v1.0.0",
750                         wantContentHash: "h1:haUSojyo3j2M9g7CEUFG8Na09dtn7QKxvPGaPVQdGwM=",
751                         wantZipHash:     "5c08ba2c09a364f93704aaa780e7504346102c6ef4fe1333a11f09904a732078",
752                 },
753                 {
754                         m:               module.Version{Path: "rsc.io/quote", Version: "v1.1.0"},
755                         vcs:             "git",
756                         url:             "https://github.com/rsc/quote",
757                         rev:             "v1.1.0",
758                         wantContentHash: "h1:n/ElL9GOlVEwL0mVjzaYj0UxTI/TX9aQ7lR5LHqP/Rw=",
759                         wantZipHash:     "730a5ae6e5c4e216e4f84bb93aa9785a85630ad73f96954ebb5f9daa123dcaa9",
760                 },
761                 {
762                         m:               module.Version{Path: "rsc.io/quote", Version: "v1.2.0"},
763                         vcs:             "git",
764                         url:             "https://github.com/rsc/quote",
765                         rev:             "v1.2.0",
766                         wantContentHash: "h1:fFMCNi0A97hfNrtUZVQKETbuc3h7bmfFQHnjutpPYCg=",
767                         wantZipHash:     "fe1bd62652e9737a30d6b7fd396ea13e54ad13fb05f295669eb63d6d33290b06",
768                 },
769                 {
770                         m:               module.Version{Path: "rsc.io/quote", Version: "v1.2.1"},
771                         vcs:             "git",
772                         url:             "https://github.com/rsc/quote",
773                         rev:             "v1.2.1",
774                         wantContentHash: "h1:l+HtgC05eds8qgXNApuv6g1oK1q3B144BM5li1akqXY=",
775                         wantZipHash:     "9f0e74de55a6bd20c1567a81e707814dc221f07df176af2a0270392c6faf32fd",
776                 },
777                 {
778                         m:               module.Version{Path: "rsc.io/quote", Version: "v1.3.0"},
779                         vcs:             "git",
780                         url:             "https://github.com/rsc/quote",
781                         rev:             "v1.3.0",
782                         wantContentHash: "h1:aPUoHx/0Cd7BTZs4SAaknT4TaKryH766GcFTvJjVbHU=",
783                         wantZipHash:     "03872ee7d6747bc2ee0abadbd4eb09e60f6df17d0a6142264abe8a8a00af50e7",
784                 },
785                 {
786                         m:               module.Version{Path: "rsc.io/quote", Version: "v1.4.0"},
787                         vcs:             "git",
788                         url:             "https://github.com/rsc/quote",
789                         rev:             "v1.4.0",
790                         wantContentHash: "h1:tYuJspOzwTRMUOX6qmSDRTEKFVV80GM0/l89OLZuVNg=",
791                         wantZipHash:     "f60be8193c607bf197da01da4bedb3d683fe84c30de61040eb5d7afaf7869f2e",
792                 },
793                 {
794                         m:               module.Version{Path: "rsc.io/quote", Version: "v1.5.0"},
795                         vcs:             "git",
796                         url:             "https://github.com/rsc/quote",
797                         rev:             "v1.5.0",
798                         wantContentHash: "h1:mVjf/WMWxfIw299sOl/O3EXn5qEaaJPMDHMsv7DBDlw=",
799                         wantZipHash:     "a2d281834ce159703540da94425fa02c7aec73b88b560081ed0d3681bfe9cd1f",
800                 },
801                 {
802                         m:               module.Version{Path: "rsc.io/quote", Version: "v1.5.1"},
803                         vcs:             "git",
804                         url:             "https://github.com/rsc/quote",
805                         rev:             "v1.5.1",
806                         wantContentHash: "h1:ptSemFtffEBvMed43o25vSUpcTVcqxfXU8Jv0sfFVJs=",
807                         wantZipHash:     "4ecd78a6d9f571e84ed2baac1688fd150400db2c5b017b496c971af30aaece02",
808                 },
809                 {
810                         m:               module.Version{Path: "rsc.io/quote", Version: "v1.5.2"},
811                         vcs:             "git",
812                         url:             "https://github.com/rsc/quote",
813                         rev:             "v1.5.2",
814                         wantContentHash: "h1:w5fcysjrx7yqtD/aO+QwRjYZOKnaM9Uh2b40tElTs3Y=",
815                         wantZipHash:     "643fcf8ef4e4cbb8f910622c42df3f9a81f3efe8b158a05825a81622c121ca0a",
816                 },
817                 {
818                         m:               module.Version{Path: "rsc.io/quote", Version: "v1.5.3-pre1"},
819                         vcs:             "git",
820                         url:             "https://github.com/rsc/quote",
821                         rev:             "v1.5.3-pre1",
822                         wantContentHash: "h1:c3EJ21kn75/hyrOL/Dvj45+ifxGFSY8Wf4WBcoWTxF0=",
823                         wantZipHash:     "24106f0f15384949df51fae5d34191bf120c3b80c1c904721ca2872cf83126b2",
824                 },
825                 {
826                         m:               module.Version{Path: "rsc.io/quote/v2", Version: "v2.0.1"},
827                         vcs:             "git",
828                         url:             "https://github.com/rsc/quote",
829                         rev:             "v2.0.1",
830                         wantContentHash: "h1:DF8hmGbDhgiIa2tpqLjHLIKkJx6WjCtLEqZBAU+hACI=",
831                         wantZipHash:     "009ed42474a59526fe56a14a9dd02bd7f977d1bd3844398bd209d0da0484aade",
832                 },
833                 {
834                         m:               module.Version{Path: "rsc.io/quote/v3", Version: "v3.0.0"},
835                         vcs:             "git",
836                         url:             "https://github.com/rsc/quote",
837                         rev:             "v3.0.0",
838                         subdir:          "v3",
839                         wantContentHash: "h1:OEIXClZHFMyx5FdatYfxxpNEvxTqHlu5PNdla+vSYGg=",
840                         wantZipHash:     "cf3ff89056b785d7b3ef3a10e984efd83b47d9e65eabe8098b927b3370d5c3eb",
841                 },
842
843                 // Test cases from vcs-test.golang.org
844                 {
845                         m:               module.Version{Path: "vcs-test.golang.org/git/v3pkg.git/v3", Version: "v3.0.0"},
846                         vcs:             "git",
847                         url:             "https://vcs-test.golang.org/git/v3pkg",
848                         rev:             "v3.0.0",
849                         wantContentHash: "h1:mZhljS1BaiW8lODR6wqY5pDxbhXja04rWPFXPwRAtvA=",
850                         wantZipHash:     "9c65f0d235e531008dc04e977f6fa5d678febc68679bb63d4148dadb91d3fe57",
851                 },
852                 {
853                         m:               module.Version{Path: "vcs-test.golang.org/go/custom-hg-hello", Version: "v0.0.0-20171010233936-a8c8e7a40da9"},
854                         vcs:             "hg",
855                         url:             "https://vcs-test.golang.org/hg/custom-hg-hello",
856                         rev:             "a8c8e7a40da9",
857                         wantContentHash: "h1:LU6jFCbwn5VVgTcj+y4LspOpJHLZvl5TGPE+LwwpMw4=",
858                         wantZipHash:     "a1b12047da979d618c639ee98f370767a13d0507bd77785dc2f8dad66b40e2e6",
859                 },
860
861                 // Latest versions of selected golang.org/x repos
862                 {
863                         m:               module.Version{Path: "golang.org/x/arch", Version: "v0.0.0-20190927153633-4e8777c89be4"},
864                         vcs:             "git",
865                         url:             "https://go.googlesource.com/arch",
866                         rev:             "4e8777c89be4d9e61691fbe5d4e6c8838a7806f3",
867                         wantContentHash: "h1:QlVATYS7JBoZMVaf+cNjb90WD/beKVHnIxFKT4QaHVI=",
868                         wantZipHash:     "d17551a0c4957180ec1507065d13dcdd0f5cd8bfd7dd735fb81f64f3e2b31b68",
869                 },
870                 {
871                         m:               module.Version{Path: "golang.org/x/blog", Version: "v0.0.0-20191017104857-0cd0cdff05c2"},
872                         vcs:             "git",
873                         url:             "https://go.googlesource.com/blog",
874                         rev:             "0cd0cdff05c251ad0c796cc94d7059e013311fc6",
875                         wantContentHash: "h1:IKGICrORhR1aH2xG/WqrnpggSNolSj5urQxggCfmj28=",
876                         wantZipHash:     "0fed6b400de54da34b52b464ef2cdff45167236aaaf9a99ba8eba8855036faff",
877                 },
878                 {
879                         m:               module.Version{Path: "golang.org/x/crypto", Version: "v0.0.0-20191011191535-87dc89f01550"},
880                         vcs:             "git",
881                         url:             "https://go.googlesource.com/crypto",
882                         rev:             "87dc89f01550277dc22b74ffcf4cd89fa2f40f4c",
883                         wantContentHash: "h1:ObdrDkeb4kJdCP557AjRjq69pTHfNouLtWZG7j9rPN8=",
884                         wantZipHash:     "88e47aa05eb25c6abdad7387ccccfc39e74541896d87b7b1269e9dd2fa00100d",
885                 },
886                 {
887                         m:               module.Version{Path: "golang.org/x/net", Version: "v0.0.0-20191014212845-da9a3fd4c582"},
888                         vcs:             "git",
889                         url:             "https://go.googlesource.com/net",
890                         rev:             "da9a3fd4c5820e74b24a6cb7fb438dc9b0dd377c",
891                         wantContentHash: "h1:p9xBe/w/OzkeYVKm234g55gMdD1nSIooTir5kV11kfA=",
892                         wantZipHash:     "34901a85e6c15475a40457c2393ce66fb0999accaf2d6aa5b64b4863751ddbde",
893                 },
894                 {
895                         m:               module.Version{Path: "golang.org/x/sync", Version: "v0.0.0-20190911185100-cd5d95a43a6e"},
896                         vcs:             "git",
897                         url:             "https://go.googlesource.com/sync",
898                         rev:             "cd5d95a43a6e21273425c7ae415d3df9ea832eeb",
899                         wantContentHash: "h1:vcxGaoTs7kV8m5Np9uUNQin4BrLOthgV7252N8V+FwY=",
900                         wantZipHash:     "9c63fe51b0c533b258d3acc30d9319fe78679ce1a051109c9dea3105b93e2eef",
901                 },
902                 {
903                         m:               module.Version{Path: "golang.org/x/sys", Version: "v0.0.0-20191010194322-b09406accb47"},
904                         vcs:             "git",
905                         url:             "https://go.googlesource.com/sys",
906                         rev:             "b09406accb4736d857a32bf9444cd7edae2ffa79",
907                         wantContentHash: "h1:/XfQ9z7ib8eEJX2hdgFTZJ/ntt0swNk5oYBziWeTCvY=",
908                         wantZipHash:     "f26f2993757670b4d1fee3156d331513259757f17133a36966c158642c3f61df",
909                 },
910                 {
911                         m:               module.Version{Path: "golang.org/x/talks", Version: "v0.0.0-20191010201600-067e0d331fee"},
912                         vcs:             "git",
913                         url:             "https://go.googlesource.com/talks",
914                         rev:             "067e0d331feee4f8d0fa17d47444db533bd904e7",
915                         wantContentHash: "h1:8fnBMBUwliuiHuzfFw6kSSx79AzQpqkjZi3FSNIoqYs=",
916                         wantZipHash:     "fab2129f3005f970dbf2247378edb3220f6bd36726acdc7300ae3bb0f129e2f2",
917                 },
918                 {
919                         m:               module.Version{Path: "golang.org/x/tools", Version: "v0.0.0-20191017205301-920acffc3e65"},
920                         vcs:             "git",
921                         url:             "https://go.googlesource.com/tools",
922                         rev:             "920acffc3e65862cb002dae6b227b8d9695e3d29",
923                         wantContentHash: "h1:GwXwgmbrvlcHLDsENMqrQTTIC2C0kIPszsq929NruKI=",
924                         wantZipHash:     "7f0ab7466448190f8ad1b8cfb05787c3fb08f4a8f9953cd4b40a51c76ddebb28",
925                 },
926                 {
927                         m:               module.Version{Path: "golang.org/x/tour", Version: "v0.0.0-20191002171047-6bb846ce41cd"},
928                         vcs:             "git",
929                         url:             "https://go.googlesource.com/tour",
930                         rev:             "6bb846ce41cdca087b14c8e3560a679691c424b6",
931                         wantContentHash: "h1:EUlK3Rq8iTkQERnCnveD654NvRJ/ZCM9XCDne+S5cJ8=",
932                         wantZipHash:     "d6a7e03e02e5f7714bd12653d319a3b0f6e1099c01b1f9a17bc3613fb31c9170",
933                 },
934         } {
935                 test := test
936                 testName := strings.ReplaceAll(test.m.String(), "/", "_")
937                 t.Run(testName, func(t *testing.T) {
938                         if have, ok := haveVCS[test.vcs]; !ok {
939                                 t.Fatalf("unknown vcs: %s", test.vcs)
940                         } else if !have {
941                                 t.Skip()
942                         }
943                         t.Parallel()
944
945                         repo, dl, cleanup, err := downloadVCSZip(test.vcs, test.url, test.rev, test.subdir)
946                         defer cleanup()
947                         if err != nil {
948                                 // This may fail if there's a problem with the network or upstream
949                                 // repository. The package being tested doesn't directly interact with
950                                 // VCS tools; the test just does this to simulate what the go command
951                                 // does. So an error should cause a skip instead of a failure. But we
952                                 // should fail after too many errors so we don't lose test coverage
953                                 // when something changes permanently.
954                                 n := atomic.AddInt32(&downloadErrorCount, 1)
955                                 if n < downloadErrorLimit {
956                                         t.Skipf("failed to download zip from repository: %v", err)
957                                 } else {
958                                         t.Fatalf("failed to download zip from repository (repeated failure): %v", err)
959                                 }
960                         }
961
962                         // Create a module zip from that archive.
963                         // (adapted from cmd/go/internal/modfetch.codeRepo.Zip)
964                         info, err := dl.Stat()
965                         if err != nil {
966                                 t.Fatal(err)
967                         }
968                         zr, err := zip.NewReader(dl, info.Size())
969                         if err != nil {
970                                 t.Fatal(err)
971                         }
972
973                         var files []modzip.File
974                         topPrefix := ""
975                         subdir := test.subdir
976                         if subdir != "" && !strings.HasSuffix(subdir, "/") {
977                                 subdir += "/"
978                         }
979                         haveLICENSE := false
980                         for _, f := range zr.File {
981                                 if !f.FileInfo().Mode().IsRegular() {
982                                         continue
983                                 }
984                                 if topPrefix == "" {
985                                         i := strings.Index(f.Name, "/")
986                                         if i < 0 {
987                                                 t.Fatal("missing top-level directory prefix")
988                                         }
989                                         topPrefix = f.Name[:i+1]
990                                 }
991                                 if strings.HasSuffix(f.Name, "/") { // drop directory dummy entries
992                                         continue
993                                 }
994                                 if !strings.HasPrefix(f.Name, topPrefix) {
995                                         t.Fatal("zip file contains more than one top-level directory")
996                                 }
997                                 name := strings.TrimPrefix(f.Name, topPrefix)
998                                 if !strings.HasPrefix(name, subdir) {
999                                         continue
1000                                 }
1001                                 name = strings.TrimPrefix(name, subdir)
1002                                 if name == ".hg_archival.txt" {
1003                                         // Inserted by hg archive.
1004                                         // Not correct to drop from other version control systems, but too bad.
1005                                         continue
1006                                 }
1007                                 if name == "LICENSE" {
1008                                         haveLICENSE = true
1009                                 }
1010                                 files = append(files, zipFile{name: name, f: f})
1011                         }
1012                         if !haveLICENSE && subdir != "" {
1013                                 license, err := downloadVCSFile(test.vcs, repo, test.rev, "LICENSE")
1014                                 if err != nil {
1015                                         t.Fatal(err)
1016                                 }
1017                                 files = append(files, fakeFile{
1018                                         name: "LICENSE",
1019                                         size: uint64(len(license)),
1020                                         data: license,
1021                                 })
1022                         }
1023
1024                         tmpModZipFile, err := ioutil.TempFile("", "TestVCS-*.zip")
1025                         if err != nil {
1026                                 t.Fatal(err)
1027                         }
1028                         tmpModZipPath := tmpModZipFile.Name()
1029                         defer func() {
1030                                 tmpModZipFile.Close()
1031                                 os.Remove(tmpModZipPath)
1032                         }()
1033                         h := sha256.New()
1034                         w := io.MultiWriter(tmpModZipFile, h)
1035                         if err := modzip.Create(w, test.m, files); err != nil {
1036                                 t.Fatal(err)
1037                         }
1038                         if err := tmpModZipFile.Close(); err != nil {
1039                                 t.Fatal(err)
1040                         }
1041
1042                         gotZipHash := hex.EncodeToString(h.Sum(nil))
1043                         if test.wantZipHash != gotZipHash {
1044                                 // If the test fails because the hash of the zip file itself differs,
1045                                 // that may be okay as long as the hash of the data within the zip file
1046                                 // does not change. For example, we might change the compression,
1047                                 // order, or alignment of files without affecting the extracted output.
1048                                 // We shouldn't make such a change unintentionally though, so this
1049                                 // test will fail either way.
1050                                 if gotSum, err := dirhash.HashZip(tmpModZipPath, dirhash.Hash1); err == nil && test.wantContentHash != gotSum {
1051                                         t.Fatalf("zip content hash: got %s, want %s", gotSum, test.wantContentHash)
1052                                 } else {
1053                                         t.Fatalf("zip file hash: got %s, want %s", gotZipHash, test.wantZipHash)
1054                                 }
1055                         }
1056                 })
1057         }
1058 }
1059
1060 func downloadVCSZip(vcs, url, rev, subdir string) (repoDir string, dl *os.File, cleanup func(), err error) {
1061         var cleanups []func()
1062         cleanup = func() {
1063                 for i := len(cleanups) - 1; i >= 0; i-- {
1064                         cleanups[i]()
1065                 }
1066         }
1067         repoDir, err = ioutil.TempDir("", "downloadVCSZip")
1068         if err != nil {
1069                 return "", nil, cleanup, err
1070         }
1071         cleanups = append(cleanups, func() { os.RemoveAll(repoDir) })
1072
1073         switch vcs {
1074         case "git":
1075                 // Create a repository and download the revision we want.
1076                 if err := run(repoDir, "git", "init", "--bare"); err != nil {
1077                         return "", nil, cleanup, err
1078                 }
1079                 if err := os.MkdirAll(filepath.Join(repoDir, "info"), 0777); err != nil {
1080                         return "", nil, cleanup, err
1081                 }
1082                 attrFile, err := os.OpenFile(filepath.Join(repoDir, "info", "attributes"), os.O_CREATE|os.O_APPEND|os.O_RDWR, 0666)
1083                 if err != nil {
1084                         return "", nil, cleanup, err
1085                 }
1086                 if _, err := attrFile.Write([]byte("\n* -export-subst -export-ignore\n")); err != nil {
1087                         attrFile.Close()
1088                         return "", nil, cleanup, err
1089                 }
1090                 if err := attrFile.Close(); err != nil {
1091                         return "", nil, cleanup, err
1092                 }
1093                 if err := run(repoDir, "git", "remote", "add", "origin", "--", url); err != nil {
1094                         return "", nil, cleanup, err
1095                 }
1096                 var refSpec string
1097                 if strings.HasPrefix(rev, "v") {
1098                         refSpec = fmt.Sprintf("refs/tags/%[1]s:refs/tags/%[1]s", rev)
1099                 } else {
1100                         refSpec = fmt.Sprintf("%s:refs/dummy", rev)
1101                 }
1102                 if err := run(repoDir, "git", "fetch", "-f", "--depth=1", "origin", refSpec); err != nil {
1103                         return "", nil, cleanup, err
1104                 }
1105
1106                 // Create an archive.
1107                 tmpZipFile, err := ioutil.TempFile("", "downloadVCSZip-*.zip")
1108                 if err != nil {
1109                         return "", nil, cleanup, err
1110                 }
1111                 cleanups = append(cleanups, func() {
1112                         name := tmpZipFile.Name()
1113                         tmpZipFile.Close()
1114                         os.Remove(name)
1115                 })
1116                 subdirArg := subdir
1117                 if subdir == "" {
1118                         subdirArg = "."
1119                 }
1120                 cmd := exec.Command("git", "-c", "core.autocrlf=input", "-c", "core.eol=lf", "archive", "--format=zip", "--prefix=prefix/", rev, "--", subdirArg)
1121                 cmd.Dir = repoDir
1122                 cmd.Stdout = tmpZipFile
1123                 if err := cmd.Run(); err != nil {
1124                         return "", nil, cleanup, err
1125                 }
1126                 if _, err := tmpZipFile.Seek(0, 0); err != nil {
1127                         return "", nil, cleanup, err
1128                 }
1129                 return repoDir, tmpZipFile, cleanup, nil
1130
1131         case "hg":
1132                 // Clone the whole repository.
1133                 if err := run(repoDir, "hg", "clone", "-U", "--", url, "."); err != nil {
1134                         return "", nil, cleanup, err
1135                 }
1136
1137                 // Create an archive.
1138                 tmpZipFile, err := ioutil.TempFile("", "downloadVCSZip-*.zip")
1139                 if err != nil {
1140                         return "", nil, cleanup, err
1141                 }
1142                 tmpZipPath := tmpZipFile.Name()
1143                 tmpZipFile.Close()
1144                 cleanups = append(cleanups, func() { os.Remove(tmpZipPath) })
1145                 args := []string{"archive", "-t", "zip", "--no-decode", "-r", rev, "--prefix=prefix/"}
1146                 if subdir != "" {
1147                         args = append(args, "-I", subdir+"/**")
1148                 }
1149                 args = append(args, "--", tmpZipPath)
1150                 if err := run(repoDir, "hg", args...); err != nil {
1151                         return "", nil, cleanup, err
1152                 }
1153                 if tmpZipFile, err = os.Open(tmpZipPath); err != nil {
1154                         return "", nil, cleanup, err
1155                 }
1156                 cleanups = append(cleanups, func() { tmpZipFile.Close() })
1157                 return repoDir, tmpZipFile, cleanup, err
1158
1159         default:
1160                 return "", nil, cleanup, fmt.Errorf("vcs %q not supported", vcs)
1161         }
1162 }
1163
1164 func downloadVCSFile(vcs, repo, rev, file string) ([]byte, error) {
1165         switch vcs {
1166         case "git":
1167                 cmd := exec.Command("git", "cat-file", "blob", rev+":"+file)
1168                 cmd.Dir = repo
1169                 return cmd.Output()
1170         default:
1171                 return nil, fmt.Errorf("vcs %q not supported", vcs)
1172         }
1173 }
1174
1175 func run(dir string, name string, args ...string) error {
1176         cmd := exec.Command(name, args...)
1177         cmd.Dir = dir
1178         if err := cmd.Run(); err != nil {
1179                 return fmt.Errorf("%s: %v", strings.Join(args, " "), err)
1180         }
1181         return nil
1182 }
1183
1184 type zipFile struct {
1185         name string
1186         f    *zip.File
1187 }
1188
1189 func (f zipFile) Path() string                 { return f.name }
1190 func (f zipFile) Lstat() (os.FileInfo, error)  { return f.f.FileInfo(), nil }
1191 func (f zipFile) Open() (io.ReadCloser, error) { return f.f.Open() }